nx_friend_video_caller.nx source
↩ module page · 388 lines · 19004 B
1// nx_friend_video_caller.nx -- bits-up NishiLang FRIEND who joins a
2// /video signaling room and reports a substrate-honest verdict.
3//
4// Replaces the python harness at bench/ops/nx_friend_video_caller.py
5// per cardinal feedback-no-third-party-in-build-path-source-of-truth-is-bits-up
6// and the operator's mandate "we are nishi lang from bits up".
7//
8// Single-friend per process: opens a TCP socket to host:port,
9// completes the RFC 6455 client-side WebSocket upgrade against the
10// live nx_signaling daemon, sends a {"type":"hello"} JSON envelope,
11// elects offerer/answerer by who-sees-whose-hello-first, exchanges
12// offer+answer + one ICE candidate. Two FRIENDS launched in
13// parallel against the same room ID complete the dance like two
14// browsers on Zoom.
15//
16// Sealed verdict enum (mirrors python harness keywords so deploy
17// scripts continue parsing):
18// NX_FRIEND_OK pair completed offer/answer/ice
19// NX_FRIEND_CONNECT_FAIL TCP connect failed
20// NX_FRIEND_UPGRADE_FAIL WS upgrade != 101
21// NX_FRIEND_ROLE_UNDECIDED never elected offerer/answerer
22// NX_FRIEND_OFFER_NEVER_ARRIVED elected answerer; no offer relayed
23// NX_FRIEND_ANSWER_NEVER_ARRIVED elected offerer; no answer relayed
24// NX_FRIEND_ICE_NEVER_RELAYED dance ok but no ICE candidate seen
25// NX_FRIEND_FRAME_BAD non-text frame from peer
26// NX_FRIEND_JSON_BAD payload not valid JSON envelope
27// NX_FRIEND_TIMEOUT 5s overall deadline elapsed
28//
29// Composes:
30// nx_syscalls (sys_socket/connect/read/write/exit)
31// nx_websocket_client_upgrade (client-side WS HTTP upgrade)
32// nx_websocket_stream (frame-from-fd / send-text-to-fd)
33// nx_websocket_frame (opcode constants)
34//
35// PROTOCOL ENVELOPES (hand-rolled byte literals per task addendum --
36// NishiLang has no json library; we write the bytes we want directly):
37//
38// hello: {"type":"hello"} (15 bytes)
39// offer: {"type":"offer","sdp":{"type":"offer","sdp":"v=0\r\n"}}
40// answer: {"type":"answer","sdp":{"type":"answer","sdp":"v=0\r\n"}}
41// ice: {"type":"ice","candidate":{"candidate":"candidate:1 1 udp 100 1.2.3.4 5000 typ host"}}
42//
43// Per Cardinals 9 (single-responsibility: ONE friend, ONE room), 12
44// (defensive at boundary: every recv verdict checked + bounded loop),
45// 14 (graceful degradation: distinct verdicts let the deploy script
46// react differently to TIMEOUT vs UPGRADE_FAIL), 22 (composition of
47// shipped primitives).
48//
49// genealogy_id: rfc_6455_client + json_rfc_8259 + nx_signaling
50// lineage_id: nishi_friend_video_caller_q1
51
52// nx_safety_envelope:
53// intended_use: AUTO_APPLIED -- friend-doctrine first NishiLang impl
54// sil_target: SIL1
55// evidence: [bits-up-replaces-python, KAT-canned-envelopes]
56// verdict: NOT_YET_EVALUATED
57
58import "nx_syscalls.nx"
59import "nx_connect.nx" // bounded connect: a raw sys_connect hangs ~127s on a black-holed host
60import "nx_websocket_client_upgrade.nx"
61import "nx_websocket_stream.nx"
62import "nx_websocket_frame.nx"
63
64const NX_FRIEND_OK: i64 = 1
65const NX_FRIEND_CONNECT_FAIL: i64 = 2
66const NX_FRIEND_UPGRADE_FAIL: i64 = 3
67const NX_FRIEND_ROLE_UNDECIDED: i64 = 4
68const NX_FRIEND_OFFER_NEVER_ARRIVED: i64 = 5
69const NX_FRIEND_ANSWER_NEVER_ARRIVED: i64 = 6
70const NX_FRIEND_ICE_NEVER_RELAYED: i64 = 7
71const NX_FRIEND_FRAME_BAD: i64 = 8
72const NX_FRIEND_JSON_BAD: i64 = 9
73const NX_FRIEND_TIMEOUT: i64 = 10
74const NX_FRIEND_VERDICT_N: i64 = 11
75
76const NX_FRIEND_ROLE_NONE: i64 = 0
77const NX_FRIEND_ROLE_OFFERER: i64 = 1
78const NX_FRIEND_ROLE_ANSWERER: i64 = 2
79
80const NX_FRIEND_AF_INET: i64 = 2
81const NX_FRIEND_SOCK_STREAM: i64 = 1
82
83const NX_FRIEND_HDR_BUF_BYTES: i64 = 2048
84const NX_FRIEND_FRAME_BUF_BYTES: i64 = 8192
85const NX_FRIEND_MAX_ITERS: i64 = 16 // hard ceiling on recv loop
86
87func nx_friend_verdict_is_valid(v: i64) -> i64 {
88 if v < NX_FRIEND_OK { return 0 }
89 if v >= NX_FRIEND_VERDICT_N { return 0 }
90 return 1
91}
92
93// JSON envelope bytes. Lengths computed at runtime via _friend_strlen
94// (NUL-terminated literal strings) so the byte counts and the source
95// text cannot diverge. Per substrate-honesty: a hand-counted const
96// silently disagreeing with the actual literal length is exactly the
97// bug class we eliminate by deriving from the source of truth.
98func nx_friend_hello_bytes() -> *u8 {
99 return "{\"type\":\"hello\"}" as *u8
100}
101
102func nx_friend_offer_bytes() -> *u8 {
103 return "{\"type\":\"offer\",\"sdp\":{\"type\":\"offer\",\"sdp\":\"v=0\\r\\n\"}}" as *u8
104}
105
106func nx_friend_answer_bytes() -> *u8 {
107 return "{\"type\":\"answer\",\"sdp\":{\"type\":\"answer\",\"sdp\":\"v=0\\r\\n\"}}" as *u8
108}
109
110func nx_friend_ice_bytes() -> *u8 {
111 return "{\"type\":\"ice\",\"candidate\":{\"candidate\":\"candidate:1 1 udp 100 1.2.3.4 5000 typ host\"}}" as *u8
112}
113
114func _friend_strlen(s: *u8) -> i64 {
115 var n: i64 = 0
116 while s[n] != 0 as u8 { n = n + 1 }
117 return n
118}
119
120func nx_friend_hello_len() -> i64 { return _friend_strlen(nx_friend_hello_bytes()) }
121func nx_friend_offer_len() -> i64 { return _friend_strlen(nx_friend_offer_bytes()) }
122func nx_friend_answer_len() -> i64 { return _friend_strlen(nx_friend_answer_bytes()) }
123func nx_friend_ice_len() -> i64 { return _friend_strlen(nx_friend_ice_bytes()) }
124
125// Build an IPv4 sockaddr_in into 16-byte buf. Reuses the layout
126// from nx_https_url_connect / nx_signaling (AF_INET LE, port BE,
127// addr in network order).
128func _friend_build_sockaddr(out: *u8, a: i64, b: i64, c: i64, d: i64, port: i64) -> i64 {
129 out[0] = 2 as u8; out[1] = 0 as u8
130 out[2] = ((port >> 8) & 0xff) as u8
131 out[3] = (port & 0xff) as u8
132 out[4] = a as u8; out[5] = b as u8; out[6] = c as u8; out[7] = d as u8
133 var i: i64 = 8
134 while i < 16 { out[i] = 0 as u8; i = i + 1 }
135 return 16
136}
137
138// Lightweight JSON-envelope type extractor. Looks for the
139// substring "type":"X" in payload[0..n) and returns:
140// 1 if X == "hello"
141// 2 if X == "offer"
142// 3 if X == "answer"
143// 4 if X == "ice"
144// 0 if nothing matches (treat as JSON_BAD)
145//
146// Tolerates whitespace between : and the value. Not a full parser
147// (intentional: we only care which envelope-kind it is).
148func nx_friend_envelope_type(payload: *u8, n: i64) -> i64 {
149 // Find the substring "type" followed by optional ws + ':' + ws + '"'.
150 var i: i64 = 0
151 while i + 7 <= n {
152 // Match literal "type"
153 if payload[i] == 34 as u8 { // '"'
154 if payload[i+1] == 116 as u8 { // 't'
155 if payload[i+2] == 121 as u8 { // 'y'
156 if payload[i+3] == 112 as u8 { // 'p'
157 if payload[i+4] == 101 as u8 { // 'e'
158 if payload[i+5] == 34 as u8 { // '"'
159 // skip ws + ':' + ws + '"'
160 var j: i64 = i + 6
161 while j < n {
162 if payload[j] == 32 as u8 { j = j + 1 }
163 else { if payload[j] == 9 as u8 { j = j + 1 } else { j = n + 1 } }
164 }
165 // (loop above breaks via j=n+1 to escape; reset)
166 j = i + 6
167 while j < n {
168 let c: u8 = payload[j]
169 if c == 32 as u8 { j = j + 1 }
170 else { if c == 9 as u8 { j = j + 1 } else { j = n + 100 } }
171 }
172 // restore: find ':'
173 var k: i64 = i + 6
174 while k < n {
175 if payload[k] == 58 as u8 { k = n + 200 }
176 k = k + 1
177 }
178 // walk past ':' and ws + '"' robustly
179 var p: i64 = i + 6
180 while p < n {
181 if payload[p] == 58 as u8 {
182 p = p + 1
183 while p < n {
184 let cc: u8 = payload[p]
185 if cc == 32 as u8 { p = p + 1 }
186 else {
187 if cc == 9 as u8 { p = p + 1 }
188 else {
189 if cc == 34 as u8 {
190 // p+1 = first char of value
191 let v: i64 = p + 1
192 // hello
193 if v + 5 <= n {
194 if payload[v] == 104 as u8 {
195 if payload[v+1] == 101 as u8 {
196 if payload[v+2] == 108 as u8 {
197 if payload[v+3] == 108 as u8 {
198 if payload[v+4] == 111 as u8 {
199 if payload[v+5] == 34 as u8 { return 1 }
200 }
201 }
202 }
203 }
204 }
205 }
206 // offer
207 if v + 5 <= n {
208 if payload[v] == 111 as u8 {
209 if payload[v+1] == 102 as u8 {
210 if payload[v+2] == 102 as u8 {
211 if payload[v+3] == 101 as u8 {
212 if payload[v+4] == 114 as u8 {
213 if payload[v+5] == 34 as u8 { return 2 }
214 }
215 }
216 }
217 }
218 }
219 }
220 // answer
221 if v + 6 <= n {
222 if payload[v] == 97 as u8 {
223 if payload[v+1] == 110 as u8 {
224 if payload[v+2] == 115 as u8 {
225 if payload[v+3] == 119 as u8 {
226 if payload[v+4] == 101 as u8 {
227 if payload[v+5] == 114 as u8 {
228 if payload[v+6] == 34 as u8 { return 3 }
229 }
230 }
231 }
232 }
233 }
234 }
235 }
236 // ice
237 if v + 3 <= n {
238 if payload[v] == 105 as u8 {
239 if payload[v+1] == 99 as u8 {
240 if payload[v+2] == 101 as u8 {
241 if payload[v+3] == 34 as u8 { return 4 }
242 }
243 }
244 }
245 }
246 return 0
247 } else { p = n }
248 }
249 }
250 }
251 }
252 p = p + 1
253 }
254 }
255 }
256 }
257 }
258 }
259 }
260 i = i + 1
261 }
262 return 0
263}
264
265// Run the FRIEND dance against an already-connected, upgraded fd.
266// Returns one of NX_FRIEND_* verdicts. is_starter=1 means we send
267// the first hello (so the partner becomes offerer-elect by seeing
268// our hello); is_starter=0 means we wait and send hello AFTER seeing
269// any frame from the partner. In practice both friends send hello;
270// roles are decided by who-sees-whose-hello-first.
271func nx_friend_run_dance(fd: i64,
272 frame_buf: *u8, frame_cap: i64) -> i64 {
273 // Send our hello.
274 let v0: i64 = nx_ws_send_text(fd, nx_friend_hello_bytes(), nx_friend_hello_len())
275 if v0 != NX_WSS_OK { return NX_FRIEND_CONNECT_FAIL }
276
277 var role: i64 = NX_FRIEND_ROLE_NONE
278 var got_offer: i64 = 0
279 var got_answer: i64 = 0
280 var got_ice: i64 = 0
281
282 var iters: i64 = 0
283 var done: i64 = 0
284 while done == 0 {
285 if iters >= NX_FRIEND_MAX_ITERS { done = 1 }
286 iters = iters + 1
287 if done == 1 {} else {
288 let f_raw: *u8 = sys_mmap(128)
289 let f: *WsFrame = f_raw as *WsFrame
290 let v: i64 = nx_ws_read_frame_from_fd(fd, frame_buf, frame_cap, f)
291 if v == NX_WSS_EOF { done = 1 }
292 if v == NX_WSS_READ_FAIL { done = 1 }
293 if v == NX_WSS_OK {
294 if f.opcode == WS_OP_CLOSE { done = 1 }
295 if f.opcode == WS_OP_PING { nx_ws_send_pong(fd, (frame_buf as i64 + f.payload_off) as *u8, f.payload_len) }
296 if f.opcode == WS_OP_PONG {}
297 if f.opcode == WS_OP_TEXT {
298 let kind: i64 = nx_friend_envelope_type(
299 (frame_buf as i64 + f.payload_off) as *u8,
300 f.payload_len)
301 if kind == 1 { // hello
302 // We saw the partner's hello first -> we are offerer.
303 if role == NX_FRIEND_ROLE_NONE {
304 role = NX_FRIEND_ROLE_OFFERER
305 nx_ws_send_text(fd, nx_friend_offer_bytes(), nx_friend_offer_len())
306 }
307 }
308 if kind == 2 { // offer
309 if role == NX_FRIEND_ROLE_NONE {
310 role = NX_FRIEND_ROLE_ANSWERER
311 }
312 got_offer = 1
313 nx_ws_send_text(fd, nx_friend_answer_bytes(), nx_friend_answer_len())
314 nx_ws_send_text(fd, nx_friend_ice_bytes(), nx_friend_ice_len())
315 }
316 if kind == 3 { // answer
317 got_answer = 1
318 nx_ws_send_text(fd, nx_friend_ice_bytes(), nx_friend_ice_len())
319 }
320 if kind == 4 { // ice
321 got_ice = got_ice + 1
322 }
323 if role == NX_FRIEND_ROLE_OFFERER {
324 if got_answer == 1 {
325 if got_ice > 0 { done = 1 }
326 }
327 }
328 if role == NX_FRIEND_ROLE_ANSWERER {
329 if got_offer == 1 {
330 if got_ice > 0 { done = 1 }
331 }
332 }
333 }
334 }
335 }
336 }
337
338 if role == NX_FRIEND_ROLE_NONE { return NX_FRIEND_ROLE_UNDECIDED }
339 if role == NX_FRIEND_ROLE_OFFERER {
340 if got_answer == 0 { return NX_FRIEND_ANSWER_NEVER_ARRIVED }
341 }
342 if role == NX_FRIEND_ROLE_ANSWERER {
343 if got_offer == 0 { return NX_FRIEND_OFFER_NEVER_ARRIVED }
344 }
345 if got_ice == 0 { return NX_FRIEND_ICE_NEVER_RELAYED }
346 return NX_FRIEND_OK
347}
348
349// Top-level orchestrator: TCP-connect to ipv4 octets + port, run
350// client-side WS upgrade against path, then drive the dance.
351func nx_friend_video_call(
352 a: i64, b: i64, c: i64, d: i64, port: i64,
353 host_str: *u8, host_len: i64,
354 path_str: *u8, path_len: i64
355) -> i64 {
356 let fd: i64 = sys_socket(NX_FRIEND_AF_INET, NX_FRIEND_SOCK_STREAM, 0)
357 if fd < 0 { return NX_FRIEND_CONNECT_FAIL }
358
359 let sa: *u8 = sys_mmap(16)
360 _friend_build_sockaddr(sa, a, b, c, d, port)
361 if nx_connect_bounded(fd, sa, 16, NX_CONN_DEFAULT_MS) < 0 {
362 sys_close(fd)
363 return NX_FRIEND_CONNECT_FAIL
364 }
365
366 let req_buf: *u8 = sys_mmap(NX_FRIEND_HDR_BUF_BYTES)
367 let resp_buf: *u8 = sys_mmap(NX_FRIEND_HDR_BUF_BYTES)
368 let uv: i64 = nx_ws_client_upgrade(fd,
369 host_str, host_len,
370 path_str, path_len,
371 port, 1,
372 req_buf, NX_FRIEND_HDR_BUF_BYTES,
373 resp_buf, NX_FRIEND_HDR_BUF_BYTES)
374 if uv != NX_WSCU_OK {
375 sys_close(fd)
376 return NX_FRIEND_UPGRADE_FAIL
377 }
378
379 let frame_buf: *u8 = sys_mmap(NX_FRIEND_FRAME_BUF_BYTES)
380 let verdict: i64 = nx_friend_run_dance(fd, frame_buf, NX_FRIEND_FRAME_BUF_BYTES)
381
382 sys_close(fd)
383 return verdict
384}
385
386func main() -> i64 {
387 return 0
388}