03. Spore - Package Format
The Spore is a Logic Capsule. It relies on its URI for identity and carries the logic DNA.
Location: Defined by capsules[].endpoints.spore in cmn.json (e.g., https://cmn.dev/cmn/spore/{hash}.json)
Schema: https://cmn.dev/schemas/v1/spore.json
1. The spore.json Manifest
{
"$schema": "https://cmn.dev/schemas/v1/spore.json",
"capsule": {
"uri": "cmn://cmn.dev/b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
"core": {
"id": "cmn-spec",
"name": "CMN Protocol Specification",
"domain": "cmn.dev",
"key": "ed25519.5XmkQ9vZP8nL3xJdFtR7wNcA6sY2bKgU1eH9pXb4",
"synopsis": "Code Mycelial Network - A sovereign-first protocol for code distribution",
"intent": ["add intent and changes array fields to spore core"],
"license": "CC0-1.0",
"mutations": [
"§2.2 Core Fields: intent type String → Array",
"§2.2 Core Fields: add mutations field (Array)",
"§2.4 Bond Types: add optional reason field",
"§1, §4.1, §6 example JSON updated"
],
"bonds": [],
"tree": {
"algorithm": "blob_tree_blake3_nfc",
"exclude_names": [".git"],
"follow_rules": [".gitignore"]
},
"updated_at_epoch_ms": 1700000000000
},
"core_signature": "ed25519.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa23yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
"dist": [
{
"type": "archive",
"filename": "cmn-spec.tar.zst"
}
]
},
"capsule_signature": "ed25519.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa23yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2"
}2. Field Definitions
2.1 Common Fields
| Field | Type | Description |
|---|---|---|
$schema | String | Schema URL: https://cmn.dev/schemas/v1/spore.json |
capsule | Object | The capsule container. |
capsule.uri | String | Full URI: cmn://{domain}/{hash} |
capsule_signature | String | Ed25519 signature of entire capsule object (ed25519.<base58>, JCS canonical). |
2.2 Core Fields (Immutable)
| Field | Type | Description |
|---|---|---|
capsule.core | Object | Immutable metadata (part of spore identity). |
capsule.core.name | String | Human-readable display name (e.g., CMN Protocol Specification). |
capsule.core.domain | String | The domain of the publisher (e.g., cmn.dev). |
capsule.core.key | String | Author’s Ed25519 public key (ed25519.<base58>). Embedded at release time — enables offline signature verification without fetching cmn.json. See 01-substrate §1.2.4 for trust model. |
capsule.core.synopsis | String | One-line summary — a visitor reads this alone and understands what the spore does. |
capsule.core.intent | Array | Multi-paragraph description of this spore’s functionality and purpose — what it does, how it works, why it exists. Each array item is a paragraph. Permanent — not cleared on release. |
capsule.core.mutations | Array | What changed relative to the spawned_from parent — describes the mutations applied to derive this spore from its ancestor. |
capsule.core.license | String | SPDX License Identifier. |
capsule.core.bonds | Array | List of {uri, relation, id?, reason?} (See §2.4 Bond Types). |
capsule.core.tree | Object | Tree hash configuration. |
capsule.core.tree.algorithm | String | Tree hash algorithm (e.g., blob_tree_blake3_nfc). |
capsule.core.tree.exclude_names | Array | Files/patterns to skip (e.g., [".git"]). |
capsule.core.tree.follow_rules | Array | Ignore systems to honor (e.g., [".gitignore"]). |
capsule.core.updated_at_epoch_ms | Number | Content update timestamp (milliseconds since Unix epoch). Publishers SHOULD derive it from the latest Git commit time for the source tree when available, with max file mtime as fallback. |
capsule.core_signature | String | Ed25519 signature of capsule.core (ed25519.<base58>, JCS canonical). |
2.2.1 Common Extensions
The following fields are optional and not required by the protocol. When present in core, they participate in the hash like any other core field.
| Field | Type | Description |
|---|---|---|
capsule.core.id | String | URL-safe path identifier (e.g., cmn-spec). Used by publishers for directory names, mycelium deduplication, and default export paths. |
capsule.core.version | String | Human-readable version (e.g., 1.0.0). Meaningful to the publisher; visitors address by hash. |
id vs name:
id: Used in paths and tooling (e.g.,cmn-spec.tar.zst). Must be URL-safe.name: Displayed to users. Can contain spaces and special characters.
2.3 Distribution Fields (Mutable)
| Field | Type | Description |
|---|---|---|
capsule.dist | Array | Physical source locations (e.g., git, ipfs). Replicate-friendly. MUST contain at least one entry. |
Each dist entry MUST be one of:
{ "type": "archive", "filename": "<filename>" }{ "type": "git", "url": "<url>", "ref"?: "<ref>" }{ "type": "ipfs", "cid": "<cid-or-uri>" }{ "type": "<extension>", ... }(protocol extension entry)
The first three are built-in v1 entries. archive uses endpoint indirection: filename is resolved through cmn.json endpoints.archive[].url. For future protocols, use the extension form with a type field (for example, { "type": "s3", "url": "..." }). Consumers that do not understand an extension type MAY skip that entry and continue trying other dist entries.
Incremental delivery note:
type=archiveis a full content snapshot transport.- Delta discovery is endpoint-driven (not dist-driven): clients MAY attempt
endpoints.archive[].delta_urlfirst when present. delta_urlMUST include{hash}(target hash) and{old_hash}(local cached base hash). Delta direction is alwaysold_hash -> hash.- Clients MUST use
archive[].formatto select decoders, not URL suffix guessing. - Current Hypha release generation emits
archive[].format = tar+zstdonly. - If delta fetch/apply fails, or no valid base is available, clients SHOULD fall back to full
type=archive. - Any optimization path (delta or full) MUST converge to the same final verified content hash (see §5.1).
2.4 Bond Types
Bonds declare relationships to other spores:
{
"bonds": [
{
"uri": "cmn://cmn.dev/b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
"relation": "spawned_from"
},
{
"uri": "cmn://lib.dev/b3.8cQnH4xPmZ2vLkJdRt7wNbA9sF3eYgU1hK6pXq5",
"relation": "depends_on",
"id": "signing-lib",
"reason": "Provides Ed25519 signature verification for spore manifests and domain key validation"
},
{
"uri": "cmn://cmn.dev/b3.8cQnH4xPmZ2vLkJdRt7wNbA9sF3eYgU1hK6pXq5",
"relation": "follows",
"id": "agent-first-data",
"reason": "Implements agent-first-data naming conventions for all field names"
},
{
"uri": "cmn://other.dev/b3.8cQnH4xPmZ2vLkJdRt7wNbA9sF3eYgU1hK6pXq5",
"relation": "absorbed_from",
"reason": "Merged authentication module with OAuth and domain verification support"
}
]
}
Bond fields:
| Field | Type | Required | Description |
|---|---|---|---|
uri | String | Yes | CMN URI of the bonded spore (e.g., cmn://{domain}/{hash}) |
relation | String | Yes | Relationship type (see predefined types below) |
id | String | No | Human-readable identifier for this bond. Bond-fetch uses it as the directory name under .cmn/bonds/ (e.g. .cmn/bonds/agent-first-data/) instead of the hash. |
reason | String | No | Why this bond exists and what role it plays in this spore |
with | Object | No | Bond-specific parameters defined by the bonded spore’s convention |
Predefined relation types:
spawned_from— Source this spore was cloned/derived from. The URI includes the hash of the version spawned from.absorbed_from— Source that was merged into this spore. Unlikespawned_from, represents a one-time merge. A spore can have multipleabsorbed_frombonds.depends_on— Runtime or build dependency required by this spore.follows— Convention or standard that this spore adheres to (e.g., data format conventions, API guidelines).implements— Specification or interface that this spore provides a concrete implementation of.inspired_by— Spores that influenced or inspired this work, for attribution and lineage tracking.related_to— General relationship.
Note: Custom relation types are allowed. The predefined types provide semantic meaning for tooling, but any string value is valid. Naming guidance (non-enforced): Use namespaced relation names for custom values to avoid collisions (e.g.,
example.com/deploys_toororg.example.deploys_to). Predefined names above are reserved by the protocol.
The reason field:
The optional reason field explains why this bond exists from the bonding spore’s perspective. It is most useful for:
depends_on- Explains what role the dependency plays (e.g., “Provides cryptographic signing”)follows- Describes which parts of the convention are implemented (e.g., “Implements payment protocol with Cashu support”)implements- Clarifies what portion of the specification is provided (e.g., “Provides MCP server implementation”)absorbed_from- Explains what was merged (e.g., “Merged authentication module”)
Note: spawned_from typically does NOT need reason because the mutations field already documents what was modified in this spawn.
Visitors can read the reason field to understand the purpose and context of each bond without needing to fetch the bonded spore’s metadata.
The with field:
The optional with field carries bond-specific parameters whose schema is defined by the bonded spore’s convention. It allows a spore to declare details about how it uses the bonded spore:
{
"uri": "cmn://cmn.dev/b3.xxx",
"relation": "follows",
"id": "strain-payment-method-evm",
"reason": "Accepts EVM payments",
"with": {
"chains": [8453, 42161],
"tokens": ["native", "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"]
}
}
The with value is an opaque object to the CMN protocol — its structure is defined entirely by the bonded spore. For example, strain-payment-method-evm defines that with may contain chains and tokens; strain-payment-method-cashu defines that with may contain mints.
2.5 Dependency Model
Taste gate: All operations that place code into a visitor’s working directory — spawning, growing, absorbing, and bonding — require the target spore to have been tasted. See 04-taste for verdict definitions, processing rules, and the taste capsule format.
Replicate convention: When releasing a spore, publishers SHOULD replicate all bonds that point to other domains (see §6.1 Replicate). A replicate hosts the same spore (identical hash, identical core and core_signature) under the publisher’s own domain, re-signed with the publisher’s capsule key. The bond URI then points to the publisher’s replicate:
{
"bonds": [
{
"uri": "cmn://mydomain.com/b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
"relation": "depends_on",
"reason": "Parsing library"
},
{
"uri": "cmn://mydomain.com/b3.8cQnH4xPmZ2vLkJdRt7wNbA9sF3eYgU1hK6pXq5",
"relation": "implements",
"reason": "Agent-first-data naming conventions"
}
]
}
The hash is identical to the original — core.domain still identifies the original author. Visitors can reach all bonded spores from a single domain, and verify authorship via core_signature against the original domain’s key. This ensures:
- Visitors only need to reach the publisher’s domain for all bonds
- The publisher’s spores remain functional if upstream domains go offline
- Authorship is preserved in
core.domain— replicates cannot alter it
Working directory (.cmn/): A visitor’s spawned project uses a .cmn/ directory for CMN operational data. This directory is project-local, gitignored, and fully regenerable from spore.core.json:
my-project/
├── src/
├── spore.core.json
└── .cmn/ ← gitignored, regenerable
├── bonds/ ← bonded spores (from bond)
│ ├── bonds.json ← index of all bonds
│ └── {id-or-hash}/ ← id when present, hash otherwise
│ ├── spore.json ← spore manifest
│ └── content/
└── absorb/ ← absorb staging (temporary)
├── ABSORB.md
└── {hash}/
└── content/
The bond operation reads spore.core.json bonds and fetches tasted-safe bonded spores to .cmn/bonds/. It excludes spawned_from (handled by grow) and absorbed_from (historical — the merge is already done). Each bond is stored under its id when present (e.g. .cmn/bonds/agent-first-data/), otherwise under its hash. A bonds.json index maps dir names to hashes, URIs, relations, and names for quick lookup by build systems and tools.
3. Schema Validation
3.1 Schema URL
| Type | Schema URL |
|---|---|
| Spore | https://cmn.dev/schemas/v1/spore.json |
3.2 Embedded Schemas
Validators SHOULD embed schemas for offline validation. No network fetch should be required.
4. Signing and Hashing
4.1 Two-Layer Signature
The spore uses a two-layer signature scheme:
┌─────────────────────────────────────────────────────┐
│ capsule │
│ ┌───────────────────────────────────────────────┐ │
│ │ core (immutable) │ │
│ │ name, domain, key, synopsis, intent, │ │
│ │ mutations, license, bonds, tree, │ │
│ │ updated_at_epoch_ms, id?, version? │ │
│ └───────────────────────────────────────────────┘ │
│ core_signature ← signs core │
│ uri ← contains hash (computed from code+core+sig) │
│ dist (mutable by replicators) │
└─────────────────────────────────────────────────────┘
capsule_signature ← signs entire capsule4.2 Core Signature
The capsule.core_signature signs the immutable metadata:
- Serialize
coreusing JCS (RFC 8785) - Sign with domain’s Ed25519 private key →
ed25519.<base58>
Purpose:
- Protects immutable metadata (name, synopsis, license, etc.)
- Can be verified independently without dist information
- Replicators cannot change core without breaking this signature
4.3 URI Hash Calculation
The hash in capsule.uri is calculated from code (Merkle Tree) + core + core_signature:
- Compute code Merkle Tree hash (see §4.6)
- Construct hash input:
{"code": "<code_hash>", "core": <core>, "core_signature": "<signature>"} - Serialize hash input using JCS
- Hash with BLAKE3 →
b3.<base58> - Construct URI:
cmn://{domain}/b3.<base58>
Key Properties:
capsule.distdoes NOT participate in the hash (replicators can change distribution URLs)capsule_signaturedoes NOT participate in the hash (can be re-signed by replicators)- Changing
coremetadata changes the hash - Code hash is based on Merkle Tree
4.4 Capsule Signature
The capsule_signature signs the entire capsule object (including uri, core, core_signature, dist):
- Serialize
capsuleusing JCS - Sign with domain’s (or replicate host’s) Ed25519 private key →
ed25519.<base58>
Purpose:
- Validates the complete capsule including dist and uri
- Ensures dist information is authorized
- When a replicate host changes dist, they re-sign with their key (but cannot change core)
4.5 Canonical JSON (JCS)
All signatures and hashes use JCS (see 01-substrate §1.3).
4.6 Code Hash (Merkle Tree)
The code hash uses a Git-like Merkle Tree approach for content-addressing.
4.6.1 Hashing Configuration
The capsule.core.tree field controls the tree hash algorithm and which files are included:
algorithm (String):
- Declares the tree hash algorithm used (e.g.,
blob_tree_blake3_nfc) - Components:
blob_tree(object format) +blake3(hash function) +nfc(Unicode normalization) - Visitors that don’t recognize the algorithm cannot verify the content hash
- Registered algorithm names are listed in 07-algorithm-registry
exclude_names (Name List):
- Exact basename match list for directory/file names to skip
- No implicit exclusions — all exclusions must be explicit in this list
follow_rules (Engine List):
- List of standard ignore file formats to honor (e.g.,
".gitignore") - Each item is resolved at the hashing root (for example,
<root>/.gitignore) - Existing files are parsed with gitignore-compatible semantics
- Missing files are ignored (no error)
- Files matching these rules are skipped during hashing
4.6.2 Merkle Tree Construction (Git-compatible)
CMN uses Git-like Merkle Tree format with two differences:
- BLAKE3 (32 bytes) instead of SHA-1 (20 bytes)
- NFC normalization for filenames (cross-platform consistency)
Blobs (Files):
Concatenate header and file content, then BLAKE3 hash the result:
blob <content_length>\0<file_content>
Example: A file containing hello (5 bytes) → blob 5\0hello → BLAKE3 → 32-byte hash.
Trees (Directories):
Build sorted entries, prepend header, then BLAKE3 hash:
tree <entries_length>\0<entry_1><entry_2>...
Each entry:
<mode> <name>\0<32-byte-binary-hash>
Processing rules:
- Traverse recursively from the selected root directory.
- Include regular files and directories.
- Skip symlinks and special files (device/socket/FIFO).
- Normalize each filename segment to NFC before tree entry encoding.
- If two sibling entries normalize to the same NFC name, hashing MUST fail (
filename_nfc_conflict). - Sort sibling entries by normalized-name UTF-8 byte order (ascending, locale-independent).
Entry Format:
- Git format:
<mode> <name>\0<32-byte-binary-hash> - Mode values:
100644: Regular file100755: Executable file40000: Directory
- On non-Unix platforms without executable bit semantics, regular files use
100644. - Hash is binary (32 bytes for BLAKE3)
4.6.3 Unicode NFC Normalization
See also: §4.6.4 below for a complete step-by-step worked example.
To prevent hash mismatches between operating systems (macOS NFD vs. Linux NFC):
- Rule: All filenames MUST be converted to Unicode Normalization Form C (NFC) before hashing
- Example:
á.txt(combining character) →á.txt(precomposed character)
4.6.4 Worked Example
A directory with two files:
my-project/
├── README.md (13 bytes: "Hello, CMN!\n")
└── src/
└── main.rs (14 bytes: "fn main() {}\n")
Step 1 — Hash blobs (files):
Each file is prefixed with blob <length>\0:
README.md:
Input bytes: "blob 13\0Hello, CMN!\n" (5 + 2 + 1 + 13 = 21 bytes)
BLAKE3 → readme_hash (32 bytes binary)
src/main.rs:
Input bytes: "blob 14\0fn main() {}\n" (5 + 2 + 1 + 14 = 22 bytes)
BLAKE3 → main_hash (32 bytes binary)
Step 2 — Build the src/ tree:
One entry for main.rs, sorted alphabetically (only one entry here):
Entry: "100644 main.rs\0" + main_hash (32 bytes binary)
Concatenate all entries to get src_entries. Prepend header:
Input bytes: "tree <len(src_entries)>\0" + src_entries
BLAKE3 → src_tree_hash (32 bytes binary)
Step 3 — Build the root tree:
Two entries sorted alphabetically: README.md (file), src (directory):
Entry 1: "100644 README.md\0" + readme_hash (32 bytes binary)
Entry 2: "40000 src\0" + src_tree_hash (32 bytes binary)
Concatenate entries to get root_entries. Prepend header:
Input bytes: "tree <len(root_entries)>\0" + root_entries
BLAKE3 → root_hash (32 bytes binary)
Step 4 — Format root hash:
Convert root_hash to base58 → b3.<~44 base58 chars>
This root hash is the code hash used in §4.3 (URI hash calculation).
Key details:
- File mode
100644for regular files,100755for executables,40000for directories - Hash bytes are binary (32 bytes), not hex — same as Git’s tree entry format
- Filenames are NFC-normalized before sorting (§4.6.3)
- The
tree.exclude_namesandtree.follow_rulesfilters are applied before hashing — excluded files never appear in the tree
5. Content Verification
5.1 Integrity (MUST)
After obtaining spore content through any distribution channel (archive, git, IPFS, or any other dist source), the client MUST compute the Merkle Tree hash (§4.6) and verify it matches the hash in the spore URI. Content with mismatched hashes MUST be rejected.
5.2 Review (SHOULD)
Hash verification only proves “content has not been tampered with” — it does not prove “content is safe.” Clients SHOULD review spore content before use. The taste system provides a framework for recording and sharing review results.
5.3 Key Trust Verification
Spores with capsule.core.key support offline signature verification. The embedded key allows clients to verify core_signature locally, then establish trust in the key through the tiered model described in 01-substrate §1.2.4.
For replicates (where core.domain differs from the URI domain), core.key is the original author’s key — core_signature verifies against it. The capsule_signature still verifies against the hosting domain’s key from cmn.json.
6. Replicating and Spawning
6.1 Replicate (Same Hash)
A replicate hosts the same spore with different dist URLs:
{
"$schema": "https://cmn.dev/schemas/v1/spore.json",
"capsule": {
"uri": "cmn://replicate.dev/b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
"core": {
"name": "CMN Protocol Specification",
"domain": "cmn.dev"
},
"core_signature": "ed25519.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa23yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
"dist": [
{ "type": "archive", "filename": "cmn-spec.tar.zst" }
]
},
"capsule_signature": "ed25519...."
}
Verification:
- If
core.keyis present, verifycore_signatureagainstcore.keylocally; otherwise fetchcmn.devpublic key fromcmn.json - Establish key trust via domain confirmation or Synapse witness (see 01-substrate §1.2.4)
- URI hash matches (
b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2) domain ≠ URI domain→ This is a replicate hosted byreplicate.devcapsule_signatureverifies againstreplicate.devpublic key
6.2 Spawn (Different Hash)
A spawn creates a new spore derived from an existing one (new domain, modified metadata):
{
"$schema": "https://cmn.dev/schemas/v1/spore.json",
"capsule": {
"uri": "cmn://fork.dev/b3.8cQnH4xPmZ2vLkJdRt7wNbA9sF3eYgU1hK6pXq5",
"core": {
"name": "CMN Spec Spawn",
"domain": "fork.dev",
"bonds": [
{
"uri": "cmn://cmn.dev/b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
"relation": "spawned_from"
}
]
},
"core_signature": "ed25519....",
"dist": [...]
},
"capsule_signature": "ed25519...."
}
Properties:
- Different hash (because core metadata changed)
- New domain and signatures
- Bonds to the original spore
7. spore.core.json
Each spore source directory includes a spore.core.json file containing the capsule.core fields (§2.2). It does not contain dist or signatures — those are added during release.
Schema: https://cmn.dev/schemas/v1/spore-core.json
7.1 Field Reference
Required fields:
| Field | Type | Description |
|---|---|---|
name | String | Human-readable display name. |
domain | String | Publisher domain (e.g., cmn.dev). |
synopsis | String | One-line summary — a visitor reads this alone and understands what the spore does. |
intent | Array | Multi-paragraph description of this spore’s functionality and purpose — what it does, how it works, why it exists. Each array item is a paragraph. Permanent — not cleared on release. Required for release. |
license | String | SPDX License Identifier. |
tree | Object | Tree hash configuration. Required for deterministic content hashing. |
Optional fields:
| Field | Type | Description |
|---|---|---|
id | String | URL-safe path identifier (e.g., cmn-spec). Used for directory names and mycelium deduplication. |
version | String | Human-readable version (e.g., 1.0.0). Informational only; visitors address by hash. |
mutations | Array | What changed relative to the spawned_from parent — describes the mutations applied to derive this spore from its ancestor. |
bonds | Array | List of {uri, relation, id?, reason?} objects (see §2.4). |
tree sub-fields:
| Field | Type | Description |
|---|---|---|
algorithm | String | Tree hash algorithm (e.g., blob_tree_blake3_nfc). |
exclude_names | Array | Directories or files to skip during hashing (e.g., [".git"]). |
follow_rules | Array | Standard ignore file formats to honor (e.g., [".gitignore"]). |
7.2 Complete Example
{
"$schema": "https://cmn.dev/schemas/v1/spore-core.json",
"id": "cmn-spec",
"version": "1.2.0",
"name": "CMN Protocol Specification",
"domain": "cmn.dev",
"synopsis": "Code Mycelial Network - A sovereign-first protocol for code distribution",
"intent": ["add intent and changes array fields to spore core"],
"mutations": [
"§2.2 Core Fields: intent type String → Array",
"§2.2 Core Fields: add mutations field (Array)",
"§1, §4.1, §6 example JSON updated"
],
"license": "CC0-1.0",
"bonds": [
{
"uri": "cmn://cmn.dev/b3.3yMR7vZQ9hL2xKJdFtN8wPcB6sY1mXgU4eH5pTa2",
"relation": "spawned_from"
}
],
"tree": {
"algorithm": "blob_tree_blake3_nfc",
"exclude_names": [".git"],
"follow_rules": [".gitignore"]
}
}7.3 Lifecycle
spore.core.json (local draft, committed to git)
│
│ hypha hatch ← create or update
│
▼
hypha release
│
├── Read spore.core.json
├── Compute Merkle Tree root hash (§4.6)
├── Sign core → core_signature
├── Compute URI hash (§4.3)
├── Add dist endpoints
├── Sign capsule → capsule_signature
│
▼
spore.json (signed, published, immutable)
The spore.core.json file is the only file a developer edits directly. The release command reads it, adds computed fields (dist, signatures, URI), and produces the final spore.json.