Skip to content

.kn86 Cartridge Format

The on-disk container format for cartridges. A .kn86 file is the unit the runtime mounts (per cartridge-lifecycle.md) and the unit the Fe compiler emits.

Authoritative decision: adr/ADR-0006 is the canonical decision record. Amendments through 2026-04-24 (post-ADR-0019) are reflected here. This doc is the implementer-facing extract of the byte-level format; the rationale and amendment history live in the ADR.

Related:


┌─ .kn86 Cartridge Image (little-endian, all multi-byte values) ─┐
│ │
├─ HEADER (80 bytes, fixed) │
│ Magic: "KN86" (4 bytes) │
│ Version: 2 (uint16) + _reserved1 (uint16) │
│ Cart ID: unique identifier (uint32) │
│ Capability type: e.g., "NETWORK_INTRUSION" (32 bytes) │
│ Declared req_api_version: e.g., 0x0201 (uint16) │
│ Declared req_vm_version: e.g., 0x0100 (uint16) │
│ Bytecode offset + size: (2 × uint32) │
│ Static data offset + size: (2 × uint32) │
│ Debug section offset + size: (2 × uint32) — 0s if none │
│ Checksum: CRC-32 (uint32) │
│ Reserved (4 bytes): padding for future use │
│ │
├─ BYTECODE SECTION (variable, aligned to 4-byte boundary) │
│ Raw Fe-compiled bytecode blob │
│ │
├─ STATIC DATA SECTION (variable, aligned to 4-byte boundary) │
│ Tagged sub-sections: SPRITES, PSG_PATTERNS, STRINGS, │
│ MISSIONS, CART_CAPABILITIES, END │
│ │
├─ DEBUG SECTION (optional, aligned to 4-byte boundary) │
│ Source, line tables, symbol names │
│ │
└─────────────────────────────────────────────────────────────────┘

struct CartridgeHeaderV2 {
uint8_t magic[4]; /* "KN86" (0x4B 0x4E 0x38 0x36) */
uint16_t version; /* 2 (for v2.0) */
uint16_t _reserved1; /* Alignment padding */
uint32_t cart_id; /* CRC of (program_name + module_class) or author-assigned */
char capability_type[32]; /* e.g., "NETWORK_INTRUSION\0" */
uint16_t req_api_version; /* Min NoshAPI version (e.g., 0x0201 = v2.1) */
uint16_t req_vm_version; /* Min Fe VM version (e.g., 0x0100 = v1.0) */
uint32_t bytecode_offset; /* Byte offset to bytecode section */
uint32_t bytecode_size; /* Size in bytes */
uint32_t static_data_offset; /* Byte offset to static data */
uint32_t static_data_size; /* Size in bytes */
uint32_t debug_offset; /* Byte offset to debug section (0 if none) */
uint32_t debug_size; /* Size in bytes (0 if no debug) */
uint32_t checksum; /* CRC-32 of header + all sections (0 if not checked) */
uint32_t _reserved2; /* Future use */
/* Total: 80 bytes */
};
  • magic — Always "KN86" (0x4B 0x4E 0x38 0x36). Identifies file type.
  • version — Format version. 2 for this spec.
  • cart_id — Unique cartridge identifier. Used for save-game isolation and mod tracking.
  • capability_type — Domain string, null-terminated, max 31 chars (e.g., "NETWORK_INTRUSION"). Primarily for UI.
  • req_api_version — Semver-style uint16 (high byte = major, low byte = minor). Runtime rejects the cart if nosh_api_version < req_api_version.
  • req_vm_version — Min Fe VM version required.
  • bytecode_offset / bytecode_size — Define the bytecode blob.
  • static_data_offset / static_data_size — Define static data location. Bounded by the SD card capacity (gigabyte-scale per ADR-0019), not RAM. The runtime reads on demand from the cartridge’s mounted filesystem.
  • debug_offset / debug_size — Optional debug section. debug_size == 0 means absent.
  • checksum — CRC-32 of [header..debug]. 0 if not computed. Verification is optional (~100 µs cost).

Opaque Fe-compiled bytecode blob.

The Fe compiler (desktop tool) takes .lsp source and outputs:

  1. Instruction stream (Fe opcodes, ~30–50 distinct).
  2. Constant table (literals: numbers, symbols, strings).
  3. Symbol table (optional, for debugging).

