code wiki / (root) / nx_external_reach_probe.nx

nx_external_reach_probe.nx source

↩ module page · 302 lines · 14674 B

1// nx_external_reach_probe.nx -- continuously probe an external endpoint for 2// per-stage reachability + latency; alert on degradation. 3// 4// module: nishi-core.perception.external_reach_probe 5// depends: nishi-core.perception.profile + nishi-core.perception.perceptual_dataset + 6// nishi-core.perception.provenance_curve_store + nishi-core.perception.instrument_diagnostician + 7// nishi-core.net.socket + nishi-core.io.syscalls 8// disk_kb: 6 9// capability: PERCEPTION 10// wired_status: PARTIAL_WIRED (nx_reach_probe_now GRADUATED to basic-glue 11// 2026-05-20: composes nx_https_get + nx_clock_monotonic_ns 12// for total-time + overall verdict; per-stage 13// breakdown still queued) 14// 15// MISSING_CAPABILITIES (still queued): 16// - PER_STAGE_CLOCK_TAPS (separate DNS / TCP / TLS / HTTP timings; 17// requires calling nx_https_url_for_fetch + nx_https_url_connect + 18// nx_tls13_client_session_run + nx_http_get directly with clock taps 19// between each phase instead of single nx_https_get black box) 20// - SCHEDULED_PROBE_LOOP (composes with nx_race_timing CONTINUOUS or SCHEDULED 21// modes already FULLY_WIRED) 22// - DEGRADATION_ALERT_DISPATCH (composes with nx_instrument_diagnostician 23// for named-fix output when reachability drops below threshold) 24// - PEER_HOST_DISPATCH (probe runs from one or more peer hosts so a single 25// monitoring vantage point doesn't masquerade as "the whole world reaches 26// us") 27// - CALLER_PROVIDED_TRUST_STORE_AND_RNG (v1 uses fixed seed + empty trust 28// store; v2 accepts caller-provided per user-owns-every-bit cardinal) 29// - DATASET_PERSIST (write result to nx_perceptual_dataset as 30// DRIFT_OBSERVATION entry; queued behind nx_pds_write_trial wire-up) 31// 32// license_tier: PUBLIC_NISHI_SUBSTRATE 33// genealogy_id: feedback-substrate-does-heavy-lifting-user-is-partner-not-gate_2026 + 34// feedback-substrate-primitives-meta-not-one-off_2026 + 35// feedback-real-playtest-loop-not-just-logs_2026 + 36// feedback-no-false-ok-substrate-honesty-audit + 37// nx_realism_score + 38// NISHIFAMILY_VIDEO_HARDENING_2026-05-20 + 39// feedback-self-surfacing-intelligence-staged-autonomy 40// 41// Triggered by 2026-05-20 external probe of nishifamily.com/video finding 42// catastrophic NETWORK_TRANSIT failure (port 443 blocked from external, 43// 21s TCP timeout) that the in-NAS nginx-log watchdog by construction 44// could not detect: no requests reach nginx when the network is broken 45// outside the LAN, so the watchdog stays green while users hit timeouts. 46// 47// THIS primitive runs from one or more peer hosts OUTSIDE the LAN and 48// probes the public endpoint on a schedule. Per-stage timings (DNS, 49// TCP connect, TLS handshake, HTTP first-byte) recorded in 50// nx_provenance_curve_store; degradation surfaces via 51// nx_instrument_diagnostician with named-fix hints (Quickconnect down, 52// port-forward broken, cert expired, etc.). 53// 54// Reuse set: 55// - nishifamily.com/video external reach (PRIMARY use case) 56// - Any future Nishi-hosted public endpoint 57// - Customer-deployed dog toys reporting back to substrate health 58// - Livestock-acoustic-welfare gateways calling home 59// - Conservation acoustic recorders pushing data to central 60// - Vet-clinic stethoscope uplinks 61// - Any deployed instrument reporting reachability to a substrate operator 62 63import "nx_syscalls.nx" 64import "nx_perceptual_profile.nx" 65import "nx_perceptual_dataset.nx" 66import "nx_provenance_curve_store.nx" 67import "nx_instrument_diagnostician.nx" 68import "nx_https_get.nx" 69import "nx_clock.nx" 70import "nx_x509_trust_store.nx" 71const NX_MAGIC_65536: i64 = 65536 72const NX_MAGIC_1718452800: i64 = 1718452800 73const NX_MAGIC_1000000: i64 = 1000000 74 75// ===== Probe stage sealed enum (matches nx_realism_score stages where applicable) === 76 77const NX_REACH_STAGE_DNS_RESOLVE: i64 = 1 78const NX_REACH_STAGE_TCP_CONNECT: i64 = 2 79const NX_REACH_STAGE_TLS_HANDSHAKE: i64 = 3 80const NX_REACH_STAGE_HTTP_TTFB: i64 = 4 81const NX_REACH_STAGE_PAYLOAD_DOWNLOAD: i64 = 5 82const NX_REACH_STAGE_TOTAL_END_TO_END: i64 = 6 83 84func nx_reach_stage_name(s: i64) -> *u8 { 85 if s == NX_REACH_STAGE_DNS_RESOLVE { return "DNS_RESOLVE" } 86 if s == NX_REACH_STAGE_TCP_CONNECT { return "TCP_CONNECT" } 87 if s == NX_REACH_STAGE_TLS_HANDSHAKE { return "TLS_HANDSHAKE" } 88 if s == NX_REACH_STAGE_HTTP_TTFB { return "HTTP_TTFB" } 89 if s == NX_REACH_STAGE_PAYLOAD_DOWNLOAD { return "PAYLOAD_DOWNLOAD" } 90 if s == NX_REACH_STAGE_TOTAL_END_TO_END { return "TOTAL_END_TO_END" } 91 return "UNKNOWN_REACH_STAGE" 92} 93 94// ===== Probe verdicts ============================================= 95 96const NX_REACH_OK: i64 = 0 97const NX_REACH_DEGRADED_SLOW: i64 = 1 // completes but exceeds threshold 98const NX_REACH_FAIL_DNS: i64 = 2 99const NX_REACH_FAIL_TCP_TIMEOUT: i64 = 3 // 2026-05-20 nishifamily.com:443 mode 100const NX_REACH_FAIL_TLS_HANDSHAKE: i64 = 4 101const NX_REACH_FAIL_HTTP_ERROR: i64 = 5 // 4xx / 5xx 102const NX_REACH_FAIL_PAYLOAD_INCOMPLETE: i64 = 6 // received < Content-Length 103const NX_REACH_FAIL_DEPENDENCY_MISSING: i64 = 7 // PARTIAL_WIRED default 104 105func nx_reach_verdict_name(v: i64) -> *u8 { 106 if v == NX_REACH_OK { return "OK" } 107 if v == NX_REACH_DEGRADED_SLOW { return "DEGRADED_SLOW" } 108 if v == NX_REACH_FAIL_DNS { return "FAIL_DNS" } 109 if v == NX_REACH_FAIL_TCP_TIMEOUT { return "FAIL_TCP_TIMEOUT" } 110 if v == NX_REACH_FAIL_TLS_HANDSHAKE { return "FAIL_TLS_HANDSHAKE" } 111 if v == NX_REACH_FAIL_HTTP_ERROR { return "FAIL_HTTP_ERROR" } 112 if v == NX_REACH_FAIL_PAYLOAD_INCOMPLETE { return "FAIL_PAYLOAD_INCOMPLETE" } 113 if v == NX_REACH_FAIL_DEPENDENCY_MISSING { return "FAIL_DEPENDENCY_MISSING" } 114 return "UNKNOWN_REACH_VERDICT" 115} 116 117// ===== Probe result struct ======================================== 118// 119// Bundled per [[feedback-nishilang-16-arg-function-limit]]. One result 120// per probe run; substrate writes to nx_perceptual_dataset as 121// DRIFT_OBSERVATION kind for the endpoint. 122 123struct NxReachProbeResult { 124 endpoint_hash_ptr: *u8 125 endpoint_hash_len: i64 // content-hashed hostname:port 126 probe_host_id: i64 // which peer host ran the probe 127 timestamp_unix_ms: i64 128 overall_verdict: i64 // NX_REACH_* 129 dns_resolve_us: i64 // microseconds 130 tcp_connect_us: i64 // -1 if never reached 131 tls_handshake_us: i64 132 http_ttfb_us: i64 133 payload_download_us: i64 134 total_end_to_end_us: i64 135 failure_stage: i64 // NX_REACH_STAGE_* where it stopped 136 http_status_code: i64 // -1 if no HTTP reached 137} 138 139// ===== Top-level entry stubs ====================================== 140 141// nx_reach_probe_now -- run a single immediate probe against the named 142// endpoint; record result in nx_perceptual_dataset. Called by scheduled 143// loop OR by user-triggered "is the site up right now?" inspector. 144// 145// GRADUATED 2026-05-20 from PARTIAL_WIRED → wired_at_basic_glue: now 146// calls nx_https_get + nx_clock_monotonic_ns for total-time + verdict. 147// Per-stage breakdown (separate DNS / TCP / TLS / HTTP timings) still 148// queued -- requires direct sub-primitive calls (nx_sock_connect + 149// nx_tls13_client_handshake + nx_http_get) with per-stage clock taps. 150// 151// v1 mapping nx_https_get → NX_REACH_*: 152// r > 0 -> NX_REACH_OK (positive = byte count) 153// r == -NX_HTTPS_GET_BAD_URL -> FAIL_DNS-class (URL parse failure) 154// r == -NX_HTTPS_GET_CONNECT_FAIL -> FAIL_TCP_TIMEOUT 155// r == -NX_HTTPS_GET_HANDSHAKE_FAIL -> FAIL_TLS_HANDSHAKE 156// r == -NX_HTTPS_GET_FETCH_FAIL -> FAIL_HTTP_ERROR 157// 158// Caller responsibilities (user-owns-every-bit): 159// - Provide endpoint_url as null-terminated bytes in endpoint_url_ptr 160// - Provide a TrustStore (can be empty for "TCP+TLS handshake reachability" 161// probe; populated for full cert-validation probe) 162// - Provide client_random + x25519_priv (caller's RNG) 163// - Allocate result_out_ptr struct 164// Timeouts are accepted in the signature for future per-stage enforcement 165// but v1 graduation does not apply them (nx_https_get's own internal 166// timeouts apply). 167 168func nx_reach_probe_now(endpoint_url_ptr: *u8, endpoint_url_len: i64, 169 tcp_timeout_ms: i64, 170 tls_timeout_ms: i64, 171 http_timeout_ms: i64, 172 result_out_ptr: *NxReachProbeResult) -> i64 { 173 if endpoint_url_len <= 0 { return NX_REACH_FAIL_DEPENDENCY_MISSING } 174 if tcp_timeout_ms <= 0 { return NX_REACH_FAIL_DEPENDENCY_MISSING } 175 if tls_timeout_ms <= 0 { return NX_REACH_FAIL_DEPENDENCY_MISSING } 176 if http_timeout_ms <= 0 { return NX_REACH_FAIL_DEPENDENCY_MISSING } 177 178 // Caller-provided ephemeral crypto state per substrate convention. 179 // v1 graduation uses fixed seed for repeatable measurement; v2 180 // accepts caller-provided RNG output. 181 let client_random: *u8 = sys_mmap(32) 182 let x25519_priv: *u8 = sys_mmap(32) 183 var i: i64 = 0 184 while i < 32 { 185 client_random[i] = (0x42 + i) as u8 186 x25519_priv[i] = (0x73 + i) as u8 187 i = i + 1 188 } 189 190 // EMPTY trust store probes reachability up to + including the TLS 191 // Certificate message receipt; chain validation will fail without 192 // anchors but that is reported as FAIL_TLS_HANDSHAKE which is the 193 // honest verdict for "we could not establish a validated TLS 194 // session." Caller can supply populated store for end-to-end OK. 195 let store: *TrustStore = trust_store_alloc(4) 196 197 // 64 KB response buffer; reach probe ignores body content. 198 let resp_buf: *u8 = sys_mmap(NX_MAGIC_65536) 199 200 let t_start_ns: i64 = nx_clock_monotonic_ns() 201 let r: i64 = nx_https_get(endpoint_url_ptr, client_random, x25519_priv, 202 store, NX_MAGIC_1718452800, resp_buf, NX_MAGIC_65536) 203 let total_us: i64 = nx_clock_elapsed_us(t_start_ns) 204 205 // Write what we know to the result struct. Per-stage fields stay 206 // at -1 (unmeasured) until per-stage clock taps land. 207 result_out_ptr.timestamp_unix_ms = nx_clock_realtime_ns() / NX_MAGIC_1000000 208 result_out_ptr.total_end_to_end_us = total_us 209 result_out_ptr.dns_resolve_us = 0 - 1 210 result_out_ptr.tcp_connect_us = 0 - 1 211 result_out_ptr.tls_handshake_us = 0 - 1 212 result_out_ptr.http_ttfb_us = 0 - 1 213 result_out_ptr.payload_download_us = 0 - 1 214 result_out_ptr.http_status_code = 0 - 1 215 result_out_ptr.probe_host_id = 0 216 result_out_ptr.endpoint_hash_ptr = endpoint_url_ptr 217 result_out_ptr.endpoint_hash_len = endpoint_url_len 218 219 if r > 0 { 220 result_out_ptr.overall_verdict = NX_REACH_OK 221 result_out_ptr.failure_stage = 0 222 return NX_REACH_OK 223 } 224 // r is negative verdict code; map to NX_REACH_*. 225 if r == (0 - NX_HTTPS_GET_BAD_URL) { 226 result_out_ptr.overall_verdict = NX_REACH_FAIL_DNS 227 result_out_ptr.failure_stage = NX_REACH_STAGE_DNS_RESOLVE 228 return NX_REACH_FAIL_DNS 229 } 230 if r == (0 - NX_HTTPS_GET_CONNECT_FAIL) { 231 result_out_ptr.overall_verdict = NX_REACH_FAIL_TCP_TIMEOUT 232 result_out_ptr.failure_stage = NX_REACH_STAGE_TCP_CONNECT 233 return NX_REACH_FAIL_TCP_TIMEOUT 234 } 235 if r == (0 - NX_HTTPS_GET_HANDSHAKE_FAIL) { 236 result_out_ptr.overall_verdict = NX_REACH_FAIL_TLS_HANDSHAKE 237 result_out_ptr.failure_stage = NX_REACH_STAGE_TLS_HANDSHAKE 238 return NX_REACH_FAIL_TLS_HANDSHAKE 239 } 240 if r == (0 - NX_HTTPS_GET_FETCH_FAIL) { 241 result_out_ptr.overall_verdict = NX_REACH_FAIL_HTTP_ERROR 242 result_out_ptr.failure_stage = NX_REACH_STAGE_HTTP_TTFB 243 return NX_REACH_FAIL_HTTP_ERROR 244 } 245 // Unrecognized return code: treat as dependency-missing. 246 result_out_ptr.overall_verdict = NX_REACH_FAIL_DEPENDENCY_MISSING 247 result_out_ptr.failure_stage = 0 248 return NX_REACH_FAIL_DEPENDENCY_MISSING 249} 250 251// nx_reach_probe_register_endpoint -- caller registers an endpoint 252// to be probed on a schedule, with degradation threshold + alert hook. 253 254func nx_reach_probe_register_endpoint(endpoint_url_ptr: *u8, endpoint_url_len: i64, 255 schedule: i64, 256 slow_threshold_ms: i64, 257 alert_hook_ptr: *u8, alert_hook_len: i64) -> i64 { 258 if endpoint_url_len <= 0 { return 0 } 259 if slow_threshold_ms <= 0 { return 0 } 260 // composes with nx_race_timing schedule enum (already FULLY_WIRED) 261 return 0 262} 263 264// nx_reach_probe_get_last_result -- inspector: most recent probe result 265// for a registered endpoint. 266 267func nx_reach_probe_get_last_result(endpoint_url_ptr: *u8, endpoint_url_len: i64, 268 result_out_ptr: *NxReachProbeResult) -> i64 { 269 if endpoint_url_len <= 0 { return NX_REACH_FAIL_DEPENDENCY_MISSING } 270 return NX_REACH_FAIL_DEPENDENCY_MISSING 271} 272 273// nx_reach_probe_count_failures_in_window -- inspector: how many failure 274// probes occurred in the last N minutes? Substrate alerts when this 275// exceeds threshold (3 consecutive failures = real outage, not just 276// transient). 277 278func nx_reach_probe_count_failures_in_window(endpoint_url_ptr: *u8, endpoint_url_len: i64, 279 window_minutes: i64) -> i64 { 280 if endpoint_url_len <= 0 { return 0 } 281 if window_minutes <= 0 { return 0 } 282 return 0 283} 284 285// nx_reach_probe_diagnose_failure -- given the failure stage from the 286// last result, compose with nx_instrument_diagnostician to emit named 287// fix hint. E.g., FAIL_TCP_TIMEOUT on :443 but DNS resolves + :80 288// reachable -> "Quickconnect down or port-forward broken; check NAS-side 289// Quickconnect daemon + router port-forward rules." 290 291func nx_reach_probe_diagnose_failure(result_ptr: *NxReachProbeResult, 292 fix_text_buf_ptr: *u8, 293 fix_text_buf_cap: i64) -> i64 { 294 if fix_text_buf_cap <= 0 { return 0 } 295 return 0 296} 297 298// nx_reach_probe_get_last_verdict -- inspector. 299 300func nx_reach_probe_get_last_verdict() -> i64 { 301 return NX_REACH_FAIL_DEPENDENCY_MISSING 302}