Skip to content

feat(ndo): per-NDO-cell model A with DnaHash binding - #128

Merged
Soushi888 merged 9 commits into
devfrom
fix/120-clone-cell-dna-properties
Aug 16, 2026
Merged

feat(ndo): per-NDO-cell model A with DnaHash binding#128
Soushi888 merged 9 commits into
devfrom
fix/120-clone-cell-dna-properties

Conversation

@Soushi888

Copy link
Copy Markdown
Collaborator

Intent

Ship the v0.1.0 peer-mesh release on dev: two peers share a Group and an NDO over a live DHT via per-NDO cloned cells (ADR-010 model A). Each NDO gets its own cloned ndo cell whose DnaHash is cryptographically bound to the NDO identity through YAML-safe DNA properties (ADR-013 binding), so the group-to-NDO anchor alone lets any member derive, verify, and join the NDO network.

Changes

  • DNA-properties binding (ADR-013). NdoDnaProperties (name, property_regime, resource_nature, created_at) rides in the ndo clone's properties; validate_create_nondominium_identity rejects a create_ndo whose classification diverges. initiator is intentionally absent (YamlProperties has no binary variant); it stays on the entry and the anchor.
  • Create-NDO flow. Provision the ndo clone, write the genesis NondominiumIdentity inside, anchor it in the group cell with full clone coordinates (ndo_dna_hash, network_seed, identity action hash, cached classification).
  • Read paths. Lobby/group grids render from NdoAnchors without joining ndo cells. NDO detail resolves the ndo cell from the anchor (ensureNdoCloneCell) and reads the live entry; shared-cell legacy fallback retained.
  • Anchor refresh on transition. New refresh_ndo_anchor_lifecycle_stage resolves by NDO identity and updates the cached stage, so cards converge without a reload (walks the NdoAnchorUpdates chain so repeated refreshes stay visible).
  • Signing-race hardening. ensureNdoCloneCell no longer re-authorizes existing-enabled cells (source-chain-head-moved); connect path and lobby builder skip disabled clones.

Decisions

Option Rejected because
Client-side update_ndo_anchor after transition Would orphan the second transition's update. The NdoAnchorUpdates link must originate from the original create hash, which only the group link knows. A Rust resolver by identity is correct.
initiator in DNA properties YamlProperties(serde_yaml::Value) has no binary variant; an AgentPubKey hangs createCloneCell. The DnaHash binds classification plus microsecond creation time instead.
Global NDO registry Replaced by per-group NdoAnchors (ADR-011): the group DHT is the discovery scope.

How to test

  • bun run build:happ
  • CARGO_TARGET_DIR=target/native-tests cargo test -p group_sweettest --test ndo_anchor -- --test-threads 4 -> 9 passed
  • CARGO_TARGET_DIR=target/native-tests cargo test -p nondominium_sweettest --test nondominium -- --test-threads 3 -> 10 passed (suite over 8 tests needs limited threads)
  • bun run test:e2e -> 19 passed on a live 2-conductor DHT (create, browse, detail, transition, cross-dimension filters; two-agent group invite, shared NDO, live cross-agent transition read)

Documentation

No in-repo documentation/ changes here. The ADRs (010/011/012/013) and design of record live in .local/ (kept local); a separate docs pass will sync the NDO specification.

Related

@Soushi888

Soushi888 commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review round applied (2026-08-10)

Self-review of this PR raised 2 blockers and 6 suggestions. All 8 are addressed.

State: pushed as 153e689 (integrity + tests), 453bfba (frontend), 6a2eec2 (docs). The PR diff above now includes all of it.

Blockers

  1. associateNdoWithGroup was a silent no-op. NdoView exposes "Associate with a group" on every NDO page; it wrote a SoftLink, but after the anchor migration both read paths (getLobbyNdoDescriptors, getGroupNdoDescriptors) follow NdoAnchors only. The modal reported success and the NDO never appeared in the target group.
    Fix: new NdoService.associateNdoWithGroup writes a second NdoAnchor in the target group, copying the source anchor's clone coordinates (no-op if already anchored). The store delegates to it. createSoftLink / getSoftLinkTargetHashes now have no callers outside their own service definition and carry a @deprecated for NDO association note.

  2. Documentation currency. specifications.md §7.2 still described ndo.service.ts as SoftLink-backed, and ADR-010/011/012/013 lived only in .local/, so the repo carried no record of a change to the NDO Layer 0 identity model (which TELOS.md explicitly says must be flagged).
    Fix: new in-repo record at documentation/specifications/adr/ADR-010-013-per-ndo-cells.md (all four decisions, the YamlProperties constraint that shaped them, the skip path, and a code-location map), indexed in DOCUMENTATION_INDEX.md. Synced specifications.md §7.1 and §7.2, ui_architecture.md, ui_design.md, IMPLEMENTATION_STATUS.md, lobby-dna.md status rows, a new REQ-UI-GRP-06, and a new NDO Anchors section in API_REFERENCE.md documenting all four anchor externs.

Suggestions

  1. Orphaned NDO cells. The anchor write in createNdo was best-effort, and the anchor is the only pointer any read path follows, so a swallowed failure left a cell nobody could reach. It now fails the create, and the destination group is resolved before the clone cell is provisioned so a bad group fails cheaply.

  2. Anchor coordinates were immutable only by convention. validate_ndo_anchor ran on updates but checked nothing beyond name/seed, so an update could rewrite ndo_dna_hash / network_seed / identity_action_hash / initiator and repoint a group at a different network. Any group member can update any member's anchor, so the rule belongs in integrity: new validate_ndo_anchor_update on RegisterUpdate, written as rebuild-original-with-mutable-fields-swapped so a field added to NdoAnchor later is immutable by default.
    Covered by a new Sweettest that exercises the real attack path: update_ndo_anchor takes original_action_hash and previous_action_hash separately, so passing two different anchors writes A's coordinates over B through the public API. Integrity refuses it.

  3. Duplicated type mirrors. dnas/group/tests re-declared NdoCellProperties plus PropertyRegime / ResourceNature / LifecycleStage; this PR was itself repairing a drift in that mirror (a stale initiator field), and a drifted mirror silently derives a different DnaHash. The test crate now imports nondominium_shared::NdoDnaProperties. On the TS side NdoAnchorInput became an alias of NdoAnchorEntry.

  4. created_at precision. The comments claimed "microsecond-unique"; it is micros derived from a millisecond clock. Corrected in Rust, TS, and the ADR: distinctness comes from the per-NDO network_seed, which is also DnaHash input.

  5. Repeated anchor scans. One scanGroupAnchors() pass now feeds the detail read, getAssociatedGroupIds, and the post-transition refresh, which touches only the groups that actually anchor the NDO instead of every group.

Verification

Check Result
bun run build:happ 6 bundles, exit 0
cargo test -p group_sweettest --test ndo_anchor -- --test-threads 4 10 passed (287s), incl. new ndo_anchor_update_cannot_rewrite_identity_coordinates
cargo test -p nondominium_sweettest --test nondominium -- --test-threads 3 10 passed (1050s)
bun run check (svelte-check) 0 errors, 0 warnings
bunx eslint (changed TS files) clean
cd ui && bun run test:e2e 19 passed (5.9m), live 2-conductor DHT

Not done on purpose: prettier formatting. All four touched TS files were already prettier-unclean at HEAD (verified per file against git show HEAD:<path>) and CI runs no lint gate, so reformatting would add unrelated churn to a review-fix diff.

18 files changed, +480 / -217, plus the new ADR.

@Soushi888

Copy link
Copy Markdown
Collaborator Author

