feat(grid-agent): add portable control plane (#126)
This commit is contained in:
@@ -16,6 +16,7 @@ use std::collections::{BTreeMap, BTreeSet, VecDeque};
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
const MAX_POLICY_TOOLS: usize = 64;
|
||||
const MAX_AUTHORIZED_AVATARS: usize = 1_024;
|
||||
@@ -238,6 +239,16 @@ pub struct ResourceCost {
|
||||
pub build_prims: u64,
|
||||
}
|
||||
|
||||
/// Secret-free operator projection of one pending approval.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct PendingApprovalMetadata {
|
||||
pub id: ApprovalId,
|
||||
pub principal: String,
|
||||
pub tool: String,
|
||||
pub expires_at: u64,
|
||||
pub cost: ResourceCost,
|
||||
}
|
||||
|
||||
impl ResourceCost {
|
||||
#[must_use]
|
||||
pub const fn one_call() -> Self {
|
||||
@@ -514,6 +525,7 @@ pub enum PolicyFinalOutcome {
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct PolicyAuditRecord {
|
||||
pub recorded_unix_millis: u64,
|
||||
pub authorization_id: Option<u64>,
|
||||
pub approval_id: Option<ApprovalId>,
|
||||
pub origin: OriginClass,
|
||||
@@ -581,6 +593,18 @@ impl PolicyAuditSink for MemoryPolicyAudit {
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
pub struct ApprovalId(u64);
|
||||
|
||||
impl ApprovalId {
|
||||
#[must_use]
|
||||
pub const fn from_raw(value: u64) -> Option<Self> {
|
||||
if value == 0 { None } else { Some(Self(value)) }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn get(self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
pub struct SchedulerGrantId(u64);
|
||||
|
||||
@@ -588,6 +612,7 @@ pub struct SchedulerGrantId(u64);
|
||||
enum ApprovalStatus {
|
||||
Pending,
|
||||
Granted,
|
||||
Denied,
|
||||
Consumed,
|
||||
}
|
||||
|
||||
@@ -820,6 +845,31 @@ impl PolicyGateway {
|
||||
PolicySnapshot(lock(&self.state).clone())
|
||||
}
|
||||
|
||||
/// Returns bounded, content-free approval metadata for operator UIs.
|
||||
#[must_use]
|
||||
pub fn pending_approvals(&self, now: u64) -> Vec<PendingApprovalMetadata> {
|
||||
lock(&self.state)
|
||||
.approvals
|
||||
.values()
|
||||
.filter(|approval| {
|
||||
approval.status == ApprovalStatus::Pending && approval.expires_at > now
|
||||
})
|
||||
.map(|approval| PendingApprovalMetadata {
|
||||
id: approval.id,
|
||||
principal: approval.principal.audit_label(),
|
||||
tool: approval.tool.clone(),
|
||||
expires_at: approval.expires_at,
|
||||
cost: approval.cost,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Current global budget use for a redacted control-plane projection.
|
||||
#[must_use]
|
||||
pub fn global_budget_usage(&self) -> ResourceCost {
|
||||
lock(&self.state).global_used
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn pending_approval_count(&self) -> usize {
|
||||
lock(&self.state)
|
||||
@@ -1064,7 +1114,9 @@ impl PolicyGateway {
|
||||
match approval.status {
|
||||
ApprovalStatus::Pending => Some(PolicyReasonCode::ApprovalNotGranted),
|
||||
ApprovalStatus::Granted => None,
|
||||
ApprovalStatus::Consumed => Some(PolicyReasonCode::ApprovalReplayed),
|
||||
ApprovalStatus::Denied | ApprovalStatus::Consumed => {
|
||||
Some(PolicyReasonCode::ApprovalReplayed)
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Some(reason) = reason {
|
||||
@@ -1121,8 +1173,12 @@ impl PolicyGateway {
|
||||
}
|
||||
if state.approvals.len() == MAX_APPROVALS {
|
||||
let removable = state.approvals.iter().find_map(|(id, approval)| {
|
||||
(approval.expires_at <= now || approval.status == ApprovalStatus::Consumed)
|
||||
.then_some(*id)
|
||||
(approval.expires_at <= now
|
||||
|| matches!(
|
||||
approval.status,
|
||||
ApprovalStatus::Denied | ApprovalStatus::Consumed
|
||||
))
|
||||
.then_some(*id)
|
||||
});
|
||||
let Some(removable) = removable else {
|
||||
drop(state);
|
||||
@@ -1323,6 +1379,40 @@ impl PolicyGateway {
|
||||
Ok(PolicyReasonCode::Allowed)
|
||||
}
|
||||
|
||||
pub fn deny_approval(
|
||||
&self,
|
||||
id: ApprovalId,
|
||||
_operator: &AuthenticatedPrincipal,
|
||||
now: u64,
|
||||
) -> Result<PolicyReasonCode, PolicyError> {
|
||||
let mut state = lock(&self.state);
|
||||
let Some(approval) = state.approvals.get_mut(&id) else {
|
||||
return Ok(PolicyReasonCode::ApprovalUnknown);
|
||||
};
|
||||
if approval.expires_at <= now {
|
||||
return Ok(PolicyReasonCode::ApprovalExpired);
|
||||
}
|
||||
if approval.status != ApprovalStatus::Pending {
|
||||
return Ok(PolicyReasonCode::ApprovalReplayed);
|
||||
}
|
||||
let audit = audit_record(
|
||||
&approval.context,
|
||||
approval.origin,
|
||||
&approval.principal,
|
||||
BoundedText::new("policy.tool", approval.tool.clone())?,
|
||||
None,
|
||||
Some(id),
|
||||
PolicyDisposition::Denied,
|
||||
PolicyReasonCode::ApprovalNotGranted,
|
||||
approval.cost,
|
||||
approval.arguments_hash,
|
||||
PolicyFinalOutcome::Denied,
|
||||
)?;
|
||||
self.emit(audit)?;
|
||||
approval.status = ApprovalStatus::Denied;
|
||||
Ok(PolicyReasonCode::ApprovalNotGranted)
|
||||
}
|
||||
|
||||
/// Consumes the originating authorization so it cannot be replayed to
|
||||
/// create more than one scheduler grant.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
@@ -1621,6 +1711,11 @@ fn audit_record(
|
||||
final_outcome: PolicyFinalOutcome,
|
||||
) -> Result<PolicyAuditRecord, PolicyError> {
|
||||
Ok(PolicyAuditRecord {
|
||||
recorded_unix_millis: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_or(0, |duration| {
|
||||
u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
|
||||
}),
|
||||
authorization_id,
|
||||
approval_id,
|
||||
origin,
|
||||
|
||||
Reference in New Issue
Block a user