Skip to content

Move module registry: on-chain namespace ownership object linking to Walrus blob + Harbor/SEAL refs #20

Open

Description

@harrymove-ctrl

Context

Namespace ownership, visibility, and access are 100% off-chain in D1 today — CloudflareNamespaceStore in apps/api/src/worker.ts stores owner_id + visibility + ctxm_* read tokens, and docs/namespace-model.md confirms "private" is just a Bearer-token check, not anything on-chain. This module makes namespace ownership a real Sui object so it can be the authority record for provenance (roadmap §2) and the link target between a namespace and its Walrus blob / Harbor Bucket / SEAL policy.

Goal / user story

As a namespace owner, my namespace exists as an on-chain Namespace object that records who owns it, whether it's public/private, its SEAL policy (when private), and a pointer to its latest attribution receipt — so ownership and provenance are verifiable on Sui, not just trusted from a D1 row.

Acceptance criteria

  • module contextmem::registry defines a Namespace struct with: name: String (the demo:/web:/walrus: namespace string from docs/namespace-model.md), owner: address, visibility: u8 (0=public,1=private), seal_policy_id: Option<ID>, display_name: String, description: String, walrus_blob_id: Option<String>, created_at_ms: u64, head_receipt: Option<ID>.
  • public fun register_namespace(...) creates a shared Namespace (so the sponsored service signer and later readers can both touch it) and emits a NamespaceRegistered event.
  • Owner-gated mutators (assert ns.owner == ctx.sender() or an explicit cap): set_visibility, set_seal_policy, set_display, set_walrus_blob, transfer_ownership.
  • public fun set_head_receipt(ns: &mut Namespace, receipt_id: ID, ctx) — owner/authority gated — lets the receipt-mint flow advance the head pointer (consumed by the receipt module / on-chain TS client).
  • #[test] coverage: register, owner-only mutation succeeds, non-owner mutation aborts, visibility/seal-policy round-trip.
  • Getters for all fields for off-chain reads.

Implementation notes

  • Source-of-truth decision (roadmap open question §2): for the alpha, keep D1 authoritative and the on-chain Namespace as an attestation/mirror — far less plumbing than making chain authoritative. Document this; the TS client backfills/syncs D1 → chain.
  • Visibility ↔ SEAL: visibility=1 (private) should carry a seal_policy_id populated from Harbor's on-chain Seal policy object (private namespace ⇔ owned Seal policy, per roadmap §1). Confirm Harbor exposes the policy id to store here. walrus_blob_id mirrors the WalrusStorageReceipt.blobId.
  • Authority model: with the scaffold's signer decision (service key signs, Enoki sponsors gas), ns.owner will initially be the service key address. If/when user wallets land, transfer_ownership hands it to the user. Avoid baking user-wallet assumptions in now.
  • Cross-module head pointer: set_head_receipt is the seam the receipt mint calls; keep the auth check here so receipt stays a pure attestation minter. A dedicated WriterCap (roadmap §2 P2 cap module) can replace the sender-equality check later — out of scope.
  • Shared vs owned: prefer shared Namespace so a sponsored backend tx can mutate head_receipt without the object being in the user's address; document the consensus-cost tradeoff.

Sui Overflow angle

Turns "namespace ownership" from an invisible database column into a first-class Sui object that links Walrus storage + SEAL access control + attribution receipts into one verifiable graph — the on-chain backbone judges can inspect, and the natural home for the private-namespace SEAL story.

Dependencies

  • Requires the Move scaffold issue.
  • Pairs with the receipt module (its head_receipt/parents reference these objects).
  • SEAL policy id linkage depends on Harbor integration landing — the field can ship now and be populated later.