Review verdict: APPROVE (merge after #137)

Reviewed per REVIEW.md's six areas.

1. Holochain entry patterns — clean. NdoDnaProperties lives in the shared crate with the field-set-is-DnaHash-input warning; anchor updates walk the NdoAnchorUpdates chain from the original create hash, so repeated refreshes stay visible.

2. Capability and security — the right call landed in integrity, not the coordinator: validate_ndo_anchor_update restricts anchor updates to cached descriptor fields (name, description, lifecycle_stage) because any group member may update any anchor; identity coordinates are immutable, with a Sweettest asserting the rewrite is rejected (ndo_anchor_update_cannot_rewrite_identity_coordinates).

3. ValueFlows — no economic entries touched; N/A.

4. Sweettest — new ndo_anchor target (12 tests): DnaHash determinism from shared coordinates, genesis round-trip, rejection on divergent classification, anchor update cache refresh + no-op case, cross-agent join via anchor coordinates. Uses the shared-crate property definition instead of a hand-kept mirror (the mirror already drifted once on initiator). Registered as a [[test]] target.

5. Zome boundaries — shared types cross via nondominium_shared, the sanctioned pattern. No coordinator reaches into another's internals.

6. Documentation — ADR-010-013 added, API_REFERENCE.md, IMPLEMENTATION_STATUS.md, requirements and specs updated.

Known gap, accepted: the ADR-013 mirror validation covers name/regime/nature only; initiator/created_at equality is deferred to a create_ndo refactor (documented in the integrity comment). Distinctness is carried by the per-NDO network_seed, also DnaHash input, so the binding holds.

Sequence note: merging after #137 (dev e2e fix + sweettest gate) so this PR's merge gets the full backend gate it has never had in CI.

- Add NdoDnaProperties (immutable Layer 0 fields, excludes lifecycle_stage)
  to nondominium_shared so both integrity zomes and the UI consume one shape.
- zome_resource_integrity::validate_create_nondominium_identity now checks the
  entry agrees with dna_info().modifiers.properties when they deserialize to
  NdoDnaProperties (ndo cells cloned with properties). On the shared
  nondominium cell (properties: ~) deserialization fails and the check is
  skipped, so legacy shared-cell NDOs keep working.
- Add holochain_serialized_bytes dep to nondominium_shared for the SerializedBytes derive.
…ification-only binding

- NdoDnaProperties drops description (entry-only) to match the Sweettest
  NdoCellProperties mirror exactly, so client and tests derive the same DnaHash.
- Binding check validates name/regime/nature only (the classification fields).
  initiator/created_at are bound by the DnaHash but not entry-validated, since the
  entry sets them at create_ndo runtime and the Sweettest agent-key-per-cell
  artifact prevents equality. Full mirror validation deferred to the create_ndo
  refactor. Non-breaking: existing ndo_anchor Sweettests keep passing.
holochain 0.6.0 transports create_clone_cell properties as
YamlProperties(serde_yaml::Value), which has no binary variant and cannot
carry an AgentPubKey. An initiator in the properties made client
createCloneCell hang. NdoDnaProperties is now 4 YAML-safe fields
(name, property_regime, resource_nature, created_at); the DnaHash binds
classification + microsecond-unique creation time, and initiator stays
authoritative on the entry and cached on the NdoAnchor for display.
After an NDO lifecycle transition, lobby/group cards read the anchor's
cached lifecycle_stage; without a refresh the card stays at the
creation-time stage. refresh_ndo_anchor_lifecycle_stage resolves the
anchor by NDO identity (not action hash) via the GroupToNdoAnchors link
and follows the NdoAnchorUpdates chain to the latest version, so repeated
refreshes stay visible to get_ndo_anchors. The client knows only the NDO
identity; original-vs-latest anchor action hashes stay inside the group
link graph.

Refactored latest_anchor_record into latest_anchor (returns latest hash
+ record) so both get_ndo_anchors and the resolver share the chain walk.
Two new sweettests: refresh updates the cache (peer-visible); no-op when
the identity is absent.
Each NDO gets its own cloned ndo cell whose DnaHash is cryptographically
bound to the NDO identity via YAML-safe DNA properties (ADR-010 model A,
ADR-013 binding). createNdo provisions the clone, writes the genesis
NondominiumIdentity inside, and anchors it in the group cell with full
clone coordinates. Lobby/group grids read NdoAnchors; NDO detail resolves
the ndo cell from the anchor and reads the live entry. Lifecycle
transitions run on the ndo cell and refresh the group anchor so cards
converge without a reload.

ensureNdoCloneCell does not re-authorize existing-enabled cells (avoids
source-chain-head-moved races). Connect path skips disabled clones;
lobby descriptor builder skips disabled clones. e2e read-backs migrated
to per-cell reads; multi-agent live-transition discriminator added.
An anchor update may only refresh the cached descriptor (name, description,
lifecycle_stage). The identity coordinates are what a peer re-derives the NDO
cell from, so rewriting them silently repoints a group at a different network.
Any group member can update any member's anchor, so the rule belongs in
integrity rather than in the coordinator.

validate_ndo_anchor_update rebuilds the original with the mutable fields
swapped in and compares, so a field added to NdoAnchor later is immutable by
default and must be opted into mutability deliberately.

The new Sweettest exercises the attack through the real API: update_ndo_anchor
takes original_action_hash and previous_action_hash separately, so passing two
different anchors writes A's coordinates over B.

Also drops the hand-kept NdoDnaProperties mirror from the test crate in favour
of the shared definition (that mirror had already drifted once on initiator; a
drifted mirror derives a different DnaHash, the exact failure these tests
exist to catch), and corrects the created_at doc comment: it is micros from a
millisecond clock, not a uniqueness source, which is the per-NDO network_seed.
…SoftLink

NdoView exposes "Associate with a group" on every NDO page. It wrote a
SoftLink, but after the anchor migration both read paths follow NdoAnchors
only, so the modal reported success and the NDO never appeared in the target
group. NdoService.associateNdoWithGroup now writes a second NdoAnchor there,
copying the source anchor's clone coordinates, and no-ops if already anchored.
createSoftLink and getSoftLinkTargetHashes lose their last callers and are
marked deprecated for NDO association.

Two related hardenings:

- createNdo no longer swallows the anchor write. The anchor is the only
  pointer any read path follows, so a swallowed failure left a cell nobody
  could reach. The destination group is now resolved before the clone cell is
  provisioned, so a bad group fails cheaply instead of orphaning a cell.

- One scanGroupAnchors() pass answers every "where is this NDO anchored"
  question. The detail read, getAssociatedGroupIds and the post-transition
  refresh share it, and the refresh now touches only the groups that actually
  anchor the NDO instead of walking every group cell.

NdoAnchorInput becomes an alias of NdoAnchorEntry: the coordinator builds the
entry straight from the payload, so a second declaration could only drift.
The per-NDO-cell change touches the NDO Layer 0 identity model, which TELOS.md
flags as load-bearing, but ADR-010/011/012/013 lived only in .local/ so the
repo carried no record of it. Adds documentation/specifications/adr/
ADR-010-013-per-ndo-cells.md: the four decisions, the YamlProperties transport
constraint that forced initiator out of the DnaHash, the shared-cell skip path,
the open follow-ups, and a map of where each piece lives in code.

Then corrects the statements the migration made stale:

- specifications.md 7.1 gains NdoDnaProperties and NdoAnchorEntry; 7.2 replaces
  the SoftLink-backed ndo.service table with the anchor-backed one
- ui_architecture.md and ui_design.md describe anchors and per-NDO cells
- IMPLEMENTATION_STATUS.md lists NdoAnchor and the four anchor externs
- lobby-dna.md marks the NDO DNA and UI cutover rows shipped
- API_REFERENCE.md gains an NDO Anchors section for all four externs
- requirements.md gains REQ-UI-GRP-06 for the anchor model

SoftLink stays documented as a planning-level link, no longer as the group to
NDO pointer.
@Soushi888
Soushi888 force-pushed the fix/120-clone-cell-dna-properties branch from 6a2eec2 to b89cb22 Compare August 15, 2026 23:06
…rdown

The rebase onto dev (which now carries the clone-guard teardown and the
expectEmptyLobby precondition from #137) dropped this branch's changes to
the same file. Both are needed: the guard teardown and CTA precondition
come from dev, while the two DHT read-backs must enumerate the agent's ndo
clone cells rather than the shared nondominium cell, since under the
per-NDO-cell model the UI writes NondominiumIdentity into its own clone.
@Soushi888
Soushi888 merged commit 9060452 into dev Aug 16, 2026
7 checks passed
Soushi888 added a commit that referenced this pull request Aug 16, 2026
…fix)

Five conflicts, all resolved by keeping both sides rather than picking one:

- crates/shared/types.rs: #132's Rivalry/ResourceScope/OperationalState and
  dev's NdoDnaProperties are independent additions; both kept.
- zome_resource integrity: kept #132's regime-semantics hook AND dev's ADR-013
  binding check. Dropping the latter would silently remove the guarantee that a
  GovernanceRule's classification cannot diverge from Layer 0.
- ndo_anchor tests: took dev's shared-crate import (#128 deliberately replaced
  the hand-kept mirror, which had already drifted on `initiator`), then
  re-applied #132's additions on top: rivalry_override on the NdoInput/NdoEntry
  mirrors and the Public-regime anchor test. That test was written against the
  old mirror, so it needed adapting: NdoDnaProperties has no `initiator` field,
  and anchor_input_from now takes the initiator as a separate argument.
