Resources
Phase 2 collapsed six legacy Move modules (memory, skills, assets, metadata, content_access, seal_policy) into a single typed-content matrix on the SoulContent root. Every content slot belongs to a kind, and the on-chain KindRegistry is the source of truth for what each kind can do.
Five built-in kinds are pre-registered at kind_registry::init. Their ids are reserved (0..=4) and cannot be re-issued. Ids 5..=15 are reserved for future built-ins; custom kinds start at FIRST_CUSTOM_KIND = 16.
| Kind | Id | op_mask | read_mode_mask | Active bind | grant_scope |
|---|---|---|---|---|---|
| soul_doc | 0 | ∅ (mint-only) | OWNER | GRANT | no | SCOPE_SEAL (1) |
| memory | 1 | APPEND | DELETE | PURGE | OWNER | GRANT | no | SCOPE_MEMORY (2) |
| skill | 2 | APPEND | DELETE | PURGE | OWNER | GRANT | no | SCOPE_SKILLS (4) |
| sprite | 3 | APPEND | DELETE | PURGE | ACTIVE_BIND | OWNER | GRANT | PAID | PUBLIC | yes | SCOPE_ASSETS (8) |
| audio | 4 | APPEND | DELETE | PURGE | ACTIVE_BIND | OWNER | GRANT | PAID | PUBLIC | yes | SCOPE_ASSETS (8) |
soul_doc is the immutable Soul bundle — appended exactly once at mint, never deleted or amended. The other four kinds support post-mint mutation.
public struct KindDescriptor has copy, drop, store {
version: u64,
kind: u32,
name: String, // canonical lowercase bytes [a-z0-9_-]
op_mask: u64, // APPEND | DELETE | PURGE | ACTIVE_BIND
read_mode_mask: u64, // OWNER | GRANT | PAID | PUBLIC (OWNER required)
has_active_binding: bool, // lock-step with OP_ACTIVE_BIND
requires_download_policy: bool, // double-implication with READ_PUBLIC
default_grant_scope_mask: u64, // single grant-scope bit when GRANT/PAID allowed
deprecated: bool,
}op_mask is snapshotted onto each ContentSlot at append time. Historical slots keep working even if the kind is later deprecated.read_mode_mask picks which Seal approval functions a kind exposes. READ_OWNER is mandatory — owner approval never disappears, even for public slots (they remain Seal-encrypted at rest).default_grant_scope_mask is exactly one of SCOPE_SEAL / SCOPE_MEMORY / SCOPE_SKILLS / SCOPE_ASSETS when grant or paid reads are allowed, and zero otherwise. The single-bit rule is enforced by assert_valid_default_grant_scope — combined masks are rejected so the slot's cached scope stays unambiguous at Seal time.Kinds with OP_ACTIVE_BIND in their op mask support an active table on the SoulContent root — active_table[KIND_SPRITE] and active_table[KIND_AUDIO] hold the currently-selected name + version. Owners set or clear bindings via the unified content::set_active / clear_active entries.
The desktop companion reads these active bindings to pick which persona art and voice to render. Switching personas is a single OP_ACTIVE_BIND TX — no content reupload required.
Custom kinds are admin-only. Holding the KindAdminCap object, an admin can call:
public fun register_kind(
registry: &mut KindRegistry,
_: &KindAdminCap,
name: String,
op_mask: u64,
read_mode_mask: u64,
has_active_binding: bool,
requires_download_policy: bool,
default_grant_scope_mask: u64,
_ctx: &mut TxContext,
): u32[a-z0-9_-], length 1..=32; duplicates are rejected.next_kind counter starting at 16; allocation is fail-closed.assert_descriptor_well_formed apply: subset op/read bits, owner-required, active-binding lock-step, public ↔ download-policy double-implication, and the single-bit grant-scope rule when grant or paid reads are allowed.deprecate_kind) or reactivate one (reactivate_kind). Deprecation blocks new appends but keeps existing slots operable through their cached op masks.The KindRegistryCreated, KindRegistered, KindDeprecated, and KindReactivated events stream all registry mutations for indexers.
On every append the content module:
kind_registry::assert_kind_active.op_mask.read_mode_mask as the slot's slot_read_mode_mask; this is cached onto the slot.op_mask and default_grant_scope_mask onto the slot. Future delete / purge / set_active checks consult the slot, not the registry — so the kind's rules at append time remain authoritative for the lifetime of that slot.The TypeScript SDK exports the same constants and a frozen BUILTIN_KIND_DESCRIPTORS table in @soulidity/sdk (web/lib/soulidity/kinds.ts). Always import from there rather than hard-coding the numbers in client code — the table is the single client-side source so any drift between Move and TS is caught at compile time.