code wiki / _hdl_build / nx_office_daemon.nx
nx_office_daemon.nx source
↩ module page · 275 lines · 15024 B
1// nx_office_daemon.nx -- the DEPLOYABLE Nishi Office daemon (ops shell around the gated pure core nx_office_serve).
2// Binds 0.0.0.0:<port> (LAN-reachable on the NAS); files live under CWD "<root>/". Route /office -> this port on the
3// sovereign edge. R-AUTHZ: resolves the OPAQUE session (X-Nishi-Session header, the `sess` form field on a zero-JS
4// POST, or the `s` query parameter the docportal's no-JS links carry) -> handle, and passes it to of_handle_auth so
5// OWNED docs are ReBAC-gated while unowned docs stay PUBLIC in open mode. FAIL-SAFE: if the auth ctx can't init in
6// OPEN mode, serve PUBLIC-only (owned docs lock, public work); in CLOSED mode it refuses to start (never public).
7//
8// 2026-08-19 (LP3, /compare/legalpractice): ONE binary, N instances. Every instance parameter is argv with today's
9// nishifamily values as defaults, so the deployed daemons.reg row (`./nx_office_daemon.elf`) keeps working unchanged:
10// nx_office_daemon [port] [keysfile] [storefile] [realm] [root] [base] [uididx] [mode] [authzprefix]
11// mode = open (default) unowned documents are public -- nishifamily.com/office
12// mode = closed EVERY request needs a valid session of this realm -- the firm instance on a client domain; the
13// open-mode namespace is NEVER exposed there (a 401 names the login surface instead)
14// The uid->handle index may be the login daemon's TAB index OR a docportal "<storefile>.uidmap" ("MAP <hex> <handle>").
15// When a session is present the HTML response is rewritten so every same-app link carries ?s=<token> (no cookies, C1).
16//
17// FORK-PER-REQUEST (2026-08-19, the docportal's proven shape): of_handle_auth mmaps its per-request scratch (me,
18// fullpath, qs, Origin/Host, the page and capture buffers) and returns from dozens of sites without unmapping --
19// the exact per-request leak class that took nx_opaque_login to 177 GB. A child that handles one connection and
20// then sys_exit(0) reclaims EVERY page BY CONSTRUCTION; the parent only accepts, forks and reaps (non-blocking
21// wait4 + backpressure at a ceiling DERIVED from the host's core count, never guessed). Functionally safe: sessions
22// are file-validated, every office write is a file write under <root>/ (versions, sign logs, authz tuples), and
23// the parent's request/response buffers are never mutated (the child works on its COW copy).
24// Build with --build-only; run deliberately. license_tier: ORIGINAL
25import "nx_office_serve.nx"
26import "nx_opaque_login.nx" // olg_ctx_setup / olg_whoami -- the SAME keys+realm the /login minter signs with
27import "nx_sysload.nx" // sl_ncpu: the fork ceiling is DERIVED from the live box (the docportal's derivation)
28
29const OD_PORT: i64 = 0x1f5e // 8030 (the nishifamily instance)
30const OD_KEYS: *u8 = "opaque_keys.bin"
31const OD_ASTORE: *u8 = "opaque_store.log"
32const OD_IDX: *u8 = "nishi_uid_handle.tsv" // the login daemon's uid(hex)->handle index (authz keys off handle)
33const OD_REALM: *u8 = "nishi_site_admin"
34const OD_REALM_LABEL: *u8 = "Nishi site admin"
35const OD_ROOT: *u8 = "office"
36const OD_BASE: *u8 = "/office"
37// REQUEST CEILING -- one named bound, and it ANNOUNCES: of_handle_auth refuses an import whose Content-Length
38// exceeds what arrived (413) instead of silently importing a truncated package. 8 MiB covers the 1 MiB document.xml
39// the format reader accepts plus media parts; raise it here, in one place, if the office ever ingests bigger files.
40const OD_REQCAP: i64 = 8388608
41const OD_RESCAP: i64 = 1048576
42const OD_MODE_OPEN: i64 = 0
43const OD_MODE_CLOSED: i64 = 1
44const OD_ARG_PORT: i64 = 1
45const OD_ARG_KEYS: i64 = 2
46const OD_ARG_STORE: i64 = 3
47const OD_ARG_REALM: i64 = 4
48const OD_ARG_ROOT: i64 = 5
49const OD_ARG_BASE: i64 = 6
50const OD_ARG_IDX: i64 = 7
51const OD_ARG_MODE: i64 = 8
52const OD_ARG_AZ: i64 = 9
53// the OPAQUE ctx + session scratch sizes the login daemon itself uses (nx_opaque_login: ctx 256, token 600, uid 64)
54const OD_CTX_SZ: i64 = 256
55const OD_TOK_CAP: i64 = 600
56const OD_UID_CAP: i64 = 64
57const OD_HANDLE_CAP: i64 = 256
58const OD_ADDR_SZ: i64 = 16
59const OD_LISTEN_BACKLOG: i64 = 16
60const OD_I64_SZ: i64 = 16
61// argon2id KSF cost: whoami only VALIDATES (register/login live in the minter), so the cost never runs here; the
62// values are the ones the previous build of this daemon carried (256/2/1) and are kept so the ctx is byte-identical.
63const OD_KSF_M: i64 = 256
64const OD_KSF_T: i64 = 2
65const OD_KSF_P: i64 = 1
66// BOUNDED CONCURRENCY (the docportal's derivation, nx_docportal_admin_daemon.dad_worker_cap): cores deliberately
67// left to the rest of the estate, and a floor of 1 (= today's serial behaviour, so this can only ADD concurrency).
68const OD_CPU_RESERVE: i64 = 2
69const OD_MIN_WORKERS: i64 = 1
70// the docportal uidmap row: "MAP " + 64 hex + " " + handle
71const OD_MAP_PFX: *u8 = "MAP "
72const OD_HEXUID: i64 = 64
73
74func d_addr(out: *u8, port: i64) -> i64 {
75 out[0] = 2 as u8; out[1] = 0 as u8
76 out[2] = ((port >> 8) & 0xff) as u8; out[3] = (port & 0xff) as u8
77 out[4] = 0 as u8; out[5] = 0 as u8; out[6] = 0 as u8; out[7] = 0 as u8
78 var i: i64 = 8
79 while i < OD_ADDR_SZ { out[i] = 0 as u8; i = i + 1 }
80 return 0
81}
82
83// uid -> handle from EITHER index shape: the login daemon's TAB index (rb_resolve_handle, single-exit + freed) or
84// the docportal "<storefile>.uidmap" whose rows read "MAP <64 hex> <handle>" (append-only, LAST row wins -- the
85// docportal's own dad_map_get contract). 0 = unmapped (handle stays empty). Whole file read via sys_read_file and
86// released via its PAIRED sys_free_file -- this runs once per authenticated request.
87func od_resolve_handle(idxpath: *u8, uid: *u8, uidn: i64, out_h: *u8, cap: i64) -> i64 {
88 let n1: i64 = rb_resolve_handle(idxpath, uid, uidn, out_h, cap)
89 if n1 > 0 { return n1 }
90 let szp: *i64 = sys_mmap(OD_I64_SZ) as *i64
91 szp[0] = 0
92 let idx: *u8 = sys_read_file(idxpath, szp)
93 var found: i64 = 0
94 if (idx as i64) != 0 {
95 let ux: *u8 = sys_mmap(OD_HEXUID * 2 + 8)
96 let uxn: i64 = rb_hex(ux, uid, uidn)
97 let pl: i64 = of_slen(OD_MAP_PFX)
98 let n: i64 = szp[0]
99 var ls: i64 = 0
100 while ls < n {
101 var le: i64 = ls
102 while le < n { if (idx[le] as i64) == 10 { break } le = le + 1 }
103 if le - ls > pl + uxn + 1 { if of_memhas((idx as i64 + ls) as *u8, pl, OD_MAP_PFX) == 1 {
104 var m: i64 = 1
105 var k: i64 = 0
106 while k < uxn { if idx[ls + pl + k] != ux[k] { m = 0; k = uxn } else { k = k + 1 } }
107 if m == 1 { if idx[ls + pl + uxn] == (32 as u8) {
108 var q: i64 = ls + pl + uxn + 1
109 var o: i64 = 0
110 while q < le { if o < cap - 1 { out_h[o] = idx[q]; o = o + 1 } q = q + 1 }
111 out_h[o] = 0 as u8
112 found = o
113 } }
114 } }
115 ls = le + 1
116 }
117 sys_munmap(ux, OD_HEXUID * 2 + 8)
118 sys_free_file(idx, n)
119 }
120 sys_munmap(szp as *u8, OD_I64_SZ)
121 if found == 0 { out_h[0] = 0 as u8 }
122 return found
123}
124
125// reap every child that has ALREADY exited, without blocking (the docportal's dad_reap_done idiom)
126func od_reap_done(wst: *i64) -> i64 {
127 var n: i64 = 0
128 var go: i64 = 1
129 while go == 1 {
130 let r: i64 = sys_wait4(0 - 1, wst, WNOHANG)
131 if r > 0 { n = n + 1 } else { go = 0 }
132 }
133 return n
134}
135
136func od_worker_cap() -> i64 {
137 let ncpu: i64 = sl_ncpu()
138 var capw: i64 = ncpu - OD_CPU_RESERVE
139 if capw < OD_MIN_WORKERS { capw = OD_MIN_WORKERS }
140 return capw
141}
142
143// the session token of this request, or "" -- header first, then the zero-JS form field, then the ?s= query
144// parameter the no-JS links carry. A token that does not validate in THIS realm is no session at all.
145func od_session(ctx: *NxAuthContext, reqb: *u8, rn: i64, tokb: *u8, qsb: *u8, uid: *u8, uidn: *i64, idx: *u8, hbuf: *u8) -> i64 {
146 hbuf[0] = 0 as u8; tokb[0] = 0 as u8
147 of_hdr_get(reqb, rn, "X-Nishi-Session:" as *u8, tokb, OD_TOK_CAP)
148 if tokb[0] == (0 as u8) {
149 var bs2: i64 = 0 - 1
150 var jj: i64 = 0
151 while jj + 3 < rn {
152 if reqb[jj] == (13 as u8) { if reqb[jj+1] == (10 as u8) { if reqb[jj+2] == (13 as u8) { if reqb[jj+3] == (10 as u8) { bs2 = jj + 4; jj = rn } } } }
153 jj = jj + 1
154 }
155 if bs2 >= 0 { of_form_get((reqb as i64 + bs2) as *u8, rn - bs2, "sess" as *u8, tokb, OD_TOK_CAP) }
156 }
157 if tokb[0] == (0 as u8) {
158 // the request-line query string: "GET /office/doc/x?s=<tok> HTTP/1.1"
159 var qi: i64 = 0
160 var qstart: i64 = 0 - 1
161 var qend: i64 = 0 - 1
162 while qi < rn { if reqb[qi] == (32 as u8) { if qstart < 0 { qstart = qi + 1 } else { if qend < 0 { qend = qi; qi = rn } } } if qi < rn { if reqb[qi] == (10 as u8) { qi = rn } } qi = qi + 1 }
163 if qstart >= 0 { if qend > qstart {
164 var qq: i64 = qstart
165 var qmark: i64 = 0 - 1
166 while qq < qend { if reqb[qq] == (63 as u8) { qmark = qq; qq = qend } else { qq = qq + 1 } }
167 if qmark >= 0 {
168 var qn: i64 = 0
169 var qc: i64 = qmark + 1
170 while qc < qend { if qn < OD_TOK_CAP - 1 { qsb[qn] = reqb[qc]; qn = qn + 1 } qc = qc + 1 }
171 qsb[qn] = 0 as u8
172 of_form_get(qsb, qn, "s" as *u8, tokb, OD_TOK_CAP)
173 }
174 } }
175 }
176 if tokb[0] != (0 as u8) {
177 if olg_whoami(ctx, tokb, of_slen(tokb), sys_now_realtime_sec(), uid, OD_UID_CAP, uidn) == NX_MAUTH_OK {
178 od_resolve_handle(idx, uid, uidn[0], hbuf, OD_HANDLE_CAP)
179 }
180 if hbuf[0] == (0 as u8) { tokb[0] = 0 as u8 }
181 }
182 return 0
183}
184
185func main(argc: i64, argv: *i64) -> i64 {
186 var port: i64 = OD_PORT
187 var keys: *u8 = OD_KEYS
188 var store: *u8 = OD_ASTORE
189 var realm: *u8 = OD_REALM
190 var root: *u8 = OD_ROOT
191 var base: *u8 = OD_BASE
192 var idx: *u8 = OD_IDX
193 var mode: i64 = OD_MODE_OPEN
194 var az: *u8 = OF_AUTHZ
195 if argc > OD_ARG_PORT { port = of_atoi(argv[OD_ARG_PORT] as *u8) }
196 if argc > OD_ARG_KEYS { keys = argv[OD_ARG_KEYS] as *u8 }
197 if argc > OD_ARG_STORE { store = argv[OD_ARG_STORE] as *u8 }
198 if argc > OD_ARG_REALM { realm = argv[OD_ARG_REALM] as *u8 }
199 if argc > OD_ARG_ROOT { root = argv[OD_ARG_ROOT] as *u8 }
200 if argc > OD_ARG_BASE { base = argv[OD_ARG_BASE] as *u8 }
201 if argc > OD_ARG_IDX { idx = argv[OD_ARG_IDX] as *u8 }
202 if argc > OD_ARG_MODE { if of_seq(argv[OD_ARG_MODE] as *u8, "closed" as *u8) == 1 { mode = OD_MODE_CLOSED } }
203 // the ReBAC authz plane of THIS instance (argv 9): a tenant instance MUST carry its own prefix so document ownership,
204 // shares and groups never cross realms; the default keeps the nishifamily instance on its existing plane unchanged
205 if argc > OD_ARG_AZ { az = argv[OD_ARG_AZ] as *u8 }
206 if port <= 0 { p("NX-OFFICE-DAEMON bad port argument -- fail loud\n" as *u8); return 1 }
207 // authz tuples (officeauthz_*) live in the cwd (nishihost/), which this daemon's user owns -- no mkdir needed.
208 // AUTH ctx -- FAIL-SAFE in open mode: on failure serve PUBLIC-only. In CLOSED mode an unusable auth context is
209 // FATAL, because every request must be authenticated and "public-only" would be exactly the exposure forbidden.
210 let ctx: *NxAuthContext = sys_mmap(OD_CTX_SZ) as *NxAuthContext
211 var auth_off: i64 = 0
212 if olg_ctx_setup(ctx, keys, store, realm, of_slen(realm), OD_REALM_LABEL, of_slen(OD_REALM_LABEL), OD_KSF_M, OD_KSF_T, OD_KSF_P) != 0 {
213 auth_off = 1
214 if mode == OD_MODE_CLOSED { p("NX-OFFICE-DAEMON closed mode and the auth ctx is unavailable -- refusing to start (fail loud, never public)\n" as *u8); return 1 }
215 p("NX-OFFICE-DAEMON auth ctx unavailable -> PUBLIC-ONLY mode (owned docs locked)\n" as *u8)
216 }
217 let addr: *u8 = sys_mmap(OD_ADDR_SZ)
218 d_addr(addr, port)
219 let lfd: i64 = sys_socket(AF_INET, SOCK_STREAM, 0)
220 if lfd < 0 { p("NX-OFFICE-DAEMON socket FAILED -- fail loud\n" as *u8); return 1 }
221 let one: *i64 = (sys_mmap(8)) as *i64
222 one[0] = 1
223 sys_setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, one as *u8, 4)
224 if sys_bind(lfd, addr, OD_ADDR_SZ) < 0 { p("NX-OFFICE-DAEMON bind FAILED (port busy?) -- fail loud\n" as *u8); return 1 }
225 if sys_listen(lfd, OD_LISTEN_BACKLOG) < 0 { p("NX-OFFICE-DAEMON listen FAILED -- fail loud\n" as *u8); return 1 }
226 let wcap: i64 = od_worker_cap()
227 p("NX-OFFICE-DAEMON serving port=" as *u8); pn(port); p(" root=" as *u8); p(root); p(" base=" as *u8); p(base); p(" realm=" as *u8); p(realm)
228 if mode == OD_MODE_CLOSED { p(" mode=closed (every request needs a session)" as *u8) } else { p(" mode=open (unowned docs public)" as *u8) }
229 p(" authz=" as *u8); p(az); p(" AUTHZ=nx_rebac fork-per-request workers=" as *u8); pn(wcap); p(" ncpu=" as *u8); pn(sl_ncpu()); p("\n" as *u8)
230
231 let reqb: *u8 = sys_mmap(OD_REQCAP)
232 let resb: *u8 = sys_mmap(OD_RESCAP)
233 let hbuf: *u8 = sys_mmap(OD_HANDLE_CAP)
234 let tokb: *u8 = sys_mmap(OD_TOK_CAP)
235 let qsb: *u8 = sys_mmap(OD_TOK_CAP)
236 let uid: *u8 = sys_mmap(OD_UID_CAP)
237 let uidn: *i64 = sys_mmap(OD_I64_SZ) as *i64
238 let wst: *i64 = sys_mmap(OD_I64_SZ) as *i64
239 var live: i64 = 0
240 var go: i64 = 1
241 while go == 1 {
242 let cfd: i64 = sys_accept(lfd)
243 if cfd >= 0 {
244 // DoS-starvation bound (nx_dos_timeout_scan seq321): one peer declaring a body it never finishes
245 // sending would otherwise hold its child forever. ACCEPT_TMO_S is the shared named bound.
246 sys_set_socket_timeout(cfd, ACCEPT_TMO_S)
247 let kid: i64 = sys_fork()
248 if kid == 0 {
249 sys_close(lfd)
250 let rn: i64 = of_read_req(cfd, reqb, OD_REQCAP)
251 if rn > 0 {
252 hbuf[0] = 0 as u8; tokb[0] = 0 as u8
253 if auth_off == 0 { od_session(ctx, reqb, rn, tokb, qsb, uid, uidn, idx, hbuf) }
254 var on: i64 = 0
255 if mode == OD_MODE_CLOSED { if hbuf[0] == (0 as u8) {
256 on = of_err(resb, "401 Unauthorized" as *u8, "this office instance is closed: sign in at the firm portal (admin.<domain>) and open the office from there, or send X-Nishi-Session" as *u8)
257 } }
258 if on == 0 { on = of_handle_auth(root, base, reqb, rn, resb, OD_RESCAP, hbuf, az, tokb) }
259 if on > 0 { on = of_propagate_tok(resb, on, base, tokb, OD_RESCAP) }
260 if on > 0 { of_write_all(cfd, resb, on) }
261 }
262 sys_close(cfd)
263 sys_exit(0)
264 }
265 sys_close(cfd)
266 if kid > 0 { live = live + 1 }
267 live = live - od_reap_done(wst)
268 // block ONLY at the ceiling, and only until one slot frees: backpressure, not serialization
269 while live >= wcap {
270 if sys_wait4(0 - 1, wst, 0) > 0 { live = live - 1 } else { live = 0 }
271 }
272 }
273 }
274 return 0
275}