// Session: scoped capability binding for an agent session.
//
// Modeled after OS process address spaces:
// - Each session owns its own Gate, Budget, and AuditTrail.
// - When the session expires, all capabilities are automatically revoked.
// - Multiple sessions cannot interfere with each other.
///|
#derive(Eq, Debug)
pub struct Session {
id : String
gate : Gate
budget : Budget
audit : AuditTrail
created_at : Int64
expires_at : Int64?
}
///|
pub extend Session with Eq::{not_equal, equal}
///|
pub extend Session with @moonbitlang/core/debug.Debug::{to_repr}
///|
/// Create a new Session with the given Gate, Budget, and optional expiry.
/// expires_at is a Unix timestamp in milliseconds; None means no expiry.
pub fn Session::new(
id~ : String,
gate~ : Gate,
budget~ : Budget,
expires_at~ : Int64?,
) -> Session {
{
id,
gate,
budget,
audit: AuditTrail::new(max_size=100),
created_at: 0L,
expires_at,
}
}
///|
/// Gate a single tool call through all layers:
/// 1. Check budget — deny if exhausted.
/// 2. Check Gate rules — deny/allow/ask.
/// 3. Record decision in audit trail.
///
/// Returns (new_session, verdict).
pub fn Session::gate_call(
self : Session,
tool : String,
action : String,
resource : String,
) -> (Session, Verdict) {
// Step 1: budget check.
let (budget2, budget_ok) = self.budget.consume(1L, 1L)
if !budget_ok {
let audit2 = self.audit.record(
tool, action, resource, "deny", "budget exhausted",
)
let s = {
id: self.id,
gate: self.gate,
budget: budget2,
audit: audit2,
created_at: self.created_at,
expires_at: self.expires_at,
}
return (s, Verdict::Deny)
}
// Step 2: Gate rule check.
let verdict = self.gate.judge(tool, action, resource)
// Step 3: audit the decision.
let reason = match verdict {
Verdict::Allow => "matched allow rule"
Verdict::Deny => "default-deny or matched deny rule"
Verdict::AskUser => "matched ask-user rule"
}
let verdict_str = match verdict {
Verdict::Allow => "allow"
Verdict::Deny => "deny"
Verdict::AskUser => "ask_user"
}
let audit2 = self.audit.record(tool, action, resource, verdict_str, reason)
let s = {
id: self.id,
gate: self.gate,
budget: budget2,
audit: audit2,
created_at: self.created_at,
expires_at: self.expires_at,
}
(s, verdict)
}
///|
/// Check whether the session has expired.
pub fn Session::is_expired(self : Session) -> Bool {
match self.expires_at {
None => false
Some(exp) => 0L > exp
}
}
///|
/// Return the session's audit trail (read-only snapshot).
pub fn Session::audit_entries(self : Session) -> Array[AuditEntry] {
self.audit.all()
}
///|
/// Return the session's remaining call budget.
pub fn Session::remaining_budget(self : Session) -> Int64? {
self.budget.remaining_calls()
}
///|
/// Close the session (clear audit, zero budget). The session struct
/// is consumed; callers should drop the returned value or use it
/// for final audit export before discarding.
pub fn Session::close(self : Session) -> Session {
let empty_audit = AuditTrail::new(max_size=0)
let zero_budget = Budget::new(
max_calls=Some(0L),
max_tokens=Some(0L),
window_seconds=None,
)
{
id: self.id,
gate: self.gate,
budget: zero_budget,
audit: empty_audit,
created_at: self.created_at,
expires_at: self.expires_at,
}
}