- ndo.service.ts: took dev's side. #132's mapListingToDescriptor is dead under
  the anchor model (zero callers) and its identityToDescriptor was a duplicate
  definition. rivalry_override survives in the retained field mapper.
- IMPLEMENTATION_STATUS.md: neither side was accurate. Arbitrated against the
  code: 20 externs, and no get_all_groups.
Soushi888 added a commit that referenced this pull request Aug 25, 2026
PR #128 moved every NDO into its own cloned `ndo` cell, but the Layer 1 and
Layer 2 UI kept calling `zome_resource` and `zome_gouvernance` on the shared
provisioned `nondominium` cell, where the NDO identity was never written.
Creating a resource specification therefore failed with
`Guest("Entry operation failed: Linked NondominiumIdentity not found")` from
`resource_specification.rs:53`, and every NDO-scoped read returned nothing.

`NdoService.resolveCellIdForNdo` resolves the NDO's clone cell from its anchor
coordinates, provisioning it for a peer who never joined, and returns null for
legacy NDOs still living in the shared cell so those keep working. `NdoView`
resolves it once per NDO and passes `ndoCellId` to the resources, governance
and activity tabs and to the four modals below them. An optional `cellId`
threads through `zome-helpers` into both zome services and both stores; every
call that does not pass one keeps its previous role-name routing.

Two further defects surfaced while verifying this in the browser:

- The governance tab's "+ New rule" handler carried a second, unrouted
  `fetchSpecificationsForNdo`. It returned nothing, so the rule was written
  with no `specification_hash` and no read path could surface it again. Routed,
  and guarded so the editor refuses to open when the NDO has no Layer 1 spec.

- `anchorToDescriptor` omitted `rivalry_override`, leaving `bun run check` red
  on the branch. The anchor caches only the card fields, so it is null there
  and the live read on open supplies the real value.

Verified in real Chrome against the 2-agent dev network: a specification and a
typed AccessRequirement rule both create and read back. A probe through the
app's own client confirms placement, `get_specifications_for_ndo` returning
0 for the shared cell, 0 for the provisioned `ndo` cell and 1 for the NDO's
own clone. `bun run check` reports 0 errors and 0 warnings.
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.

fix: createCloneCell with DNA properties fails from JS client (blocks #117)

1 participant