code wiki / (root) / nx_rebac.nx

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}