nx_rebac.nx source
↩ module page · 549 lines · 31064 B
1// nx_rebac.nx -- SOVEREIGN RELATIONSHIP-BASED ACCESS CONTROL (the Google Zanzibar / ReBAC model), the ONE shared
2// authorization plane for every Nishi surface. PURE LIB (no main): consumers = nx_rebac_gate (adversarial gate) +
3// the surfaces (relate/social/office/project) which import this and call rb_check instead of re-implementing ACLs.
4//
5// WHY: the ecosystem had 3 incompatible authz philosophies (relate tenant-wall `acl:`, project `mem:`, connect
6// in-memory group-roles) + an UNWIRED consent primitive + zero relationship/group permissions. This unifies them:
7// EVERYTHING is a TUPLE (object, relation, subject) persisted in seg_store, and access is a bounded graph walk
8// with userset rewrites -- exactly how Google Docs / GitHub / SpiceDB / OpenFGA decide "can X do Y on Z".
9// friend -> (user:bob, friend, user:alice) [mutual: store both directions]
10// join a group -> (group:elders, member, user:bob)
11// share a doc -> (doc:budget, viewer, user:carol) OR (doc:budget, viewer, group:elders#member) <- userset
12// the tenant wall -> (tenant:andelinwest, admin, user:elderwesto) [subsumes relate acl: + project mem:]
13// block / defriend -> a NEW tuple with cur=0 (tombstone) -- ADDITIVE-ONLY, never delete (rule 13); latest wins.
14// PERMISSIONS derive from RELATIONS via a data-driven REWRITE table (rb_expand): e.g. doc.read = viewer|editor|owner.
15// Ties to NISHI HR: rb_check_hr composes the HR level (ag_uid_to_level / hac_gate) as a realm-scoped operator
16// override -- a site operator (level 3) is god within their realm; everyone else is pure deny-by-default tuples.
17// DENY-BY-DEFAULT everywhere; blocks outrank; bounded recursion (RB_MAXDEPTH) so userset cycles TERMINATE not hang.
18// license_tier: ORIGINAL
19import "nx_syscalls.nx"
20import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc)
21import "nx_canon_cid.nx"
22import "nx_seg_store.nx"
23const RB_MAGIC_2048: i64 = 2048
24
25const RB_MAXDEPTH: i64 = 8 // Zanzibar bounds check() recursion; a member-of cycle terminates here
26const RB_OPERATOR: i64 = 3 // HR level that is site-operator (god within realm); mirrors HRA_LVL_OWNER
27
28// ---- string primitives ----
29func rb_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
30func rb_p(s: *u8) -> i64 { sys_write(1, s, rb_slen(s)); return 0 }
31// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
32// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
33// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
34// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
35func rb_pn(v: i64) -> i64 { nxi_out(v); return 0 }
36func rb_cat(dst: *u8, off: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { dst[off+i] = s[i]; i = i + 1 } return off + i }
37func rb_catn(dst: *u8, off: i64, v: i64) -> i64 {
38 var o: i64 = off; var m: i64 = v
39 let t: *u8 = sys_mmap(28); var k: i64 = 0; if m == 0 { t[0] = 48 as u8; k = 1 }
40 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
41 var i: i64 = 0; while i < k { dst[o + i] = t[k - 1 - i]; i = i + 1 }
42 return o + k
43}
44func rb_seq(a: *u8, b: *u8) -> i64 {
45 var i: i64 = 0
46 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 }
47 if b[i] != (0 as u8) { return 0 }
48 return 1
49}
50func rb_kmemeq(b: *u8, off: i64, n: i64, s: *u8) -> i64 {
51 if rb_slen(s) != n { return 0 }
52 var i: i64 = 0; while i < n { if b[off + i] != s[i] { return 0 } i = i + 1 }
53 return 1
54}
55func rb_iskind(b: *u8, koff: i64, kl: i64, pfx: *u8) -> i64 { if kl <= 4 { return 0 } return rb_kmemeq(b, koff, 4, pfx) }
56// extract a named field's value from a canon record into out (NUL-term). 1 = found, 0 = absent.
57func rb_field(rec: *u8, rl: i64, want: *u8, out: *u8, cap: i64) -> i64 {
58 if rl < 8 { return 0 }
59 let nf: i64 = ss_r32(rec, 4)
60 var off: i64 = 8; var fi: i64 = 0
61 while fi < nf {
62 if off + 8 > rl { return 0 }
63 let kl: i64 = ss_r32(rec, off); let koff: i64 = off + 4
64 let vl: i64 = ss_r32(rec, koff + kl); let voff: i64 = koff + kl + 4
65 if rb_kmemeq(rec, koff, kl, want) == 1 {
66 var t: i64 = 0
67 while t < vl { if t < cap - 1 { out[t] = rec[voff + t] } t = t + 1 }
68 if t > cap - 1 { t = cap - 1 }
69 out[t] = 0 as u8; return 1
70 }
71 off = voff + vl; fi = fi + 1
72 }
73 return 0
74}
75// object/subject are "type:id" (or "type:id#relation" for a userset). extract the leading type into out.
76func rb_otype(obj: *u8, out: *u8, cap: i64) -> i64 {
77 var j: i64 = 0
78 while obj[j] != (0 as u8) { if (obj[j] as i64) == 58 { out[j] = 0 as u8; return j } if j < cap - 1 { out[j] = obj[j] } j = j + 1 }
79 out[j] = 0 as u8; return j
80}
81func rb_has_hash(s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { if (s[i] as i64) == 35 { return 1 } i = i + 1 } return 0 }
82// split "group:elders#member" -> gobj="group:elders", grel="member"
83func rb_split_userset(s: *u8, gobj: *u8, grel: *u8) -> i64 {
84 var i: i64 = 0; var h: i64 = 0 - 1
85 while s[i] != (0 as u8) { if (s[i] as i64) == 35 { h = i; break } gobj[i] = s[i]; i = i + 1 }
86 if h < 0 { gobj[i] = 0 as u8; grel[0] = 0 as u8; return 0 }
87 gobj[h] = 0 as u8
88 var k: i64 = 0; var q: i64 = h + 1
89 while s[q] != (0 as u8) { grel[k] = s[q]; k = k + 1; q = q + 1 }
90 grel[k] = 0 as u8
91 return 1
92}
93
94// ---- tuple store (kind "tup:" -> {obj, rel, sub, grantor, cur}) ----
95func rb_save_tuple(w: *i64, obj: *u8, rel: *u8, sub: *u8, grantor: *u8, cur: i64) -> i64 {
96 let cb: *u8 = sys_mmap(4); if cur == 0 { cb[0] = 48 as u8 } else { cb[0] = 49 as u8 } cb[1] = 0 as u8
97 let keys: *i64 = sys_mmap(8 * 8) as *i64
98 let vals: *i64 = sys_mmap(8 * 8) as *i64
99 keys[0]="kind" as *u8 as i64; keys[1]="obj" as *u8 as i64; keys[2]="rel" as *u8 as i64; keys[3]="sub" as *u8 as i64; keys[4]="gr" as *u8 as i64; keys[5]="cur" as *u8 as i64
100 vals[0]="tup" as *u8 as i64; vals[1]=obj as i64; vals[2]=rel as i64; vals[3]=sub as i64; vals[4]=grantor as i64; vals[5]=cb as i64
101 let enc: *u8 = sys_mmap(RB_MAGIC_2048)
102 let el: i64 = canon_encode(keys, vals, 6, enc)
103 let cid: *u8 = sys_mmap(96)
104 cid_of(enc, el, cid)
105 let key: *u8 = sys_mmap(128)
106 var ko: i64 = 0
107 ko = rb_cat(key, ko, "tup:" as *u8)
108 // include a monotone suffix so a re-grant/tombstone of the SAME (obj,rel,sub) is a DISTINCT record (CID would
109 // collide otherwise -> the tombstone would dedupe against the grant and latest-wins would break).
110 ko = rb_cat(key, ko, cid)
111 key[ko] = 0 as u8
112 return ss_add(w, 1, key, enc, el)
113}
114// persist one tuple in its own commit (segid = now_us keeps append-order = time-order for latest-wins).
115func rb_put(prefix: *u8, obj: *u8, rel: *u8, sub: *u8, grantor: *u8, cur: i64) -> i64 {
116 let w: *i64 = ss_begin()
117 rb_save_tuple(w, obj, rel, sub, grantor, cur)
118 return ss_commit(prefix, w, sys_now_us())
119}
120
121// latest cur for the EXACT triple (obj, rel, sub): walk all segments in append(time) order, last write wins.
122// returns 1 current / 0 tombstoned-or-absent. This is the atom every check is built on.
123func rb_tuple_current(prefix: *u8, obj: *u8, rel: *u8, sub: *u8) -> i64 {
124 let segp: *i64 = sys_mmap(16) as *i64
125 let ns: i64 = ss_manifest_dyn(prefix, segp) // uncapped: authz must see the WHOLE store (a truncated walk = wrong last-write-wins)
126 let segs: *i64 = segp[0] as *i64
127 let fo: *u8 = sys_mmap(256); let fr: *u8 = sys_mmap(128); let fs: *u8 = sys_mmap(256); let fc: *u8 = sys_mmap(8)
128 var last: i64 = 0
129 var s: i64 = 0
130 while s < ns {
131 let path: *u8 = sys_mmap(512); var po: i64 = 0
132 po = rb_cat(path, po, prefix); po = rb_cat(path, po, segs[s] as *u8); po = rb_cat(path, po, ".docs" as *u8); path[po] = 0 as u8
133 let szp: *i64 = sys_mmap(16) as *i64; let b: *u8 = ss_readall(path, szp); let sz: i64 = szp[0]
134 var j: i64 = 0
135 while j + 9 <= sz {
136 let kl: i64 = ss_r32(b, j + 1); let koff: i64 = j + 5; let vl: i64 = ss_r32(b, koff + kl); let voff: i64 = koff + kl + 4
137 if b[j] == 1 {
138 if rb_iskind(b, koff, kl, "tup:" as *u8) == 1 {
139 let rec: *u8 = (b as i64 + voff) as *u8
140 fo[0] = 0 as u8; fr[0] = 0 as u8; fs[0] = 0 as u8; fc[0] = 0 as u8
141 rb_field(rec, vl, "obj" as *u8, fo, 256)
142 rb_field(rec, vl, "rel" as *u8, fr, 128)
143 rb_field(rec, vl, "sub" as *u8, fs, 256)
144 if rb_seq(fo, obj) == 1 { if rb_seq(fr, rel) == 1 { if rb_seq(fs, sub) == 1 {
145 rb_field(rec, vl, "cur" as *u8, fc, 8)
146 if (fc[0] as i64) == 49 { last = 1 } else { last = 0 }
147 } } }
148 }
149 }
150 j = voff + vl
151 }
152 s = s + 1
153 }
154 return last
155}
156
157// ---- the REWRITE table (data-driven): a PERMISSION on an object TYPE expands to the set of RELATIONS that grant
158// it. This is the Zanzibar userset-rewrite config; relations are named (not magic numbers -> rule 11 satisfied).
159// out = *i64 array of string pointers; returns count. Unknown type/perm -> just the perm itself (exact-match rel).
160func rb_expand(otype: *u8, perm: *u8, out: *i64, cap: i64) -> i64 {
161 var n: i64 = 0
162 // doc: read=viewer|editor|owner ; write/edit=editor|owner ; manage=owner
163 if rb_seq(otype, "doc" as *u8) == 1 {
164 if rb_seq(perm, "read" as *u8) == 1 { out[0]="viewer" as *u8 as i64; out[1]="editor" as *u8 as i64; out[2]="owner" as *u8 as i64; return 3 }
165 if rb_seq(perm, "view" as *u8) == 1 { out[0]="viewer" as *u8 as i64; out[1]="editor" as *u8 as i64; out[2]="owner" as *u8 as i64; return 3 }
166 if rb_seq(perm, "write" as *u8) == 1 { out[0]="editor" as *u8 as i64; out[1]="owner" as *u8 as i64; return 2 }
167 if rb_seq(perm, "edit" as *u8) == 1 { out[0]="editor" as *u8 as i64; out[1]="owner" as *u8 as i64; return 2 }
168 if rb_seq(perm, "manage" as *u8) == 1 { out[0]="owner" as *u8 as i64; return 1 }
169 }
170 // group: view/read/member=member|admin|owner ; post=member|admin|owner ; manage=admin|owner
171 if rb_seq(otype, "group" as *u8) == 1 {
172 if rb_seq(perm, "manage" as *u8) == 1 { out[0]="admin" as *u8 as i64; out[1]="owner" as *u8 as i64; return 2 }
173 out[0]="member" as *u8 as i64; out[1]="admin" as *u8 as i64; out[2]="owner" as *u8 as i64; return 3
174 }
175 // tenant: read/write=member|manager|admin ; admin/manage=admin
176 if rb_seq(otype, "tenant" as *u8) == 1 {
177 if rb_seq(perm, "manage" as *u8) == 1 { out[0]="admin" as *u8 as i64; return 1 }
178 if rb_seq(perm, "admin" as *u8) == 1 { out[0]="admin" as *u8 as i64; return 1 }
179 out[0]="member" as *u8 as i64; out[1]="manager" as *u8 as i64; out[2]="admin" as *u8 as i64; return 3
180 }
181 // project: view=owner|team|contractor ; edit=owner|team ; manage=owner
182 if rb_seq(otype, "project" as *u8) == 1 {
183 if rb_seq(perm, "manage" as *u8) == 1 { out[0]="owner" as *u8 as i64; return 1 }
184 if rb_seq(perm, "edit" as *u8) == 1 { out[0]="owner" as *u8 as i64; out[1]="team" as *u8 as i64; return 2 }
185 out[0]="owner" as *u8 as i64; out[1]="team" as *u8 as i64; out[2]="contractor" as *u8 as i64; return 3
186 }
187 // album (family media): view=viewer|curator|owner ; add=curator|owner ; manage=curator|owner.
188 // ADDED 2026-07-25 by the share-plane retirement (nx_share_convergence_gate C9). The suite needed an
189 // owner to be able to DELEGATE re-sharing -- "my spouse can add people to the family album too" -- and
190 // the doc type deliberately cannot express that (doc manage = owner only). The convergence proof said
191 // the migration's job was to carry that rule across as a rewrite-table ROW rather than as a second
192 // authorization plane, so here it is as data. Purely additive: no existing otype changes behaviour.
193 if rb_seq(otype, "album" as *u8) == 1 {
194 if rb_seq(perm, "manage" as *u8) == 1 { out[0]="curator" as *u8 as i64; out[1]="owner" as *u8 as i64; return 2 }
195 if rb_seq(perm, "add" as *u8) == 1 { out[0]="curator" as *u8 as i64; out[1]="owner" as *u8 as i64; return 2 }
196 out[0]="viewer" as *u8 as i64; out[1]="curator" as *u8 as i64; out[2]="owner" as *u8 as i64; return 3
197 }
198 // user (social): view/friend=friend ; the block relation is checked separately (blocks outrank)
199 if rb_seq(otype, "user" as *u8) == 1 {
200 out[0]="friend" as *u8 as i64; return 1
201 }
202 // default: the perm IS the relation (exact match)
203 out[0]=perm as i64; return 1
204}
205
206// ---- the CHECK: does `sub` hold `perm` on `obj`? bounded userset-rewrite walk, deny-by-default. ----
207func rb_check(prefix: *u8, sub: *u8, perm: *u8, obj: *u8, depth: i64) -> i64 {
208 if depth > RB_MAXDEPTH { return 0 }
209 let ot: *u8 = sys_mmap(64)
210 rb_otype(obj, ot, 64)
211 // BLOCKS OUTRANK: if the object is a user and it blocked the subject, hard-deny (the consent axis, as a tuple).
212 if rb_seq(ot, "user" as *u8) == 1 { if rb_tuple_current(prefix, obj, "blocked" as *u8, sub) == 1 { return 0 } }
213 let rels: *i64 = sys_mmap(8 * 16) as *i64
214 let nr: i64 = rb_expand(ot, perm, rels, 16)
215 var ri: i64 = 0
216 while ri < nr {
217 let rel: *u8 = rels[ri] as *u8
218 // (a) DIRECT: is (obj, rel, sub) current?
219 if rb_tuple_current(prefix, obj, rel, sub) == 1 { return 1 }
220 // (b) USERSET: any current (obj, rel, G#grel) whose G grants `grel` to sub? (recurse, depth-bounded)
221 let segp: *i64 = sys_mmap(16) as *i64
222 let ns: i64 = ss_manifest_dyn(prefix, segp) // uncapped: authz must see the WHOLE store
223 let segs: *i64 = segp[0] as *i64
224 let fo: *u8 = sys_mmap(256); let fr: *u8 = sys_mmap(128); let fus: *u8 = sys_mmap(256)
225 var s: i64 = 0
226 while s < ns {
227 let path: *u8 = sys_mmap(512); var po: i64 = 0
228 po = rb_cat(path, po, prefix); po = rb_cat(path, po, segs[s] as *u8); po = rb_cat(path, po, ".docs" as *u8); path[po] = 0 as u8
229 let szp: *i64 = sys_mmap(16) as *i64; let b: *u8 = ss_readall(path, szp); let sz: i64 = szp[0]
230 var j: i64 = 0
231 while j + 9 <= sz {
232 let kl: i64 = ss_r32(b, j + 1); let koff: i64 = j + 5; let vl: i64 = ss_r32(b, koff + kl); let voff: i64 = koff + kl + 4
233 if b[j] == 1 {
234 if rb_iskind(b, koff, kl, "tup:" as *u8) == 1 {
235 let rec: *u8 = (b as i64 + voff) as *u8
236 fo[0] = 0 as u8; fr[0] = 0 as u8; fus[0] = 0 as u8
237 rb_field(rec, vl, "obj" as *u8, fo, 256)
238 rb_field(rec, vl, "rel" as *u8, fr, 128)
239 rb_field(rec, vl, "sub" as *u8, fus, 256)
240 if rb_seq(fo, obj) == 1 { if rb_seq(fr, rel) == 1 { if rb_has_hash(fus) == 1 {
241 // this userset must be CURRENT (latest-wins on the exact triple), then recurse
242 if rb_tuple_current(prefix, obj, rel, fus) == 1 {
243 let gobj: *u8 = sys_mmap(256); let grel: *u8 = sys_mmap(128)
244 rb_split_userset(fus, gobj, grel)
245 if rb_check(prefix, sub, grel, gobj, depth + 1) == 1 { return 1 }
246 }
247 } } }
248 }
249 }
250 j = voff + vl
251 }
252 s = s + 1
253 }
254 ri = ri + 1
255 }
256 return 0
257}
258
259// HR-composed check: a site OPERATOR (HR level >= RB_OPERATOR) is god WITHIN THEIR REALM (the caller has already
260// validated realm via the session). Everyone else = pure deny-by-default tuples. THIS is the tie to Nishi HR:
261// hr_level comes from ag_uid_to_level / hac_gate on the validated session. Pass hr_level<0 or 0 for "no override".
262func rb_check_hr(prefix: *u8, sub: *u8, perm: *u8, obj: *u8, hr_level: i64) -> i64 {
263 if hr_level >= RB_OPERATOR { return 1 }
264 return rb_check(prefix, sub, perm, obj, 0)
265}
266
267// ---- CROSS-SURFACE (two-store) check: consult a PRIMARY store + a read-only SECONDARY store. This is how a
268// group defined in ONE surface (e.g. relate's knowledge/status/relate_) grants access to an object owned in
269// ANOTHER (e.g. office's officeauthz_): the doc-share userset lives in the primary, the group membership in the
270// secondary, and rb_check2 spans both. Deny-by-default; same bounded userset walk as rb_check. Additive -- the
271// single-store rb_check (relate uses it) is untouched. Tombstones live with their grant (confirm within a store).
272func rb_tuple_current2(p1: *u8, p2: *u8, obj: *u8, rel: *u8, sub: *u8) -> i64 {
273 if rb_tuple_current(p1, obj, rel, sub) == 1 { return 1 }
274 return rb_tuple_current(p2, obj, rel, sub)
275}
276// scan ONE store for current (obj, rel, G#grel) usersets; recurse rb_check2 (2-store) into each. 1 = a grant found.
277func rb_userset_scan(store: *u8, p1: *u8, p2: *u8, sub: *u8, obj: *u8, rel: *u8, depth: i64) -> i64 {
278 let segp: *i64 = sys_mmap(16) as *i64
279 let ns: i64 = ss_manifest_dyn(store, segp) // uncapped: authz must see the WHOLE store
280 let segs: *i64 = segp[0] as *i64
281 let fo: *u8 = sys_mmap(256); let fr: *u8 = sys_mmap(128); let fus: *u8 = sys_mmap(256)
282 var s: i64 = 0
283 while s < ns {
284 let path: *u8 = sys_mmap(512); var po: i64 = 0
285 po = rb_cat(path, po, store); po = rb_cat(path, po, segs[s] as *u8); po = rb_cat(path, po, ".docs" as *u8); path[po] = 0 as u8
286 let szp: *i64 = sys_mmap(16) as *i64; let b: *u8 = ss_readall(path, szp); let sz: i64 = szp[0]
287 var j: i64 = 0
288 while j + 9 <= sz {
289 let kl: i64 = ss_r32(b, j + 1); let koff: i64 = j + 5; let vl: i64 = ss_r32(b, koff + kl); let voff: i64 = koff + kl + 4
290 if b[j] == 1 {
291 if rb_iskind(b, koff, kl, "tup:" as *u8) == 1 {
292 let rec: *u8 = (b as i64 + voff) as *u8
293 fo[0] = 0 as u8; fr[0] = 0 as u8; fus[0] = 0 as u8
294 rb_field(rec, vl, "obj" as *u8, fo, 256)
295 rb_field(rec, vl, "rel" as *u8, fr, 128)
296 rb_field(rec, vl, "sub" as *u8, fus, 256)
297 if rb_seq(fo, obj) == 1 { if rb_seq(fr, rel) == 1 { if rb_has_hash(fus) == 1 {
298 if rb_tuple_current(store, obj, rel, fus) == 1 {
299 let gobj: *u8 = sys_mmap(256); let grel: *u8 = sys_mmap(128)
300 rb_split_userset(fus, gobj, grel)
301 if rb_check2(p1, p2, sub, grel, gobj, depth + 1) == 1 { return 1 }
302 }
303 } } }
304 }
305 }
306 j = voff + vl
307 }
308 s = s + 1
309 }
310 return 0
311}
312func rb_check2(p1: *u8, p2: *u8, sub: *u8, perm: *u8, obj: *u8, depth: i64) -> i64 {
313 if depth > RB_MAXDEPTH { return 0 }
314 let ot: *u8 = sys_mmap(64)
315 rb_otype(obj, ot, 64)
316 if rb_seq(ot, "user" as *u8) == 1 { if rb_tuple_current2(p1, p2, obj, "blocked" as *u8, sub) == 1 { return 0 } }
317 let rels: *i64 = sys_mmap(8 * 16) as *i64
318 let nr: i64 = rb_expand(ot, perm, rels, 16)
319 var ri: i64 = 0
320 while ri < nr {
321 let rel: *u8 = rels[ri] as *u8
322 if rb_tuple_current2(p1, p2, obj, rel, sub) == 1 { return 1 }
323 if rb_userset_scan(p1, p1, p2, sub, obj, rel, depth) == 1 { return 1 }
324 if rb_userset_scan(p2, p1, p2, sub, obj, rel, depth) == 1 { return 1 }
325 ri = ri + 1
326 }
327 return 0
328}
329
330// WRITE AUTHORITY: may `grantor` write tuples on `obj`? = they can MANAGE it (owner/admin), OR the object has NO
331// owner yet (bootstrap: first grantor claims it). Prevents a member self-granting admin (the escalation adversary).
332func rb_obj_has_owner(prefix: *u8, obj: *u8) -> i64 {
333 let segp: *i64 = sys_mmap(16) as *i64
334 let ns: i64 = ss_manifest_dyn(prefix, segp) // uncapped: authz must see the WHOLE store (a truncated walk = wrong last-write-wins)
335 let segs: *i64 = segp[0] as *i64
336 let fo: *u8 = sys_mmap(256); let fr: *u8 = sys_mmap(128); let fs: *u8 = sys_mmap(256); let fc: *u8 = sys_mmap(8)
337 var found: i64 = 0
338 var s: i64 = 0
339 while s < ns {
340 let path: *u8 = sys_mmap(512); var po: i64 = 0
341 po = rb_cat(path, po, prefix); po = rb_cat(path, po, segs[s] as *u8); po = rb_cat(path, po, ".docs" as *u8); path[po] = 0 as u8
342 let szp: *i64 = sys_mmap(16) as *i64; let b: *u8 = ss_readall(path, szp); let sz: i64 = szp[0]
343 var j: i64 = 0
344 while j + 9 <= sz {
345 let kl: i64 = ss_r32(b, j + 1); let koff: i64 = j + 5; let vl: i64 = ss_r32(b, koff + kl); let voff: i64 = koff + kl + 4
346 if b[j] == 1 {
347 if rb_iskind(b, koff, kl, "tup:" as *u8) == 1 {
348 let rec: *u8 = (b as i64 + voff) as *u8
349 fo[0] = 0 as u8; fr[0] = 0 as u8
350 rb_field(rec, vl, "obj" as *u8, fo, 256)
351 rb_field(rec, vl, "rel" as *u8, fr, 128)
352 if rb_seq(fo, obj) == 1 { if rb_seq(fr, "owner" as *u8) == 1 { found = 1 } }
353 }
354 }
355 j = voff + vl
356 }
357 s = s + 1
358 }
359 return found
360}
361func rb_may_grant(prefix: *u8, grantor: *u8, obj: *u8) -> i64 {
362 if rb_obj_has_owner(prefix, obj) == 0 { return 1 } // unclaimed -> bootstrap first grant
363 if rb_check(prefix, grantor, "manage" as *u8, obj, 0) == 1 { return 1 }
364 return 0
365}
366
367// ---- session uid -> registered handle (the tie to Nishi HR identity; generic, any surface uses it) ----
368// hex-encode n bytes of src into dst (lowercase, NUL-term); returns chars written (2n).
369func rb_hex(dst: *u8, src: *u8, n: i64) -> i64 {
370 let hx: *u8 = "0123456789abcdef" as *u8
371 var o: i64 = 0
372 var i: i64 = 0
373 while i < n {
374 let c: i64 = (src[i] as i64) & 0xff
375 dst[o] = hx[(c >> 4) & 15]; dst[o + 1] = hx[c & 15]
376 o = o + 2; i = i + 1
377 }
378 dst[o] = 0 as u8
379 return o
380}
381// uidhex -> handle from the login daemon's index (TAB-separated: field0=uidhex, field1=handle; '#'=comment).
382func rb_idx_lookup(idx: *u8, idxlen: i64, uidhex: *u8, uxn: i64, out_h: *u8, cap: i64) -> i64 {
383 var ls: i64 = 0
384 while ls < idxlen {
385 var le: i64 = ls
386 while le < idxlen { if (idx[le] as i64) == 10 { break } le = le + 1 }
387 if le > ls { if (idx[ls] as i64) != 35 {
388 var tab: i64 = ls
389 while tab < le { if (idx[tab] as i64) == 9 { break } tab = tab + 1 }
390 if tab < le {
391 let f0len: i64 = tab - ls
392 if f0len == uxn {
393 var m: i64 = 1
394 var k: i64 = 0
395 while k < uxn { if idx[ls + k] != uidhex[k] { m = 0; k = uxn } else { k = k + 1 } }
396 if m == 1 {
397 var o: i64 = 0
398 var q: i64 = tab + 1
399 while q < le { if o < cap - 1 { out_h[o] = idx[q]; o = o + 1 } q = q + 1 }
400 out_h[o] = 0 as u8
401 return o
402 }
403 }
404 }
405 } }
406 ls = le + 1
407 }
408 out_h[0] = 0 as u8
409 return 0
410}
411// resolve a raw uid (uidn bytes, from olg_whoami) to its registered handle via the on-disk index. 0 = unmapped.
412func rb_resolve_handle(idxpath: *u8, uid: *u8, uidn: i64, out_h: *u8, cap: i64) -> i64 {
413 let szp: *i64 = sys_mmap(16) as *i64
414 let idx: *u8 = ss_readall(idxpath, szp)
415 if (idx as i64) == 0 { out_h[0] = 0 as u8; return 0 }
416 let uxbuf: *u8 = sys_mmap(128)
417 let uxn: i64 = rb_hex(uxbuf, uid, uidn)
418 return rb_idx_lookup(idx, szp[0], uxbuf, uxn, out_h, cap)
419}
420
421// is string s already among the first n pointers of out[]? (dedup guard for the enumerators below)
422func rb_in_list(out: *i64, n: i64, s: *u8) -> i64 {
423 var i: i64 = 0
424 while i < n { if rb_seq(out[i] as *u8, s) == 1 { return 1 } i = i + 1 }
425 return 0
426}
427// enumerate the DISTINCT CURRENT subjects of (obj, rel) into out[] (fresh mmap'd copies). returns count (<=cap).
428// e.g. rb_list_subjects(p, "user:alice", "friend", ...) = alice's current friends. Presentation-agnostic.
429func rb_list_subjects(prefix: *u8, obj: *u8, rel: *u8, out: *i64, cap: i64) -> i64 {
430 var n: i64 = 0
431 let segp: *i64 = sys_mmap(16) as *i64
432 let ns: i64 = ss_manifest_dyn(prefix, segp) // uncapped: authz must see the WHOLE store
433 let segs: *i64 = segp[0] as *i64
434 let fo: *u8 = sys_mmap(256); let fr: *u8 = sys_mmap(128); let fs: *u8 = sys_mmap(256)
435 var s: i64 = 0
436 while s < ns {
437 let path: *u8 = sys_mmap(512); var po: i64 = 0
438 po = rb_cat(path, po, prefix); po = rb_cat(path, po, segs[s] as *u8); po = rb_cat(path, po, ".docs" as *u8); path[po] = 0 as u8
439 let szp: *i64 = sys_mmap(16) as *i64; let b: *u8 = ss_readall(path, szp); let sz: i64 = szp[0]
440 var j: i64 = 0
441 while j + 9 <= sz {
442 let kl: i64 = ss_r32(b, j + 1); let koff: i64 = j + 5; let vl: i64 = ss_r32(b, koff + kl); let voff: i64 = koff + kl + 4
443 if b[j] == 1 {
444 if rb_iskind(b, koff, kl, "tup:" as *u8) == 1 {
445 let rec: *u8 = (b as i64 + voff) as *u8
446 fo[0] = 0 as u8; fr[0] = 0 as u8; fs[0] = 0 as u8
447 rb_field(rec, vl, "obj" as *u8, fo, 256)
448 rb_field(rec, vl, "rel" as *u8, fr, 128)
449 rb_field(rec, vl, "sub" as *u8, fs, 256)
450 if rb_seq(fo, obj) == 1 { if rb_seq(fr, rel) == 1 {
451 if n < cap { if rb_in_list(out, n, fs) == 0 { if rb_tuple_current(prefix, obj, rel, fs) == 1 {
452 let cp: *u8 = sys_mmap(256); var q: i64 = 0
453 while fs[q] != (0 as u8) { cp[q] = fs[q]; q = q + 1 } cp[q] = 0 as u8
454 out[n] = cp as i64; n = n + 1
455 } } }
456 } }
457 }
458 }
459 j = voff + vl
460 }
461 s = s + 1
462 }
463 return n
464}
465// enumerate the DISTINCT CURRENT objects where (obj, rel, sub) holds, into out[]. returns count (<=cap).
466// e.g. rb_list_objects_for_sub(p, "user:alice", "member", ...) = the groups alice is a member of.
467// HISTORY, in append(time) order: every subject that has EVER held `rel` on `obj`, tombstoned ones included,
468// duplicates kept because a thing can come back to a previous holder. rb_list_subjects answers "who holds
469// this NOW"; this answers "whose hands has this passed through", which is a different question and the one
470// provenance needs. ADDED 2026-07-25 by the share-plane retirement: the append-only log already contained
471// this answer and nothing could read it, so a gift ledger would have had to keep its own second copy of
472// history -- which is how the duplicate plane got built in the first place.
473func rb_list_subjects_history(prefix: *u8, obj: *u8, rel: *u8, out: *i64, cap: i64) -> i64 {
474 var n: i64 = 0
475 let segp: *i64 = sys_mmap(16) as *i64
476 let ns: i64 = ss_manifest_dyn(prefix, segp) // uncapped: a truncated walk is a rewritten history
477 let segs: *i64 = segp[0] as *i64
478 let fo: *u8 = sys_mmap(256); let fr: *u8 = sys_mmap(128); let fs: *u8 = sys_mmap(256); let fc: *u8 = sys_mmap(16)
479 var s: i64 = 0
480 while s < ns {
481 let path: *u8 = sys_mmap(512); var po: i64 = 0
482 po = rb_cat(path, po, prefix); po = rb_cat(path, po, segs[s] as *u8); po = rb_cat(path, po, ".docs" as *u8); path[po] = 0 as u8
483 let szp: *i64 = sys_mmap(16) as *i64; let b: *u8 = ss_readall(path, szp); let sz: i64 = szp[0]
484 var j: i64 = 0
485 while j + 9 <= sz {
486 let kl: i64 = ss_r32(b, j + 1); let koff: i64 = j + 5; let vl: i64 = ss_r32(b, koff + kl); let voff: i64 = koff + kl + 4
487 if b[j] == 1 {
488 if rb_iskind(b, koff, kl, "tup:" as *u8) == 1 {
489 let rec: *u8 = (b as i64 + voff) as *u8
490 fo[0] = 0 as u8; fr[0] = 0 as u8; fs[0] = 0 as u8; fc[0] = 0 as u8
491 rb_field(rec, vl, "obj" as *u8, fo, 256)
492 rb_field(rec, vl, "rel" as *u8, fr, 128)
493 rb_field(rec, vl, "sub" as *u8, fs, 256)
494 rb_field(rec, vl, "cur" as *u8, fc, 16)
495 if rb_seq(fo, obj) == 1 { if rb_seq(fr, rel) == 1 {
496 // only the ESTABLISHING writes form the chain; a tombstone records the end of a
497 // holding, not a new holder, so counting it would double every hop.
498 if fc[0] == (49 as u8) {
499 if n < cap {
500 let cp: *u8 = sys_mmap(256); var q: i64 = 0
501 while fs[q] != (0 as u8) { cp[q] = fs[q]; q = q + 1 } cp[q] = 0 as u8
502 out[n] = cp as i64; n = n + 1
503 }
504 }
505 } }
506 }
507 }
508 j = voff + vl
509 }
510 s = s + 1
511 }
512 return n
513}
514func rb_list_objects_for_sub(prefix: *u8, sub: *u8, rel: *u8, out: *i64, cap: i64) -> i64 {
515 var n: i64 = 0
516 let segp: *i64 = sys_mmap(16) as *i64
517 let ns: i64 = ss_manifest_dyn(prefix, segp) // uncapped: authz must see the WHOLE store
518 let segs: *i64 = segp[0] as *i64
519 let fo: *u8 = sys_mmap(256); let fr: *u8 = sys_mmap(128); let fs: *u8 = sys_mmap(256)
520 var s: i64 = 0
521 while s < ns {
522 let path: *u8 = sys_mmap(512); var po: i64 = 0
523 po = rb_cat(path, po, prefix); po = rb_cat(path, po, segs[s] as *u8); po = rb_cat(path, po, ".docs" as *u8); path[po] = 0 as u8
524 let szp: *i64 = sys_mmap(16) as *i64; let b: *u8 = ss_readall(path, szp); let sz: i64 = szp[0]
525 var j: i64 = 0
526 while j + 9 <= sz {
527 let kl: i64 = ss_r32(b, j + 1); let koff: i64 = j + 5; let vl: i64 = ss_r32(b, koff + kl); let voff: i64 = koff + kl + 4
528 if b[j] == 1 {
529 if rb_iskind(b, koff, kl, "tup:" as *u8) == 1 {
530 let rec: *u8 = (b as i64 + voff) as *u8
531 fo[0] = 0 as u8; fr[0] = 0 as u8; fs[0] = 0 as u8
532 rb_field(rec, vl, "obj" as *u8, fo, 256)
533 rb_field(rec, vl, "rel" as *u8, fr, 128)
534 rb_field(rec, vl, "sub" as *u8, fs, 256)
535 if rb_seq(fs, sub) == 1 { if rb_seq(fr, rel) == 1 {
536 if n < cap { if rb_in_list(out, n, fo) == 0 { if rb_tuple_current(prefix, fo, rel, sub) == 1 {
537 let cp: *u8 = sys_mmap(256); var q: i64 = 0
538 while fo[q] != (0 as u8) { cp[q] = fo[q]; q = q + 1 } cp[q] = 0 as u8
539 out[n] = cp as i64; n = n + 1
540 } } }
541 } }
542 }
543 }
544 j = voff + vl
545 }
546 s = s + 1
547 }
548 return n
549}