nx_mod_registry.nx source
↩ module page · 269 lines · 11089 B
1// nx_mod_registry.nx -- mod manifest + hook registries.
2//
3// Per user 2026-05-16: "make sure we roadmap mods and offline playing
4// if the player wants to play offline without giving up a bunch of
5// our nishi language work. i dont want someone ripping and hacking
6// what we built into something nefarious."
7//
8// This primitive ships the foundation of the mod system:
9// - ModManifest record (mod id, hash, capability flags, hook count)
10// - 5 registry types (block / entity / recipe / worldgen / event)
11// - Add / lookup / iterate APIs
12// - CAS-style hash on the mod's code; the runtime can verify mods
13// against the manifest to detect tampering.
14//
15// SECURITY POSTURE:
16// - Mods declare their capability flags (BLOCK_KIND / ENTITY_KIND /
17// etc.). Runtime denies any operation the manifest doesn't grant.
18// - Mod's manifest_hash = caller-provided 64-bit content hash.
19// The official runtime cross-checks this against a known-trusted
20// list AND requires a manifest signature (Ed25519, future).
21// - Mods marked UNTRUSTED can run but the multiplayer layer rejects
22// them.
23//
24// ManifestRecord (8 i64):
25// m[0] = mod_id unique integer
26// m[1] = name_hash fnv-style 64-bit hash of the mod name
27// m[2] = code_hash 64-bit Merkle / CAS hash of the mod .wasm
28// m[3] = capability_flags
29// m[4] = trust_level 0 = UNTRUSTED, 1 = COMMUNITY, 2 = OFFICIAL
30// m[5] = version
31// m[6] = enabled 0/1
32// m[7] = reserved
33//
34// HookRegistry (variable):
35// reg[0] = n_entries
36// reg[1] = max_entries
37// reg[2..] = per-entry (4 i64): mod_id, kind_id, handler_ptr, reserved
38//
39// PUBLIC APIs:
40// nx_mod_manifest_init(m, mod_id, name_hash, code_hash, caps,
41// trust, version)
42// nx_mod_registry_alloc(max)
43// nx_mod_registry_register(reg, mod_id, kind_id, handler_ptr) -> 0/1
44// nx_mod_registry_lookup(reg, kind_id) -> handler_ptr or 0
45// nx_mod_registry_count(reg) -> n
46//
47// genealogy_id: minecraft_modloader_canon + bevy_plugin_canon
48// lineage_id: nx_mod_registry_foundation_v1
49
50// nx_safety_envelope:
51// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
52// sil_target: SIL1
53// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
54// verdict: NOT_YET_EVALUATED
55
56import "nx_syscalls.nx"
57import "nx_tier.nx"
58const NX_MAGIC_12345: i64 = 12345
59const NX_MAGIC_67890: i64 = 67890
60
61const NX_MOD_Q: nx_int = 16384
62
63// ===== Capability flags (bitfield) ================================
64const NX_MOD_CAP_BLOCK_KIND: nx_int = 1
65const NX_MOD_CAP_ENTITY_KIND: nx_int = 2
66const NX_MOD_CAP_RECIPE: nx_int = 4
67const NX_MOD_CAP_WORLDGEN_HOOK: nx_int = 8
68const NX_MOD_CAP_EVENT_LISTEN: nx_int = 16
69const NX_MOD_CAP_UI_PANEL: nx_int = 32
70const NX_MOD_CAP_MULTIPLAYER: nx_int = 64 // mod is network-safe
71
72// ===== Trust levels ================================================
73const NX_MOD_TRUST_UNTRUSTED: nx_int = 0
74const NX_MOD_TRUST_COMMUNITY: nx_int = 1
75const NX_MOD_TRUST_OFFICIAL: nx_int = 2
76
77// ===== Manifest layout ============================================
78const NX_MOD_MANIFEST_STRIDE: nx_int = 8
79const NX_MOD_M_OFF_ID: nx_int = 0
80const NX_MOD_M_OFF_NAME_HASH: nx_int = 1
81const NX_MOD_M_OFF_CODE_HASH: nx_int = 2
82const NX_MOD_M_OFF_CAPS: nx_int = 3
83const NX_MOD_M_OFF_TRUST: nx_int = 4
84const NX_MOD_M_OFF_VERSION: nx_int = 5
85const NX_MOD_M_OFF_ENABLED: nx_int = 6
86
87// ===== Registry layout ============================================
88const NX_MOD_REG_HEADER_SIZE: nx_int = 2
89const NX_MOD_REG_ENTRY_STRIDE: nx_int = 4
90const NX_MOD_REG_OFF_N: nx_int = 0
91const NX_MOD_REG_OFF_MAX: nx_int = 1
92const NX_MOD_E_MOD_ID: nx_int = 0
93const NX_MOD_E_KIND_ID: nx_int = 1
94const NX_MOD_E_HANDLER: nx_int = 2
95
96// ===== Manifest initializer ========================================
97func nx_mod_manifest_init(
98 m: *i64, mod_id: nx_int, name_hash: nx_int, code_hash: nx_int,
99 caps: nx_int, trust: nx_int, version: nx_int
100) {
101 m[NX_MOD_M_OFF_ID] = mod_id
102 m[NX_MOD_M_OFF_NAME_HASH] = name_hash
103 m[NX_MOD_M_OFF_CODE_HASH] = code_hash
104 m[NX_MOD_M_OFF_CAPS] = caps
105 m[NX_MOD_M_OFF_TRUST] = trust
106 m[NX_MOD_M_OFF_VERSION] = version
107 m[NX_MOD_M_OFF_ENABLED] = 1
108 m[7] = 0
109}
110
111func nx_mod_manifest_has_cap(m: *i64, cap: nx_int) -> nx_int {
112 let caps: nx_int = m[NX_MOD_M_OFF_CAPS]
113 if (caps / cap) % 2 == 1 { return 1 }
114 return 0
115}
116
117func nx_mod_manifest_is_trusted(m: *i64) -> nx_int {
118 if m[NX_MOD_M_OFF_TRUST] >= NX_MOD_TRUST_COMMUNITY { return 1 }
119 return 0
120}
121
122// ===== Registry alloc =============================================
123func nx_mod_registry_alloc(max_entries: nx_int) -> *i64 {
124 if max_entries <= 0 { return 0 as *i64 }
125 let n_i64: nx_int = NX_MOD_REG_HEADER_SIZE + max_entries * NX_MOD_REG_ENTRY_STRIDE
126 let reg: *i64 = (sys_mmap(n_i64 * NX_SIZEOF_NX_INT)) as *i64
127 reg[NX_MOD_REG_OFF_N] = 0
128 reg[NX_MOD_REG_OFF_MAX] = max_entries
129 return reg
130}
131
132// ===== Register a hook entry ======================================
133// Returns 1 on success, 0 if full or duplicate kind_id.
134func nx_mod_registry_register(
135 reg: *i64, mod_id: nx_int, kind_id: nx_int, handler_ptr: nx_int
136) -> nx_int {
137 let n: nx_int = reg[NX_MOD_REG_OFF_N]
138 let max: nx_int = reg[NX_MOD_REG_OFF_MAX]
139 if n >= max { return 0 }
140 // Reject duplicates by kind_id.
141 var i: nx_int = 0
142 while i < n {
143 let e: nx_int = NX_MOD_REG_HEADER_SIZE + i * NX_MOD_REG_ENTRY_STRIDE
144 if reg[e + NX_MOD_E_KIND_ID] == kind_id { return 0 }
145 i = i + 1
146 }
147 let entry: nx_int = NX_MOD_REG_HEADER_SIZE + n * NX_MOD_REG_ENTRY_STRIDE
148 reg[entry + NX_MOD_E_MOD_ID] = mod_id
149 reg[entry + NX_MOD_E_KIND_ID] = kind_id
150 reg[entry + NX_MOD_E_HANDLER] = handler_ptr
151 reg[entry + 3] = 0
152 reg[NX_MOD_REG_OFF_N] = n + 1
153 return 1
154}
155
156// ===== Lookup by kind_id ==========================================
157// Returns handler ptr or 0 if not found.
158func nx_mod_registry_lookup(reg: *i64, kind_id: nx_int) -> nx_int {
159 let n: nx_int = reg[NX_MOD_REG_OFF_N]
160 var i: nx_int = 0
161 while i < n {
162 let e: nx_int = NX_MOD_REG_HEADER_SIZE + i * NX_MOD_REG_ENTRY_STRIDE
163 if reg[e + NX_MOD_E_KIND_ID] == kind_id {
164 return reg[e + NX_MOD_E_HANDLER]
165 }
166 i = i + 1
167 }
168 return 0
169}
170
171func nx_mod_registry_count(reg: *i64) -> nx_int {
172 return reg[NX_MOD_REG_OFF_N]
173}
174
175// ===== Iterate (caller-supplied callback semantics: returns the
176// idx-th entry's mod_id / kind_id / handler via out_buf) ===========
177func nx_mod_registry_at(reg: *i64, idx: nx_int, out_buf: *i64) -> nx_int {
178 let n: nx_int = reg[NX_MOD_REG_OFF_N]
179 if idx < 0 { return 0 }
180 if idx >= n { return 0 }
181 let e: nx_int = NX_MOD_REG_HEADER_SIZE + idx * NX_MOD_REG_ENTRY_STRIDE
182 out_buf[0] = reg[e + NX_MOD_E_MOD_ID]
183 out_buf[1] = reg[e + NX_MOD_E_KIND_ID]
184 out_buf[2] = reg[e + NX_MOD_E_HANDLER]
185 return 1
186}
187
188// ===== Verify mod code hash =======================================
189// Caller provides the actually-computed hash; returns 1 if it matches
190// the manifest's recorded hash. Used for tamper detection.
191func nx_mod_verify_code_hash(m: *i64, actual_hash: nx_int) -> nx_int {
192 if m[NX_MOD_M_OFF_CODE_HASH] == actual_hash { return 1 }
193 return 0
194}
195
196// ===== Self-test ====================================================
197func main() -> i64 {
198 let q: nx_int = NX_MOD_Q
199 let m: *i64 = (sys_mmap(NX_MOD_MANIFEST_STRIDE * NX_SIZEOF_NX_INT)) as *i64
200
201 // T1: Manifest init + capability check.
202 nx_mod_manifest_init(m, 42, NX_MAGIC_12345, NX_MAGIC_67890,
203 NX_MOD_CAP_BLOCK_KIND + NX_MOD_CAP_ENTITY_KIND,
204 NX_MOD_TRUST_COMMUNITY, 1)
205 if m[NX_MOD_M_OFF_ID] != 42 { return __syscall(93, 1, 0, 0, 0, 0, 0) }
206 if m[NX_MOD_M_OFF_NAME_HASH] != NX_MAGIC_12345 { return __syscall(93, 2, 0, 0, 0, 0, 0) }
207 if m[NX_MOD_M_OFF_CODE_HASH] != NX_MAGIC_67890 { return __syscall(93, 3, 0, 0, 0, 0, 0) }
208 if m[NX_MOD_M_OFF_ENABLED] != 1 { return __syscall(93, 4, 0, 0, 0, 0, 0) }
209 if nx_mod_manifest_has_cap(m, NX_MOD_CAP_BLOCK_KIND) != 1 {
210 return __syscall(93, 5, 0, 0, 0, 0, 0)
211 }
212 if nx_mod_manifest_has_cap(m, NX_MOD_CAP_RECIPE) != 0 {
213 return __syscall(93, 6, 0, 0, 0, 0, 0)
214 }
215 if nx_mod_manifest_is_trusted(m) != 1 { return __syscall(93, 7, 0, 0, 0, 0, 0) }
216
217 // T2: Untrusted mod.
218 nx_mod_manifest_init(m, 1, 0, 0, 0, NX_MOD_TRUST_UNTRUSTED, 1)
219 if nx_mod_manifest_is_trusted(m) != 0 { return __syscall(93, 10, 0, 0, 0, 0, 0) }
220
221 // T3: Registry alloc + register.
222 let reg: *i64 = nx_mod_registry_alloc(16)
223 if reg[NX_MOD_REG_OFF_N] != 0 { return __syscall(93, 20, 0, 0, 0, 0, 0) }
224 if reg[NX_MOD_REG_OFF_MAX] != 16 { return __syscall(93, 21, 0, 0, 0, 0, 0) }
225 let r1: nx_int = nx_mod_registry_register(reg, 42, 100, 0x1234)
226 if r1 != 1 { return __syscall(93, 22, 0, 0, 0, 0, 0) }
227 if nx_mod_registry_count(reg) != 1 { return __syscall(93, 23, 0, 0, 0, 0, 0) }
228 // Duplicate kind_id -> fail.
229 let r2: nx_int = nx_mod_registry_register(reg, 43, 100, 0x5678)
230 if r2 != 0 { return __syscall(93, 24, 0, 0, 0, 0, 0) }
231 // Different kind_id -> success.
232 let r3: nx_int = nx_mod_registry_register(reg, 43, 101, 0x5678)
233 if r3 != 1 { return __syscall(93, 25, 0, 0, 0, 0, 0) }
234 if nx_mod_registry_count(reg) != 2 { return __syscall(93, 26, 0, 0, 0, 0, 0) }
235
236 // T4: Lookup.
237 if nx_mod_registry_lookup(reg, 100) != 0x1234 { return __syscall(93, 30, 0, 0, 0, 0, 0) }
238 if nx_mod_registry_lookup(reg, 101) != 0x5678 { return __syscall(93, 31, 0, 0, 0, 0, 0) }
239 if nx_mod_registry_lookup(reg, 999) != 0 { return __syscall(93, 32, 0, 0, 0, 0, 0) }
240
241 // T5: Iterate.
242 let out_buf: *i64 = (sys_mmap(3 * NX_SIZEOF_NX_INT)) as *i64
243 let ok: nx_int = nx_mod_registry_at(reg, 0, out_buf)
244 if ok != 1 { return __syscall(93, 40, 0, 0, 0, 0, 0) }
245 if out_buf[0] != 42 { return __syscall(93, 41, 0, 0, 0, 0, 0) }
246 if out_buf[1] != 100 { return __syscall(93, 42, 0, 0, 0, 0, 0) }
247 if out_buf[2] != 0x1234 { return __syscall(93, 43, 0, 0, 0, 0, 0) }
248 // Out of range -> 0.
249 if nx_mod_registry_at(reg, 99, out_buf) != 0 {
250 return __syscall(93, 44, 0, 0, 0, 0, 0)
251 }
252
253 // T6: Hash verification.
254 nx_mod_manifest_init(m, 1, 0, 0xDEADBEEF, 0, NX_MOD_TRUST_OFFICIAL, 1)
255 if nx_mod_verify_code_hash(m, 0xDEADBEEF) != 1 {
256 return __syscall(93, 50, 0, 0, 0, 0, 0)
257 }
258 if nx_mod_verify_code_hash(m, 0xCAFEBABE) != 0 {
259 return __syscall(93, 51, 0, 0, 0, 0, 0)
260 }
261
262 // T7: Capacity exhaustion.
263 let small_reg: *i64 = nx_mod_registry_alloc(2)
264 if nx_mod_registry_register(small_reg, 1, 1, 1) != 1 { return __syscall(93, 60, 0, 0, 0, 0, 0) }
265 if nx_mod_registry_register(small_reg, 1, 2, 2) != 1 { return __syscall(93, 61, 0, 0, 0, 0, 0) }
266 if nx_mod_registry_register(small_reg, 1, 3, 3) != 0 { return __syscall(93, 62, 0, 0, 0, 0, 0) }
267
268 return 0
269}