Resources
Skills under Phase 2 live as (kind=KIND_SKILL, name=<skillName>, version_index=N) slots in the unified SoulContent object. Each skill name has its own independent version vector, with public vs private visibility chosen per version.
// All versions of one skill:
items[ContentKey { kind: 2 /* KIND_SKILL */, name: "my-skill-name" }]
→ vector<ContentSlot>
// One version:
ContentSlot {
version: u64,
kind: 2,
blob_object_id: ID, // Walrus Blob
is_public: bool, // chosen per version at append
deleted: bool,
purged: bool,
download_policy: u8, // 0=public, 1=owner_only, 2=allowlist
grant_scope_mask: 4, // SCOPE_SKILLS
read_mode_mask: OWNER | GRANT,
op_mask: APPEND | DELETE | PURGE,
seal_encrypted: true,
created_at_ms: u64,
}version_index is the 0-based index into the slot vector. There is no shared SoulSkills object after Phase 2 — skills live alongside memory, sprites, audio, and soul.md under the single SoulContent root.
Skills must be uploaded as .zip archives. The upload validator enforces this before the Walrus upload. The ZIP root must contain a SKILL.md with a name front-matter field.
---
name: my-skill-name # on-chain slot name (lowercase, digits, dash, underscore)
version: 1.0.0 # human-readable version label (not the index)
description: |
What this skill does.
---
# Skill content hereThe name front-matter becomes the on-chain slot name. Re-uploading the same name appends a new version_index on the same slot vector. A fresh name creates a new slot starting at version_index = 0. The human-readable version is informational only; canonical version is the on-chain index.
Each version is individually marked is_public at append time. Built-in KIND_SKILL uses read_mode_mask = OWNER | GRANT — there is no READ_PAID or READ_PUBLIC on built-in skills, so visibility is binary on the slot via the is_public flag combined with download_policy:
| Visibility | Access model |
|---|---|
| public | Walrus blob URL returned directly. No Seal session required. The slot is still Seal-encrypted at rest with the owner Seal path always available. |
| private | Requires owner wallet or active SoulGrant with SCOPE_SKILLS. Client builds the approval TX and runs Seal decryption. |
Visibility is immutable per version. To change visibility you must append a new version with the desired setting.
When the owner appends a private skill version, Soulidity auto-issues scope-matched grants to every active agent that doesn't already cover SCOPE_SKILLS. Existing scopes are preserved via the grant-merge-masks pre-check. Failures surface on the My Souls page for retry — see Agent Integration.
Public versions are not auto-granted — they require no grant to read.
content::delete_version_as_owner or delete_version_as_granted_agent) flips deleted = true. The version index is preserved; reads abort with EVersionDeleted.content::purge_deleted_version_as_owner) is owner-only and only valid after soft delete. It clears the on-chain blob pointer.GET /api/souls/[id]/content/2/my-skill/N/access
// Public response
{
visibility: "public",
artifact: { walrusBlobUrl, walrusBlobId, blobObjectId }
}
// Private response
{
visibility: "private",
artifact: { walrusBlobUrl, walrusBlobId, blobObjectId },
accessPolicy: {
packageId,
stateObjectId,
contentObjectId,
kind: 2,
name: "my-skill",
versionIndex: N,
moduleName: "content",
functionName:
"seal_approve_content_owner"
| "seal_approve_content_granted_agent",
soulGrantObjectId: string | null,
documentIdHex: string,
},
seal: { ... },
sealSidecar: { ... },
viewerAddress, accessKind, sessionTtlMin
}The content-slot access endpoint /api/souls/[id]/content/[kind]/[name]/[versionIndex]/access serves specific skill versions. The legacy /api/souls/[id]/access route resolves only the canonical Soul document at (KIND_SOUL_DOC, "soul", 0).