code wiki / _hdl_build / nx_share_plane.nx
nx_share_plane.nx source
↩ module page · 276 lines · 12250 B
1// ⚠⚠ SUPERSEDED 2026-07-25 -- DO NOT BUILD ON THIS. Use `nx_suite_share.nx`.
2//
3// This organ was a SECOND implementation of a rule the estate already had. `runtime/nx_rebac.nx` is the one
4// authorization plane -- a sovereign Zanzibar/ReBAC tuple model, deny-by-default, additive tombstones with
5// latest-wins -- and its own header says it exists because the ecosystem once carried three incompatible
6// authz philosophies. This file was quietly becoming the fourth.
7//
8// It was retired the way the estate requires: by PROOF, not assertion.
9// * `nx_share_convergence_gate` ran the same nine scenarios through both planes: 7 AGREE, 2 declared
10// divergences, 0 silent ones -- so the grant/revoke/check half was measurably redundant.
11// * `nx_suite_share` now speaks the suite's vocabulary over nx_rebac, and `nx_suite_share_gate` re-aims
12// all fifteen of the attacks below at it: 15/15 GREEN. Nothing this file protected went unprotected.
13// * The two divergences became work rather than losses: claim-before-share is kept as a facade-level
14// tightening, and delegated re-sharing moved onto the shared plane as a rewrite-table ROW (the new
15// `album` object type with its `curator` relation) instead of a capability bit only this file knew about.
16// KEPT, not deleted, because the convergence gate imports it -- the evidence for the retirement has to keep
17// compiling, or the retirement becomes a claim again. That gate is its only remaining consumer.
18//
19// ---- original header, for the record ----
20// nx_share_plane.nx -- THE SHARE PLANE. One identity-keyed answer to "who may see what", so that shared
21// gallery, shared contact directory and digital gifts are three thin surfaces over one rule instead of
22// three copies of it.
23//
24// WHY THIS IS RUNG 1 AND NOT A SHARED-GALLERY PAGE. The ruler's gap_queue put D2 (shared gallery), E3
25// (shared contact info) and F1 (digital gifts) in its top four, all at rank 9. Three of the top four are
26// the SAME primitive wearing different clothes. Building them one page at a time would produce three
27// who-may-see-what implementations that drift apart, and the one that drifts is a privacy bug. So the
28// measurement chose the rung; the composition is just rule 15 applied to what the measurement found.
29//
30// SHAPE: a caller-supplied arena, no syscalls, no main -> the live daemon composes it per request AND a
31// gate imports it standalone, exactly like nx_connect_ui. The log is APPEND-ONLY (rule 13): revoking
32// appends a revocation, it never erases the grant that existed. History is what makes "who could see this
33// last March" answerable, and a sharing plane that cannot answer that is not auditable.
34//
35// THE INVARIANTS (each one is a gate tooth, not a promise in a comment):
36// * DENY BY DEFAULT -- no matching live grant means no.
37// * ONLY THE OWNER GRANTS -- a subject cannot grant themselves, and holding VIEW never lets you pass
38// VIEW to someone else. No transitive escalation without an explicit ADMIN bit.
39// * REVOCATION IS TOTAL AND IMMEDIATE -- after a revoke every capability answers no, on the next call.
40// * SELF-GRANT IS REFUSED -- an owner already has their own resource; letting a self-grant exist would
41// let an owner revoke themselves out of their own album.
42// * A GIFT IS A TRANSFER OF OWNERSHIP, NOT A COPY -- after a gift the giver is no longer the owner. That
43// is the honest difference between giving something and sharing it, and the ledger keeps both names.
44// * BOUNDED AND LOUD -- past capacity it REFUSES. A plane that silently drops the last grant is a plane
45// that silently grants access it should have refused.
46// license_tier: ORIGINAL
47
48// record layout, 8 slots per row
49const SP_KIND: i64 = 0
50const SP_OWNER: i64 = 1
51const SP_SUBJ: i64 = 2
52const SP_CLASS: i64 = 3
53const SP_RID: i64 = 4
54const SP_CAP: i64 = 5
55const SP_STATE: i64 = 6
56const SP_SEQ: i64 = 7
57const SP_WIDTH: i64 = 8
58// arena header
59const SP_H_N: i64 = 0
60const SP_H_CAP: i64 = 1
61const SP_H_SEQ: i64 = 2
62const SP_HDR: i64 = 4
63
64const SP_K_GRANT: i64 = 1
65const SP_K_OWN: i64 = 2
66
67const SP_ST_LIVE: i64 = 1
68const SP_ST_REVOKED: i64 = 0
69
70// capability bits. VIEW is the floor; ADMIN is the only bit that lets a non-owner re-share.
71const SP_VIEW: i64 = 1
72const SP_ADD: i64 = 2
73const SP_ADMIN: i64 = 4
74const SP_CAPMAX: i64 = 7
75
76// resource classes -- the three surfaces the ruler's queue named, plus calendar for the rung after
77const SP_C_ALBUM: i64 = 1
78const SP_C_CONTACT: i64 = 2
79const SP_C_GIFT: i64 = 3
80const SP_C_CALENDAR: i64 = 4
81
82// errors, all negative and all distinct so a caller can render WHY rather than "it did nothing"
83const SP_E_FULL: i64 = -2
84const SP_E_NOTOWNER: i64 = -1
85const SP_E_SELF: i64 = -3
86const SP_E_BADCAP: i64 = -4
87const SP_E_NOOWNER: i64 = -5
88const SP_E_BADARG: i64 = -6
89
90func sp_init(a: *i64, cap: i64) -> i64 {
91 if cap <= 0 { return SP_E_BADARG }
92 a[SP_H_N] = 0
93 a[SP_H_CAP] = cap
94 a[SP_H_SEQ] = 0
95 return 0
96}
97func sp_count(a: *i64) -> i64 { return a[SP_H_N] }
98func sp_slots_needed(cap: i64) -> i64 { return SP_HDR + cap*SP_WIDTH }
99func sp_at(a: *i64, i: i64, field: i64) -> i64 { return a[SP_HDR + i*SP_WIDTH + field] }
100
101func sp_append(a: *i64, kind: i64, owner: i64, subj: i64, rclass: i64, rid: i64, cap: i64, state: i64) -> i64 {
102 let n: i64 = a[SP_H_N]
103 if n >= a[SP_H_CAP] { return SP_E_FULL }
104 let b: i64 = SP_HDR + n*SP_WIDTH
105 a[SP_H_SEQ] = a[SP_H_SEQ] + 1
106 a[b+SP_KIND] = kind
107 a[b+SP_OWNER] = owner
108 a[b+SP_SUBJ] = subj
109 a[b+SP_CLASS] = rclass
110 a[b+SP_RID] = rid
111 a[b+SP_CAP] = cap
112 a[b+SP_STATE] = state
113 a[b+SP_SEQ] = a[SP_H_SEQ]
114 a[SP_H_N] = n + 1
115 return a[SP_H_SEQ]
116}
117
118// current owner of a resource: the LATEST ownership record wins, so a gift chain resolves to its end.
119// 0 means nobody has claimed it on this plane.
120func sp_owner_of(a: *i64, rclass: i64, rid: i64) -> i64 {
121 var found: i64 = 0
122 var i: i64 = 0
123 let n: i64 = a[SP_H_N]
124 while i < n {
125 if sp_at(a,i,SP_KIND)==SP_K_OWN {
126 if sp_at(a,i,SP_CLASS)==rclass {
127 if sp_at(a,i,SP_RID)==rid { found = sp_at(a,i,SP_SUBJ) }
128 }
129 }
130 i = i + 1
131 }
132 return found
133}
134func sp_claim(a: *i64, owner: i64, rclass: i64, rid: i64) -> i64 {
135 if owner<=0 { return SP_E_BADARG }
136 let cur: i64 = sp_owner_of(a, rclass, rid)
137 // claiming is idempotent (rule 10): re-claiming what you already own appends nothing
138 if cur==owner { return 0 }
139 if cur!=0 { return SP_E_NOTOWNER }
140 return sp_append(a, SP_K_OWN, owner, owner, rclass, rid, SP_CAPMAX, SP_ST_LIVE)
141}
142
143// the LIVE grant for (subject, resource): latest record wins, so grant -> revoke -> grant resolves to the
144// last thing that actually happened rather than to whichever row was scanned first.
145func sp_live_cap(a: *i64, subj: i64, rclass: i64, rid: i64) -> i64 {
146 var capbits: i64 = 0
147 var i: i64 = 0
148 let n: i64 = a[SP_H_N]
149 while i < n {
150 if sp_at(a,i,SP_KIND)==SP_K_GRANT {
151 if sp_at(a,i,SP_SUBJ)==subj {
152 if sp_at(a,i,SP_CLASS)==rclass {
153 if sp_at(a,i,SP_RID)==rid {
154 if sp_at(a,i,SP_STATE)==SP_ST_LIVE { capbits = sp_at(a,i,SP_CAP) }
155 else { capbits = 0 }
156 }
157 }
158 }
159 }
160 i = i + 1
161 }
162 return capbits
163}
164
165// May `actor` grant on this resource? Only the owner, or a holder of an explicit ADMIN grant. VIEW never
166// implies the right to hand VIEW onward -- that is the whole difference between reading and re-sharing.
167func sp_may_grant(a: *i64, actor: i64, rclass: i64, rid: i64) -> i64 {
168 let own: i64 = sp_owner_of(a, rclass, rid)
169 if own==0 { return 0 }
170 if own==actor { return 1 }
171 let held: i64 = sp_live_cap(a, actor, rclass, rid)
172 if (held & SP_ADMIN) == SP_ADMIN { return 1 }
173 return 0
174}
175
176func sp_grant(a: *i64, actor: i64, subj: i64, rclass: i64, rid: i64, capbits: i64) -> i64 {
177 if subj<=0 { return SP_E_BADARG }
178 if capbits<=0 { return SP_E_BADCAP }
179 if capbits>SP_CAPMAX { return SP_E_BADCAP }
180 let own: i64 = sp_owner_of(a, rclass, rid)
181 if own==0 { return SP_E_NOOWNER }
182 if subj==own { return SP_E_SELF }
183 if sp_may_grant(a, actor, rclass, rid)==0 { return SP_E_NOTOWNER }
184 return sp_append(a, SP_K_GRANT, own, subj, rclass, rid, capbits, SP_ST_LIVE)
185}
186
187func sp_revoke(a: *i64, actor: i64, subj: i64, rclass: i64, rid: i64) -> i64 {
188 let own: i64 = sp_owner_of(a, rclass, rid)
189 if own==0 { return SP_E_NOOWNER }
190 if sp_may_grant(a, actor, rclass, rid)==0 { return SP_E_NOTOWNER }
191 return sp_append(a, SP_K_GRANT, own, subj, rclass, rid, 0, SP_ST_REVOKED)
192}
193
194// THE READ PATH. Everything above exists so that this one answer is the same everywhere.
195// SUBSET TEST, not a single-bit test. The first version divided by `want` and read the low bit, which is
196// only correct when `want` is one power of two -- a caller asking "may they VIEW *and* ADD" (want=3) would
197// have got an arithmetic accident for an answer. On a permission check an accident is an access-control
198// bug, so this asks the only question that is always right: are ALL the requested bits held?
199func sp_may(a: *i64, actor: i64, rclass: i64, rid: i64, want: i64) -> i64 {
200 if want<=0 { return 0 }
201 if want>SP_CAPMAX { return 0 }
202 let own: i64 = sp_owner_of(a, rclass, rid)
203 if own==0 { return 0 }
204 if own==actor { return 1 }
205 let held: i64 = sp_live_cap(a, actor, rclass, rid)
206 if (held & want) != want { return 0 }
207 return 1
208}
209
210// A GIFT. Ownership moves; the giver stops being the owner. The ledger keeps every hop, so provenance is
211// recoverable ("who gave this to whom, and when") without any record ever being rewritten.
212func sp_gift(a: *i64, giver: i64, receiver: i64, rclass: i64, rid: i64) -> i64 {
213 if receiver<=0 { return SP_E_BADARG }
214 if giver==receiver { return SP_E_SELF }
215 let own: i64 = sp_owner_of(a, rclass, rid)
216 if own==0 { return SP_E_NOOWNER }
217 if own!=giver { return SP_E_NOTOWNER }
218 return sp_append(a, SP_K_OWN, giver, receiver, rclass, rid, SP_CAPMAX, SP_ST_LIVE)
219}
220// how many hands has this passed through? (ownership records for the resource)
221func sp_provenance_len(a: *i64, rclass: i64, rid: i64) -> i64 {
222 var c: i64 = 0
223 var i: i64 = 0
224 let n: i64 = a[SP_H_N]
225 while i < n {
226 if sp_at(a,i,SP_KIND)==SP_K_OWN {
227 if sp_at(a,i,SP_CLASS)==rclass { if sp_at(a,i,SP_RID)==rid { c = c + 1 } }
228 }
229 i = i + 1
230 }
231 return c
232}
233// the nth holder in the chain (0 = first claimant), or 0 if out of range -- this is what a gift card's
234// "given by" line renders from
235func sp_provenance_at(a: *i64, rclass: i64, rid: i64, idx: i64) -> i64 {
236 var c: i64 = 0
237 var i: i64 = 0
238 let n: i64 = a[SP_H_N]
239 while i < n {
240 if sp_at(a,i,SP_KIND)==SP_K_OWN {
241 if sp_at(a,i,SP_CLASS)==rclass {
242 if sp_at(a,i,SP_RID)==rid {
243 if c==idx { return sp_at(a,i,SP_SUBJ) }
244 c = c + 1
245 }
246 }
247 }
248 i = i + 1
249 }
250 return 0
251}
252// everyone who can currently see this resource, excluding the owner. Written for the UI panel that shows a
253// person WHO CAN SEE THIS -- a sharing rule nobody can inspect is one nobody can trust.
254func sp_viewers(a: *i64, rclass: i64, rid: i64, out: *i64, outcap: i64) -> i64 {
255 var k: i64 = 0
256 var i: i64 = 0
257 let n: i64 = a[SP_H_N]
258 while i < n {
259 if sp_at(a,i,SP_KIND)==SP_K_GRANT {
260 if sp_at(a,i,SP_CLASS)==rclass {
261 if sp_at(a,i,SP_RID)==rid {
262 let s: i64 = sp_at(a,i,SP_SUBJ)
263 // recompute from the log rather than trusting this row: the latest record for s wins
264 if sp_live_cap(a, s, rclass, rid) > 0 {
265 var seen: i64 = 0
266 var j: i64 = 0
267 while j < k { if out[j]==s { seen=1 } j=j+1 }
268 if seen==0 { if k<outcap { out[k]=s; k=k+1 } }
269 }
270 }
271 }
272 }
273 i = i + 1
274 }
275 return k
276}