nx_elder_supervisor_main.nx source
↩ module page · 182 lines · 6963 B
1// nx_elder_supervisor_main.nx -- substrate-side Elder AI supervisor.
2//
3// Cardinal 2026-05-21 dual-mandate (top-down + bottom-up): the
4// SUBSTRATE owns the service-lifecycle decision (which services
5// exist, which ports, which healthchecks). The OS adapter
6// (bench/nx_elder_supervise.sh) is now a dumb pipe -- read the
7// substrate-emitted manifest, launch python, poll the URL.
8//
9// V1 contract -- emit one tab-separated line per registered
10// service on stdout (fd 1):
11//
12// SVC<TAB>service_name<TAB>port<TAB>healthcheck_url<NEWLINE>
13//
14// Lines prefixed by anything other than "SVC" are diagnostic
15// (logged by the adapter, not parsed as service entries).
16//
17// V1 mechanics: builds an nx_elder_registry with the services the
18// substrate currently knows how to host (svc-config today;
19// svc-discovery + the data-* + orch-* tier when those are wired),
20// then iterates and writes one manifest line per service. Adapter
21// reads stdin, parses, launches.
22//
23// Why not bash-only? Because then bash owns the source of truth
24// about services + ports + healthchecks, and as Elder AI's 150-
25// service inventory ports over, that bash table becomes the
26// substrate's most important policy artifact -- shadow state.
27// Putting it in nx_elder_registry means the registry IS the
28// policy, with canary protection + sealed-enum validation + the
29// supervisor state machine attached.
30//
31// genealogy_id: erlang_otp_application_start_1996 +
32// systemd_unit_files_2010 +
33// docker_compose_v3_2021 +
34// cardinal_2026-05-21_top_down_bottom_up_dual_mandate
35// lineage_id: substrate_elder_supervisor_main_v1
36
37import "nx_syscalls.nx"
38import "nx_hal.nx"
39import "nx_elder_registry.nx"
40const K_MAGIC_8423: i64 = 8423
41const K_MAGIC_8600: i64 = 8600
42const K_MAGIC_8100: i64 = 8100
43const K_MAGIC_8101: i64 = 8101
44const K_MAGIC_8102: i64 = 8102
45
46// ===== Output helpers (writes to stdout, fd 1) =================================================
47//
48// nx_hal_console_write goes to fd 2 (debug channel). For the
49// MANIFEST we want fd 1 (substrate-to-adapter communication channel).
50func _emit_line(buf: *u8, len: i64) -> i64 {
51 return sys_write(1, buf, len)
52}
53
54func _emit_byte(b: i64) -> i64 {
55 let tmp: *u8 = (sys_mmap(8)) as *u8
56 tmp[0] = b
57 return sys_write(1, tmp, 1)
58}
59
60// Write a small positive integer as ASCII decimal. V1 single-digit
61// minimum + multi-digit via simple division-by-10 loop.
62func _emit_i64(n: i64) -> i64 {
63 if n == 0 {
64 return _emit_byte(48) // ASCII '0'
65 }
66 // Buffer up to 20 digits (i64 max).
67 let buf: *u8 = (sys_mmap(32)) as *u8
68 var v: i64 = n
69 var i: i64 = 0
70 if v < 0 {
71 _emit_byte(45) // ASCII '-'
72 v = 0 - v
73 }
74 while v > 0 {
75 buf[i] = (v - (v / 10) * 10) + 48
76 v = v / 10
77 i = i + 1
78 }
79 // buf currently has digits in reverse; emit in correct order.
80 var j: i64 = i - 1
81 while j >= 0 {
82 _emit_byte(buf[j])
83 j = j - 1
84 }
85 return i
86}
87
88// ===== Manifest line emission =================================================
89//
90// Format: "SVC\tNAME\tPORT\tHEALTHCHECK\n"
91func _emit_manifest_line(name: *u8, name_len: i64, port: i64, hc: *u8, hc_len: i64) -> i64 {
92 let prefix: *u8 = "SVC\t" as *u8
93 _emit_line(prefix, 4)
94 _emit_line(name, name_len)
95 _emit_byte(9) // TAB
96 _emit_i64(port)
97 _emit_byte(9) // TAB
98 _emit_line(hc, hc_len)
99 _emit_byte(10) // NEWLINE
100 return 0
101}
102
103// ===== Registry build =================================================
104//
105// Hardcoded V1 -- substrate knows svc-config today. As services port
106// over, additional nx_elder_register calls land here. Future V2
107// reads the service inventory from a substrate-owned KV store
108// (composes with nx_vacuole / future nx_kv_store).
109func _build_registry(r: *NxElderRegistry) -> i64 {
110 let py: *u8 = "python" as *u8
111 let py_args: *u8 = "-m uvicorn app:app --host 127.0.0.1 --port <PORT> --app-dir <SVC_DIR>" as *u8
112
113 // svc-config (port 8423)
114 let cfg_name: *u8 = "svc-config" as *u8
115 let cfg_hc: *u8 = "http://127.0.0.1:8423/health" as *u8
116 let rc0: i64 = nx_elder_register(r, 100, cfg_name, 10, K_MAGIC_8423, py, 6, py_args, 73, cfg_hc, 28)
117 if rc0 < 0 { return 0 - 1 }
118
119 // svc-discovery (port 8600 -- ONLY hardcoded port in Elder AI;
120 // every other service registers here at startup and gets a
121 // dynamically assigned port per CLAUDE.md)
122 let disc_name: *u8 = "svc-discovery" as *u8
123 let disc_hc: *u8 = "http://127.0.0.1:8600/health" as *u8
124 let rc1: i64 = nx_elder_register(r, 101, disc_name, 13, K_MAGIC_8600, py, 6, py_args, 73, disc_hc, 28)
125 if rc1 < 0 { return 0 - 2 }
126
127 // data-character (port 8100) -- FastAPI + BaseDataService over
128 // characters.db. No discovery auto-registration; lifespan is
129 // just startup log + yield + shutdown log.
130 let char_name: *u8 = "data-character" as *u8
131 let char_hc: *u8 = "http://127.0.0.1:8100/health" as *u8
132 let rc2: i64 = nx_elder_register(r, 200, char_name, 14, K_MAGIC_8100, py, 6, py_args, 73, char_hc, 28)
133 if rc2 < 0 { return 0 - 3 }
134
135 // data-memory (port 8101) -- conversation memories CRUD.
136 let mem_name: *u8 = "data-memory" as *u8
137 let mem_hc: *u8 = "http://127.0.0.1:8101/health" as *u8
138 let rc3: i64 = nx_elder_register(r, 201, mem_name, 11, K_MAGIC_8101, py, 6, py_args, 73, mem_hc, 28)
139 if rc3 < 0 { return 0 - 4 }
140
141 // data-relationship (port 8102) -- affection/trust/loyalty/romance.
142 let rel_name: *u8 = "data-relationship" as *u8
143 let rel_hc: *u8 = "http://127.0.0.1:8102/health" as *u8
144 let rc4: i64 = nx_elder_register(r, 202, rel_name, 17, K_MAGIC_8102, py, 6, py_args, 73, rel_hc, 28)
145 if rc4 < 0 { return 0 - 5 }
146
147 return 0
148}
149
150// ===== Banner =================================================
151func _emit_banner() -> i64 {
152 let banner: *u8 = "# nx_elder_supervisor_main -- substrate-emitted manifest follows\n# Format: SVC\\tNAME\\tPORT\\tHEALTHCHECK\n" as *u8
153 sys_write(2, banner, 110) // debug channel
154 return 0
155}
156
157// ===== Main =================================================
158func main() -> i64 {
159 _emit_banner()
160
161 let r: *NxElderRegistry = nx_elder_registry_new(NX_ELDER_MAX_SERVICES)
162 if nx_elder_registry_is_valid(r) != 1 { return 1 }
163
164 if _build_registry(r) < 0 { return 2 }
165
166 let n: i64 = nx_elder_n_services(r)
167 if n <= 0 { return 3 }
168
169 var i: i64 = 0
170 while i < n {
171 let s: *NxElderService = nx_elder_lookup_by_index(r, i)
172 if nx_elder_service_is_valid(s) != 1 { return 4 }
173 _emit_manifest_line(s.name, s.name_len, s.port, s.healthcheck_url, s.healthcheck_len)
174 i = i + 1
175 }
176
177 // Trailer line so the adapter knows the manifest is complete.
178 let done: *u8 = "DONE\n" as *u8
179 _emit_line(done, 5)
180
181 return 0
182}