1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
//! v2.69.0 — **`lease` over a vendor: the CT-2 Anchor Breach fires on a tool
//! call, not just a store op.**
//!
//! v2.67.0 gave `lease` its first use-site: a store operation is a *use* of the
//! resource, so a post-expiry store op is the breach. v2.69.0 made a `tool` name
//! a resource too — so a **tool call** is also a use, and a post-expiry vendor call
//! must breach the same way.
//!
//! This is the shared lease guard, keyed by **resource** (a lease is over a
//! resource, not over the thing that uses it). `StoreRegistry` holds its own guard
//! for store-held leases (v2.67.0, unchanged); this one is held on `ServerState` for
//! tool-held leases.
//!
//! # Why one guard never charges another's lease
//!
//! `axon-T945` (extended in v2.69.0 to count tool holders) guarantees a leased —
//! i.e. `affine` or `linear` — resource has **exactly one holder**: a store XOR a
//! tool, never both. So a given leased resource is charged by exactly one path. The
//! two guards are disjoint by construction, not by luck.
use std::collections::HashMap;
use std::sync::Mutex;
use crate::ir_nodes::{IRLease, IRResource};
use crate::runtime::lease_kernel::{Clock, LeaseKernel, LeaseToken, UseOutcome};
/// A CT-2 Anchor Breach: a resource was used after its lease expired.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LeaseBreach {
pub resource: String,
pub lease: String,
pub detail: String,
}
impl std::fmt::Display for LeaseBreach {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"CT-2 ANCHOR BREACH — resource '{}' was used, but `lease {}` over it is no longer \
held: {}. A lease is a τ-decaying affine capability: using the resource after \
expiry is the breach.",
self.resource, self.lease, self.detail
)
}
}
/// Why a lease could not be acquired at build.
#[derive(Debug, Clone)]
pub struct LeaseAcquireError {
pub lease: String,
pub resource: String,
pub detail: String,
}
/// The leases held over a program's resources, keyed by resource name.
pub struct ResourceLeaseGuard {
kernel: Mutex<LeaseKernel>,
tokens: Mutex<HashMap<String, LeaseToken>>,
}
impl std::fmt::Debug for ResourceLeaseGuard {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Never dump token internals; report only which resources are governed.
let held: Vec<String> = self
.tokens
.lock()
.map(|t| t.keys().cloned().collect())
.unwrap_or_default();
f.debug_struct("ResourceLeaseGuard").field("resources", &held).finish()
}
}
impl ResourceLeaseGuard {
/// Acquire the program's leases. `acquire: on_start` takes the token now, so it
/// exists BEFORE any use — which is what makes a post-expiry use a detectable
/// event. Returns `Ok(None)` when the program declares no leases (costs
/// nothing).
pub fn from_ir(
leases: &[IRLease],
resources: &[IRResource],
) -> Result<Option<Self>, LeaseAcquireError> {
Self::from_ir_with_clock(leases, resources, Box::new(chrono::Utc::now))
}
/// As [`Self::from_ir`], with an injectable clock (τ-decay is only testable if
/// time can be moved — a gate that slept an hour would never run).
pub fn from_ir_with_clock(
leases: &[IRLease],
resources: &[IRResource],
clock: Clock,
) -> Result<Option<Self>, LeaseAcquireError> {
if leases.is_empty() {
return Ok(None);
}
let mut kernel = LeaseKernel::with_clock(clock);
let mut tokens = HashMap::new();
for l in leases {
let Some(res) = resources.iter().find(|r| r.name == l.resource_ref) else {
return Err(LeaseAcquireError {
lease: l.name.clone(),
resource: l.resource_ref.clone(),
detail: "the lease names a resource the program does not declare".into(),
});
};
let token = kernel.acquire(l, res).map_err(|e| LeaseAcquireError {
lease: l.name.clone(),
resource: l.resource_ref.clone(),
detail: e.message.clone(),
})?;
tokens.insert(l.resource_ref.clone(), token);
}
Ok(Some(ResourceLeaseGuard {
kernel: Mutex::new(kernel),
tokens: Mutex::new(tokens),
}))
}
/// Charge a **use** of `resource_name` against its lease.
///
/// `Ok(())` ⇒ no lease governs this resource, or the lease is live (or was
/// renewed under `on_expire: extend`). `Err` ⇒ the CT-2 Anchor Breach — the
/// use must not proceed.
pub fn charge(&self, resource_name: &str) -> Result<(), LeaseBreach> {
let token = {
let tokens = self.tokens.lock().unwrap_or_else(|p| p.into_inner());
match tokens.get(resource_name) {
Some(t) => t.clone(),
None => return Ok(()), // no lease over this resource
}
};
let mut kernel = self.kernel.lock().unwrap_or_else(|p| p.into_inner());
match kernel.use_token(&token) {
Ok(UseOutcome::Valid(_)) => Ok(()),
Ok(UseOutcome::Extended(renewed)) => {
// `on_expire: extend` — the window rolls forward. Record the new
// token, or the next use would present a revoked one.
self.tokens
.lock()
.unwrap_or_else(|p| p.into_inner())
.insert(resource_name.to_string(), renewed);
Ok(())
}
Ok(UseOutcome::Released) => {
self.tokens
.lock()
.unwrap_or_else(|p| p.into_inner())
.remove(resource_name);
Err(LeaseBreach {
resource: resource_name.to_string(),
lease: token.lease_name.clone(),
detail: "lease released at expiry (on_expire: release) — the capability is no \
longer held"
.into(),
})
}
Err(e) => Err(LeaseBreach {
resource: resource_name.to_string(),
lease: token.lease_name.clone(),
detail: e.message,
}),
}
}
}