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}