Pipeline:

source.lsp → Fe compiler → bytecode + constants → packager → .kn86

Loading:

.kn86 → read header → validate version → load bytecode into arena → Fe evaluator

MVP note (per ADR-0006): v0.1 may ship .lsp source directly in the bytecode section (Fe reads source at load). A custom bytecode format is a Phase 2 optimization (~30% smaller carts). Both are compatible with this format.


Organized as tagged sub-sections. Each carries a type tag, size, and payload:

[Type:SPRITES] [Size:4096] [Bitmap Data...]
[Type:PSG_PATTERNS] [Size:512] [Pattern Data...]
[Type:STRINGS] [Size:1024] [String Table...]
[Type:MISSIONS] [Size:2048] [Mission Template Data...]
[Type:CART_CAPABILITIES] [Size:26] [count + capability keywords]
[Type:END] [Size:0]
struct StaticDataSubsection {
uint32_t type; /* Tag — see table below */
uint32_t size; /* Bytes of payload (excluding this header) */
/* Payload (size bytes) follows immediately */
};
TagTypePayload
0ENDEmpty (terminator).
1SPRITES1-bpp row-major bitmap data. Sprite metadata (width, height, offset) lives in the section header.
2PSG_PATTERNSPSG register dump sequences: [pattern_id (2B), register_sequence...].
3STRINGSString table: [string_id (2B), length (2B), null-terminated string...]. Cart code references strings by ID.
4MISSIONSBinary serialization of mission template structures (objectives, threat ranges, phase chains, payout formulas). Parsed at mission-board generation time.
5CART_CAPABILITIESLength-prefixed list of ASCII capability keywords the cart requests. Omit when no capabilities are requested (the v0.1 default for every launch cart except Null). See adr/ADR-0015 §3a.

Forward compatibility: the runtime skips unknown type tags, so future sub-section types can be introduced without breaking older cartridges’ load path. Cartridges include only the sub-sections they need (no bloat from unused asset types).

[Type:SPRITES] [Size:2048] [network_diagram, threat_meter, grid_bg, ...]
[Type:PSG_PATTERNS] [Size:256] [alarm_pattern, hack_complete_sting, ...]
[Type:STRINGS] [Size:512] ["CONTRACT_EXTRACT", "NETWORK_INTRUSION", ...]
[Type:MISSIONS] [Size:1024] [mission_meridian_extract, mission_cascade_vault, ...]
[Type:END] [Size:0]
[Type:SPRITES] [Size:1024] [cipher_debug_glyphs, ...]
[Type:STRINGS] [Size:512] ["CIPHER_ANALYSIS", ...]
[Type:MISSIONS] [Size:512] [mission_cipher_analysis, ...]
[Type:CART_CAPABILITIES] [Size:26] [count:1, _reserved:0, "cipher-main-grid-escape"]
[Type:END] [Size:0]

Present only when debug_size > 0. Omitted from production carts.

[Header]
Type: "DEBUG_v1\0" (8 bytes)
Line table size (uint32)
Symbol table size (uint32)
Source size (uint32)
[Line Table]
Bytecode instruction offset → source line number
Format: [instr_offset (uint32), line_number (uint32)]...
[Symbol Table]
Symbols → internal indices
Format: [symbol_hash (uint32), name_length (uint16), name (variable)]...
[Source Code]
Original .lsp source (uncompressed text). May be truncated if oversize.

Typical debug size: 10–20 KB for a 50-line cartridge. Strippable for production.


CRC-32 over [header..debug]. Stored in the header’s checksum field. 0 means “not computed”. Verification is optional and costs ~100 µs on the runtime’s target hardware.


The runtime validates in this order before registering the cart (see cartridge-lifecycle.md for the surrounding state machine):

  1. magic == "KN86" → else reject as not a cartridge.
  2. version in supported range → else reject with version mismatch.
  3. nosh_api_version >= req_api_version → else reject with API mismatch.
  4. vm_version >= req_vm_version → else reject with VM mismatch.
  5. Section offsets + sizes within file bounds → else reject as truncated.
  6. Optional CRC-32 check on checksum field → soft fail (warn, allow).

Any failure surfaces in fiction on the bare-deck terminal (> CARTRIDGE FORMAT NOT RECOGNIZED etc.) and the cart stays in MOUNTED state until removed.