code wiki / (root) / nx_friend_video_caller.nx

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}