nx_trace_consent.nx source
↩ module page · 207 lines · 7284 B
1// nx_trace_consent.nx -- per-call cross-device sharing consent gate.
2//
3// Per [[feedback-end-to-end-bit-traceability-architecture]] CARDINAL:
4// "Local trace per call in ~/.nishi/trace/<call_id>.jsonl default-off
5// cross-device; federated via Merkle-root + branch fetch with per-call
6// mutual consent."
7//
8// THE 5TH AND FINAL bit-traceability primitive. Without this, traces
9// stay local (default-off) and cannot be shared across devices. With
10// it, traces can be shared but ONLY with explicit per-call consent
11// from BOTH parties (sharer + recipient).
12//
13// Per [[feedback-privacy-by-default-no-tracking]]: consent is the
14// substrate's default-deny posture for cross-device data movement.
15// No automatic syncing.
16//
17// V1 ships:
18// - sealed enum NxConsentDecision (PENDING/GRANTED/DENIED/EXPIRED/REVOKED)
19// - struct NxConsentRequest with call_id + sharer + recipient + scope + ttl
20// - struct NxConsentLedger of all decisions for audit
21// - propose / accept / decline / revoke verbs
22// - check predicate -- "is sharing call_id X to peer Y currently consented?"
23
24import "nx_syscalls.nx"
25import "nx_tier.nx"
26
27// ===== Sealed enum: NxConsentDecision =============================
28
29const NX_CC_PENDING: nx_int = 0
30const NX_CC_GRANTED: nx_int = 1
31const NX_CC_DENIED: nx_int = 2
32const NX_CC_EXPIRED: nx_int = 3
33const NX_CC_REVOKED: nx_int = 4
34const NX_CC_N_STATES: nx_int = 5
35
36// ===== Sealed enum: NxConsentScope ================================
37
38const NX_CS_FULL_TRACE: nx_int = 0 // share the whole chain
39const NX_CS_MERKLE_ROOT_ONLY: nx_int = 1 // share only the root hash
40const NX_CS_BRANCH_FETCH: nx_int = 2 // recipient may pull specific links
41const NX_CS_N_SCOPES: nx_int = 3
42
43const NX_TC_OK: nx_int = 0
44const NX_TC_ERR_BAD_DECISION: nx_int = 1
45const NX_TC_ERR_BAD_SCOPE: nx_int = 2
46const NX_TC_ERR_NOT_PENDING: nx_int = 3
47const NX_TC_ERR_NOT_FOUND: nx_int = 4
48
49// ===== Struct: NxConsentRequest ====================================
50
51struct NxConsentRequest {
52 request_id: nx_int,
53 call_id: nx_int,
54 sharer_peer: nx_int,
55 recipient_peer: nx_int,
56 scope: nx_int,
57 decision: nx_int,
58 proposed_us: nx_size,
59 decided_us: nx_size,
60 expires_us: nx_size,
61}
62
63struct NxConsentLedger {
64 requests: *NxConsentRequest,
65 capacity: nx_size,
66 count: nx_size,
67}
68
69const NX_TC_REQ_BYTES: nx_size = 72
70
71func nx_cc_decision_is_valid(d: nx_int) -> nx_int {
72 if d < 0 { return 0 }
73 if d >= NX_CC_N_STATES { return 0 }
74 return 1
75}
76
77func nx_cs_scope_is_valid(s: nx_int) -> nx_int {
78 if s < 0 { return 0 }
79 if s >= NX_CS_N_SCOPES { return 0 }
80 return 1
81}
82
83func nx_trace_consent_new(capacity: nx_size) -> *NxConsentLedger {
84 let l: *NxConsentLedger = (sys_mmap(24)) as *NxConsentLedger
85 let bytes: nx_size = capacity * NX_TC_REQ_BYTES
86 l.requests = (sys_mmap(bytes)) as *NxConsentRequest
87 l.capacity = capacity
88 l.count = 0
89 return l
90}
91
92func _tc_at(l: *NxConsentLedger, idx: nx_size) -> *NxConsentRequest {
93 return (l.requests as i64 + (idx as i64) * NX_TC_REQ_BYTES) as *NxConsentRequest
94}
95
96func _tc_find(l: *NxConsentLedger, request_id: nx_int) -> nx_int {
97 var i: nx_size = 0
98 while i < l.count {
99 let r: *NxConsentRequest = _tc_at(l, i)
100 if r.request_id == request_id { return i as i64 }
101 i = i + 1
102 }
103 return -1
104}
105
106// ===== nx_trace_consent_propose ====================================
107//
108// Sharer proposes a share request. Default decision = PENDING.
109// Recipient must accept or decline before the share can happen.
110
111func nx_trace_consent_propose(l: *NxConsentLedger,
112 request_id: nx_int,
113 call_id: nx_int,
114 sharer_peer: nx_int,
115 recipient_peer: nx_int,
116 scope: nx_int,
117 now_us: nx_size,
118 ttl_us: nx_size) -> nx_int {
119 if nx_cs_scope_is_valid(scope) == 0 { return NX_TC_ERR_BAD_SCOPE }
120 if l.count >= l.capacity { return NX_TC_ERR_NOT_FOUND }
121 let r: *NxConsentRequest = _tc_at(l, l.count)
122 r.request_id = request_id
123 r.call_id = call_id
124 r.sharer_peer = sharer_peer
125 r.recipient_peer = recipient_peer
126 r.scope = scope
127 r.decision = NX_CC_PENDING
128 r.proposed_us = now_us
129 r.decided_us = 0
130 r.expires_us = now_us + ttl_us
131 l.count = l.count + 1
132 return NX_TC_OK
133}
134
135// ===== nx_trace_consent_decide =====================================
136//
137// Recipient (or sharer revoking) decides. Decision can be GRANTED,
138// DENIED, or REVOKED. Must be PENDING to transition; once decided,
139// further calls are refused.
140
141func nx_trace_consent_decide(l: *NxConsentLedger,
142 request_id: nx_int,
143 decision: nx_int,
144 now_us: nx_size) -> nx_int {
145 if nx_cc_decision_is_valid(decision) == 0 { return NX_TC_ERR_BAD_DECISION }
146 if decision == NX_CC_PENDING { return NX_TC_ERR_BAD_DECISION }
147 if decision == NX_CC_EXPIRED { return NX_TC_ERR_BAD_DECISION }
148 let idx: nx_int = _tc_find(l, request_id)
149 if idx < 0 { return NX_TC_ERR_NOT_FOUND }
150 let r: *NxConsentRequest = _tc_at(l, idx as nx_size)
151 if decision == NX_CC_REVOKED {
152 if r.decision != NX_CC_GRANTED { return NX_TC_ERR_NOT_PENDING }
153 } else {
154 if r.decision != NX_CC_PENDING { return NX_TC_ERR_NOT_PENDING }
155 }
156 r.decision = decision
157 r.decided_us = now_us
158 return NX_TC_OK
159}
160
161// ===== nx_trace_consent_check =====================================
162//
163// Predicate: is sharing call_id X from sharer to recipient currently
164// permitted? Returns NX_CC_GRANTED if so. Walks the ledger; auto-
165// updates EXPIRED state if now_us > expires_us.
166
167func nx_trace_consent_check(l: *NxConsentLedger,
168 call_id: nx_int,
169 sharer_peer: nx_int,
170 recipient_peer: nx_int,
171 now_us: nx_size) -> nx_int {
172 var i: nx_size = 0
173 while i < l.count {
174 let r: *NxConsentRequest = _tc_at(l, i)
175 if r.call_id == call_id {
176 if r.sharer_peer == sharer_peer {
177 if r.recipient_peer == recipient_peer {
178 if r.decision == NX_CC_GRANTED {
179 if now_us > r.expires_us {
180 r.decision = NX_CC_EXPIRED
181 return NX_CC_EXPIRED
182 }
183 return NX_CC_GRANTED
184 }
185 return r.decision
186 }
187 }
188 }
189 i = i + 1
190 }
191 return NX_CC_DENIED // no matching request = default-deny
192}
193
194func nx_trace_consent_count(l: *NxConsentLedger) -> nx_size {
195 return l.count
196}
197
198func nx_trace_consent_count_by_decision(l: *NxConsentLedger, decision: nx_int) -> nx_int {
199 var hits: nx_int = 0
200 var i: nx_size = 0
201 while i < l.count {
202 let r: *NxConsentRequest = _tc_at(l, i)
203 if r.decision == decision { hits = hits + 1 }
204 i = i + 1
205 }
206 return hits
207}