Part of the ContextMEM roadmap (#4) • Sui Overflow build.

Activity

  1. added this to the Sui Overflow milestone on Jun 16, 2026
  2. added
    P1Important: hardens the demo and core product
    featureUser- or agent-facing capability
    moveSui Move smart-contract package (registry/receipt)
    suiSui chain: tx signing, objects, wallet, zkLogin, explorer
    on Jun 16, 2026
  3. harrymove-ctrl commented on Jun 19, 2026

    @harrymove-ctrl
    OwnerAuthor

    Implementation Plan

    Approach
    Author one new Move module, contextmem::registry, in the package stood up by the scaffold issue (#8) — this replaces the empty move/contextmem/sources/registry.move stub. It defines a shared Namespace object that mirrors today's D1 contextmem_namespaces row (owner/visibility/display/description + new on-chain pointers seal_policy_id, walrus_blob_id, head_receipt), with register_namespace + owner-gated mutators + a set_head_receipt seam for the receipt-mint flow. Key design decisions: (1) D1 stays authoritative, the on-chain object is an attestation/mirror (per issue note + roadmap §2 open-Q #9) — so this module deliberately does NOT enforce on-chain name uniqueness; idempotency is the TS client's job (#21). (2) head_receipt is stored as an opaque Option<ID> so registry never imports receipt — avoiding a circular module dependency while still letting a PTB pass a freshly-minted receipt id. (3) Shared (not owned) so an Enoki-sponsored backend tx signed by the service key can advance head_receipt without the object living in a user address.

    Prerequisites & dependencies

    Step-by-step

    1. Confirm scaffold is in place. Verify move/contextmem/Move.toml, move/contextmem/sources/registry.move (stub module contextmem::registry {}), and move/contextmem/tests/ exist (from Scaffold contextmem Move package (Move.toml, module stubs) + CI build/test + testnet publish #8). Run sui move build to confirm a green baseline before editing.
    2. Define the Namespace struct + constants in move/contextmem/sources/registry.move. Use std::string::String, sui::object::{Self, ID, UID}, std::option::{Self, Option}, sui::clock::Clock, sui::event, sui::tx_context::TxContext. Struct (has key, store):
      • id: UID, name: String, owner: address, visibility: u8, seal_policy_id: Option<ID>, display_name: String, description: String, walrus_blob_id: Option<String>, created_at_ms: u64, head_receipt: Option<ID>.
      • Constants: const VISIBILITY_PUBLIC: u8 = 0; const VISIBILITY_PRIVATE: u8 = 1; and abort codes const ENotOwner: u64 = 0; const EBadVisibility: u64 = 1;.
      • name maps to the demo:/web:/walrus:/target: string from namespaceForTarget (packages/core/src/utils.ts:123) / docs/namespace-model.md; walrus_blob_id mirrors WalrusStorageReceipt.blobId (packages/core/src/types.ts:655); visibility/owner/display_name/description mirror the D1 columns read in CloudflareNamespaceStore (apps/api/src/worker.ts:401, SELECT at :415).
    3. public fun register_namespace(name, owner, visibility, display_name, description, clock: &Clock, ctx: &mut TxContext): assert visibility ∈ {0,1} (EBadVisibility); let ns = Namespace { id: object::new(ctx), name, owner, visibility, seal_policy_id: option::none(), display_name, description, walrus_blob_id: option::none(), created_at_ms: clock.timestamp_ms(), head_receipt: option::none() }; capture object::uid_to_inner(&ns.id) for the event; event::emit(NamespaceRegistered { id, name: ns.name, owner, visibility }); transfer::share_object(ns). Define public struct NamespaceRegistered has copy, drop { id: ID, name: String, owner: address, visibility: u8 }. (Take owner as a param rather than ctx.sender() so the TS client can register on behalf of the service key explicitly.)
    4. Owner-gated mutators — each does assert!(ns.owner == ctx.sender(), ENotOwner) first, all public fun ... (ns: &mut Namespace, ..., ctx: &TxContext):
      • set_visibility(ns, visibility: u8, ctx) — also assert visibility valid.
      • set_seal_policy(ns, seal_policy_id: ID, ctx) — ns.seal_policy_id = option::some(seal_policy_id) (and a clear_seal_policy for the public path, optional).
      • set_display(ns, display_name: String, description: String, ctx).
      • set_walrus_blob(ns, blob_id: String, ctx) — option::some(...).
      • transfer_ownership(ns, new_owner: address, ctx) — set ns.owner = new_owner (reserved for when user wallets land).
      • Factor the check into fun assert_owner(ns: &Namespace, ctx: &TxContext).
    5. public fun set_head_receipt(ns: &mut Namespace, receipt_id: ID, ctx: &TxContext) — owner-gated via assert_owner; ns.head_receipt = option::some(receipt_id). This is the seam the mint PTB (On-chain attribution receipts: sign/submit receipt+registry PTBs and emit a receipt on every triad call #21) calls after receipt::mint_receipt returns the new id; keep the auth check here so receipt stays a pure attestation minter (a WriterCap can replace the sender check later — roadmap §2 P2, out of scope). Document that this requires mint_receipt to return the ID (coordinate with Move module receipt: typed on-chain attribution receipt (op / digests / model / parent lineage) #9) so a single PTB can mint then advance the head.
    6. Getters for every field (off-chain reads via devInspect): public fun name(&Namespace): String, owner(): address, visibility(): u8, seal_policy_id(): Option<ID>, display_name(): String, description(): String, walrus_blob_id(): Option<String>, created_at_ms(): u64, head_receipt(): Option<ID>. (String/ID/Option of those all have copy, so return-by-value is fine.)
    7. Tests in move/contextmem/tests/registry_tests.move using sui::test_scenario + sui::clock::create_for_testing:
      • register → take the shared object, assert getters equal seeded values; assert NamespaceRegistered emitted (test_scenario::take_shared + check via test_scenario::next_tx event count / returned effects).
      • owner-only mutation succeeds: as owner, run set_visibility/set_seal_policy/set_display/set_walrus_blob/set_head_receipt/transfer_ownership, assert via getters (visibility + seal-policy round-trip explicitly).
      • non-owner mutation aborts: #[test, expected_failure(abort_code = contextmem::registry::ENotOwner)] calling a mutator from a second address.
      • EBadVisibility path: #[expected_failure] on register_namespace/set_visibility with visibility = 2.
    8. README note in move/contextmem/README.md (extend Scaffold contextmem Move package (Move.toml, module stubs) + CI build/test + testnet publish #8's): document the D1-authoritative / chain-mirror decision, the shared-vs-owned consensus-cost tradeoff, and the alpha owner == service-key-address model.
    9. Build green: sui move build && sui move test locally; PR triggers move.yml (Scaffold contextmem Move package (Move.toml, module stubs) + CI build/test + testnet publish #8). Optionally trigger the workflow_dispatch publish to refresh published.testnet.json so On-chain attribution receipts: sign/submit receipt+registry PTBs and emit a receipt on every triad call #21 has the packageId (coordinate so Move module receipt: typed on-chain attribution receipt (op / digests / model / parent lineage) #9 lands in the same publish).

    New / changed files

    Test plan

    • Unit (authoritative gate): sui move test covers register, owner mutation success, non-owner abort (expected_failure on ENotOwner), bad-visibility abort, and visibility/seal-policy/head-receipt round-trip via getters — runs in move.yml on every move/** PR.
    • Integration / proof it works: after workflow_dispatch publishes to testnet (Scaffold contextmem Move package (Move.toml, module stubs) + CI build/test + testnet publish #8), run a real sui client call --package <pkgId> --module registry --function register_namespace ... (Clock at 0x6) — produces a concrete testnet tx digest + a shared Namespace object id viewable on suiscan/suiexplorer, with NamespaceRegistered in the tx events. Verify getters via sui client ptb / devInspect. (Full TS register→mutate→read round-trip is On-chain attribution receipts: sign/submit receipt+registry PTBs and emit a receipt on every triad call #21's test surface.)
    • Manual assert: confirm a non-owner set_visibility call fails on-chain (paste the abort), proving owner-gating is real, not a D1 token check.

    Risks & gotchas

    Acceptance-criteria mapping

    • Namespace struct with all 9 fields → Step 2.
    • register_namespace creates shared object + emits NamespaceRegistered → Step 3.
    • Owner-gated set_visibility/set_seal_policy/set_display/set_walrus_blob/transfer_ownership → Step 4.
    • Owner/authority-gated set_head_receipt(ns, receipt_id, ctx) → Step 5.
    • #[test] coverage (register, owner-only success, non-owner abort, visibility/seal-policy round-trip) → Step 7.
    • Getters for all fields → Step 6.

    Effort
    M — ~1.5–2 person-days. The module is small and fully specified (~150–220 lines incl. tests), but budget time for Sui test_scenario/Clock test ergonomics, getting sui move test green, freezing struct shapes jointly with #9, and a testnet publish dry-run. Hard-blocked on #8 landing first.


    Implementation plan generated for execution (planning phase). Part of the roadmap (#4).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Important: hardens the demo and core productfeatureUser- or agent-facing capabilitymoveSui Move smart-contract package (registry/receipt)suiSui chain: tx signing, objects, wallet, zkLogin, explorer

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions