feat: nested spaces — SpaceSign gate, legalOwner limits, keyless governance + external seats (SYN-51, SYN-54, SYN-57) - #134
Draft
cheggaaa wants to merge 10 commits into
Draft
feat: nested spaces — SpaceSign gate, legalOwner limits, keyless governance + external seats (SYN-51, SYN-54, SYN-57)#134cheggaaa wants to merge 10 commits into
cheggaaa wants to merge 10 commits into
Conversation
…g (SYN-51) - SpaceSignRequest.parentAclRecordId threaded through rpcHandler.SpaceSign - verifyNestedSpace — the trust gate for child spaces at sign time: parent exists, is active, is not tech/1-1 and not itself a child (v1 = single-level nesting); the child acl root mirrors the header's parentSpaceId and pins the parent's CURRENT owner (fresh read through the acl service — all acl writes flow through this coordinator, so its cache is current for records it accepted); the AclChildRegister record matches (record id, child space id, child acl root cid, not revoked) and its author held Admin+ at that record; parent options don't forbid children - limit accounting: child spaces are billed to the legalOwner — spacestatus.NewChildStatus stores billedIdentity on the space doc and enforces the legalOwner's sharedSpacesLimit at sign (net-new check; the creator's own identity/limits stay on the doc as before) - top-level signs reject a stray parentAclRecordId
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
…e (SYN-54) - SpaceDelete: the parent space's CURRENT owner may delete a child it cannot read — resolved fresh via acl.OwnerPubKey; the doc-identity predicate is skipped via SpaceDeletion.AuthorizedByLegalOwner (the coordinator already authorized). Top-level spaces keep the owner-only path unchanged - AclAddRecord: keyless-governance records (AclAccountRemoveNoRotate / AclLegalOwnerUpdate) must be authored by the parent's current owner — the fresh-derivation half of the hybrid legalOwner model: an ex-owner passes stale client validation but is rejected here at submit (fail-closed transfer window). Content sniffing only; anything opaque falls through to the full validation inside acl.AddRecord - space docs carry parentSpaceId (set at child sign) so delete/acl-add can resolve the parent without parsing headers
…registry (SYN-57) - accountlimit.Limits.ExternalSeatsLimit, settable by the payment node via AccountLimitsSet - AclAddRecord gate: admissions into a child space (direct adds, request accepts, invite joins) that name identities NOT in the parent acl are external seats, charged against ONE per-org pool of distinct external identities across all of the org's children (Q9 grooming decision), counted live from the acl states — no persistent counter to drift - externalSeats mongo registry (best-effort, maintained on admit/remove in the AclAddRecord path) backing the new ExternalCompartments rpc — the discovery surface for externals, who cannot read the parent's registrations; rows for deleted spaces are filtered lazily
…s, child type (review) - verifyNestedSpace: the pinned legalOwner must equal the parent's CURRENT owner only at child creation (isNew); on re-sign the parent may have transferred ownership since — previously every receipt renewal failed forever. Billing/authorization now targets the parent's current owner. - verifyExternalSeats: a child ACL that can't be counted now FAILS CLOSED (rejects the admission) instead of silently undercounting the pool and letting an over-limit external through. - child SpaceType tech/1-1 rejected at sign — such a child would be permanently undeletable (SpaceDelete refuses those types). - pin any-sync with the legalOwner-proof binding fix. Regression test: re-sign after a real parent a->b ownership transfer.
…ild type (review R2-1) - verifyNestedSpace now checks childAclRoot.ParentAclRootId == the parent's actual acl root id (st.Id()). Without it a registering admin could pin a binding scope they control, partially resurrecting the cross-space legalOwner-proof replay the any-sync fix targets. - require the V1 header for children at sign time (defense-in-depth; nodes also enforce it now). - child spaceType is allowlisted to Regular (was a denylist that let a Personal-classified header through). - ChildrenCreationDisallowed is enforced at CREATION only — enforcing it on re-sign would starve existing children of receipt renewals. Regression test: mismatched parentAclRootId rejected.
billedIdentity was pinned at child creation: after an org transfer the old and new owners held disjoint pools, so ping-ponging ownership across N accomplices multiplied the per-owner children allowance N times. ChangeOwner now migrates all children of the space to the new owner's pool in the same transaction, and a child re-sign refreshes a stale assignment to the billed identity the coordinator resolved from the parent's current owner.
…pass - The distinct-external pool was counted live across sibling children with nothing ordering the count against a concurrent admission's commit (different children = different consensus logs), so two admits could both see the pre-commit count and overshoot the paid limit. The check->commit window is now serialized per org via a striped lock. - Permission changes never routed through the gate: admit an external at None (uncounted by the steady-state scan), elevate to Writer, and the pool limit was bypassed. Non-None permission changes now count as admissions; demotions to None stay free.
Surfaced by de-skipping the SDK's nested-spaces e2e: - MakeSpaceShareable rejected non-owners before the already-shareable no-op, so an org admin ensuring the parent was shareable ahead of an AclChildRegister write got ErrForbidden even though the org was already shareable. The idempotent no-op now comes first; the actual flip stays owner-gated. - defaultLimits gains externalSeatsLimit: external seats default to a per-identity payment-node grant (0 without one), which is right for production but left self-hosted networks unable to admit any org-external member into a child. The config knob keeps 0 as the default.
The striped mutex around the seat check→commit window only covers one process: with several coordinator instances, two admissions into sibling children can land on different replicas, each pass the live count, and both commit past the limit (sibling children are different consensus logs, so nothing else orders them). Adds db.Lease — a mongo-backed per-key lease (TTL takeover, jittered polling, release survives a canceled request context) — and holds the org's lease across the window in addition to the local stripe. The live counting stays (it is drift-free ground truth); a reservation ledger was considered and rejected because every seat-freeing path (removal, demotion, child deletion, registration revoke) would need bookkeeping that drifts fail-closed when missed. Residual: a holder stalled past the 30s TTL loses exclusivity; the TTL sits well above the worst-case check→commit.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Nested spaces — coordinator validation, limits & keyless governance
Draft PR accumulating the coordinator side of nested (parent/child) spaces (SYN-51, SYN-54, SYN-57). Design: any-sync-sdk
docs/16-nested-spaces.md(anyproto/any-sync-sdk#30). Protocol side: anyproto/any-sync#725. Targetsfiles-v2; any-sync pinned to thecheggaaa/nested-spacesbranch.Landed (SYN-51 — SpaceSign trust gate + legalOwner limits)
rpcHandler.SpaceSignthreads the newSpaceSignRequest.parentAclRecordId.verifyNestedSpace— nothing else enforces parent authority, so at child sign the coordinator verifies: parent exists, is active, is not tech/1-1 and not itself a child (v1 = single-level nesting); the child ACL root mirrors the header'sparentSpaceIdand pins the parent's current owner (acl.ReadState— every ACL write flows through this coordinator, so its cache is current for records it accepted); theAclChildRegisterrecord matches on record id + child space id + child ACL root CID and is not revoked; its author held Admin+ at that record's position; parent options don't forbid children (childrenCreationDisallowed).spacestatus.NewChildStatusstoresbilledIdentityon the space doc and enforces the legalOwner'ssharedSpacesLimitat sign — a net-new check (top-level spaces only hit the global cap at sign today). The creator's identity stays on the doc; billing is a separate axis.parentAclRecordId.Landed (SYN-54 — phase 2, keyless governance)
SpaceDelete: the parent's current owner (freshacl.OwnerPubKey) may delete a child it cannot read; the doc-identity predicate is skipped viaSpaceDeletion.AuthorizedByLegalOwnersince the coordinator already authorized. Top-level path unchanged.AclAddRecordfail-closed gate: keyless-governance records (AclAccountRemoveNoRotate/AclLegalOwnerUpdate) must be authored by the parent's current owner — the fresh-derivation half of the hybrid legalOwner model. An ex-owner passes stale client validation (stored key) but is rejected here at submit. Content sniffing only; opaque payloads fall through to the full validation insideacl.AddRecord.parentSpaceId(set at child sign) so delete/ACL-add resolve the parent without parsing headers.Linear: SYN-51, SYN-54 (parent SYN-49).
🤖 Generated with Claude Code
Landed (SYN-57 — phase 5, external seats)
accountlimit.Limits.ExternalSeatsLimit, settable viaAccountLimitsSet.AclAddRecordgate: admissions into a child (direct adds, request accepts, invite joins) naming identities not in the parent ACL are external seats, charged against one per-org pool of distinct external identities across all the org's children (Q9 resolved: per-org total), counted live from the ACL states.ExternalCompartmentsRPC — discovery for externals (who cannot read the parent's registrations), backed by a best-effortexternalSeatsmongo registry maintained in the AclAddRecord path; deleted spaces filtered lazily.