code wiki / (root) / nx_trace_consent.nx

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}