use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use serde::Serialize;
use sha2::{Digest, Sha256};
use crate::paths::state::StateLayout;
use crate::state::{escalation as escalation_state, pod_identity, session as session_state};
#[derive(Debug, Clone)]
pub(crate) struct PolicySource {
pub(crate) kind: &'static str,
pub(crate) path: PathBuf,
pub(crate) status: &'static str,
pub(crate) content: Option<String>,
}
#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum PolicyDecision {
Allow,
ApprovalRequired,
EscalationRequired,
Deny,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum PolicyActionFamily {
ReadOnly,
LocalExecution,
LocalIsolation,
LocalFinalize,
PublishExternal,
SharedQueueMutation,
DurableMemoryMutation,
RepairDestructive,
ProtectedSurfaceMutation,
}
#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum AutonomousPolicyPosture {
GuardedLocalExecution,
}
#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum InteractiveCompatibilityMode {
HumanSupervised,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct PolicyProjectionView {
pub(crate) status: &'static str,
pub(crate) source_fingerprint: String,
pub(crate) authored_policy: AuthoredPolicyView,
pub(crate) interactive_compatibility_mode: InteractiveCompatibilityMode,
pub(crate) autonomous_default_posture: AutonomousPolicyPosture,
pub(crate) fail_closed: bool,
pub(crate) action_families: Vec<ActionFamilyDecisionView>,
pub(crate) runtime_constraints: PolicyRuntimeConstraintsView,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct AuthoredPolicyView {
pub(crate) status: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) effective_content_sha256: Option<String>,
pub(crate) parts: Vec<PolicySourceView>,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct PolicySourceView {
pub(crate) kind: &'static str,
pub(crate) path: String,
pub(crate) status: &'static str,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct ActionFamilyDecisionView {
pub(crate) family: PolicyActionFamily,
pub(crate) default_decision: PolicyDecision,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct PolicyRuntimeConstraintsView {
pub(crate) blocking_escalation_active: bool,
pub(crate) blocking_escalation_count: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) session_lifecycle: Option<session_state::SessionLifecycle>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) session_owner_kind: Option<session_state::SessionOwnerKind>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) session_mode: Option<session_state::SessionMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) session_stale: Option<bool>,
pub(crate) protected_writes_require_current_owner: bool,
pub(crate) stale_session_blocks_protected_writes: bool,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct PolicyEvaluationView {
pub(crate) decision: PolicyDecision,
pub(crate) reason_code: &'static str,
pub(crate) summary: String,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct PolicySessionContext {
pub(crate) mode: Option<session_state::SessionMode>,
pub(crate) lifecycle: Option<session_state::SessionLifecycle>,
pub(crate) owner_kind: Option<session_state::SessionOwnerKind>,
pub(crate) stale: Option<bool>,
}
const DEFAULT_AUTONOMOUS_POLICY: &[(PolicyActionFamily, PolicyDecision)] = &[
(PolicyActionFamily::ReadOnly, PolicyDecision::Allow),
(PolicyActionFamily::LocalExecution, PolicyDecision::Allow),
(PolicyActionFamily::LocalIsolation, PolicyDecision::Allow),
(
PolicyActionFamily::LocalFinalize,
PolicyDecision::ApprovalRequired,
),
(
PolicyActionFamily::PublishExternal,
PolicyDecision::ApprovalRequired,
),
(
PolicyActionFamily::SharedQueueMutation,
PolicyDecision::ApprovalRequired,
),
(
PolicyActionFamily::DurableMemoryMutation,
PolicyDecision::ApprovalRequired,
),
(PolicyActionFamily::RepairDestructive, PolicyDecision::Deny),
(
PolicyActionFamily::ProtectedSurfaceMutation,
PolicyDecision::ApprovalRequired,
),
];
impl PolicySessionContext {
pub(crate) fn missing() -> Self {
Self {
mode: None,
lifecycle: None,
owner_kind: None,
stale: None,
}
}
pub(crate) fn from_start_view(
mode: Option<session_state::SessionMode>,
lifecycle: &session_state::SessionLifecycleProjection,
) -> Self {
Self {
mode,
lifecycle: lifecycle.lifecycle,
owner_kind: lifecycle.owner_kind,
stale: lifecycle.stale,
}
}
}
pub(crate) fn skipped_view() -> PolicyProjectionView {
PolicyProjectionView {
status: "skipped",
source_fingerprint: String::new(),
authored_policy: AuthoredPolicyView {
status: "skipped",
effective_content_sha256: None,
parts: Vec::new(),
},
interactive_compatibility_mode: InteractiveCompatibilityMode::HumanSupervised,
autonomous_default_posture: AutonomousPolicyPosture::GuardedLocalExecution,
fail_closed: true,
action_families: default_action_families(),
runtime_constraints: PolicyRuntimeConstraintsView {
blocking_escalation_active: false,
blocking_escalation_count: 0,
session_lifecycle: None,
session_owner_kind: None,
session_mode: None,
session_stale: None,
protected_writes_require_current_owner: true,
stale_session_blocks_protected_writes: true,
},
}
}
pub(crate) fn read_policy_sources(
layout: &StateLayout,
locality_id: &str,
active_pod_identity: Option<&pod_identity::LoadedPodIdentity>,
) -> Result<Vec<PolicySource>> {
let mut sources = Vec::new();
if let Some(identity) = active_pod_identity {
sources.push(read_policy_source("pod_policy", &identity.policy_path)?);
}
sources.push(read_policy_source(
"profile_policy",
&layout.profile_policy_path(),
)?);
sources.push(read_policy_source(
"locality_policy",
&layout.locality_policy_path(locality_id)?,
)?);
Ok(sources)
}
pub(crate) fn load_session_context(layout: &StateLayout) -> Result<PolicySessionContext> {
let Some(state) = session_state::load_for_layout(layout)? else {
return Ok(PolicySessionContext::missing());
};
let now = session_state::now_epoch_s()?;
let lifecycle = session_state::lifecycle_projection(&state, now, None, None);
Ok(PolicySessionContext {
mode: Some(state.mode),
lifecycle: lifecycle.lifecycle,
owner_kind: lifecycle.owner_kind,
stale: lifecycle.stale,
})
}
pub(crate) fn build_view(
sources: &[PolicySource],
session: PolicySessionContext,
escalation: &escalation_state::EscalationView,
) -> PolicyProjectionView {
let authored_policy = authored_policy_view(sources);
PolicyProjectionView {
status: "derived",
source_fingerprint: fingerprint_sources(sources),
authored_policy,
interactive_compatibility_mode: InteractiveCompatibilityMode::HumanSupervised,
autonomous_default_posture: AutonomousPolicyPosture::GuardedLocalExecution,
fail_closed: true,
action_families: default_action_families(),
runtime_constraints: PolicyRuntimeConstraintsView {
blocking_escalation_active: escalation.blocking_count > 0,
blocking_escalation_count: escalation.blocking_count,
session_lifecycle: session.lifecycle,
session_owner_kind: session.owner_kind,
session_mode: session.mode,
session_stale: session.stale,
protected_writes_require_current_owner: true,
stale_session_blocks_protected_writes: true,
},
}
}
pub(crate) fn evaluate_action_family(
projection: &PolicyProjectionView,
family: PolicyActionFamily,
) -> PolicyEvaluationView {
let constraints = &projection.runtime_constraints;
if matches!(family, PolicyActionFamily::ReadOnly) {
return PolicyEvaluationView {
decision: PolicyDecision::Allow,
reason_code: "read_only_allowed",
summary: "Read-only inspection stays allowed under the default policy posture."
.to_owned(),
};
}
if constraints.blocking_escalation_active {
return PolicyEvaluationView {
decision: PolicyDecision::EscalationRequired,
reason_code: "blocking_escalation_active",
summary: format!(
"{} blocking escalation(s) are active; resolve them before autonomous {}.",
constraints.blocking_escalation_count,
family.label()
),
};
}
if matches!(
constraints.session_lifecycle,
Some(session_state::SessionLifecycle::Autonomous)
) && matches!(constraints.session_stale, Some(true))
{
return PolicyEvaluationView {
decision: PolicyDecision::EscalationRequired,
reason_code: "stale_autonomous_session",
summary: format!(
"The active autonomous session lease is stale; heartbeat or takeover before {}.",
family.label()
),
};
}
let default_decision = default_decision_for_family(family);
PolicyEvaluationView {
decision: default_decision,
reason_code: default_reason_code(family, default_decision),
summary: default_summary(family, default_decision).to_owned(),
}
}
fn read_policy_source(kind: &'static str, path: &Path) -> Result<PolicySource> {
match fs::read_to_string(path) {
Ok(contents) => Ok(PolicySource {
kind,
path: path.to_path_buf(),
status: "loaded",
content: Some(contents),
}),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(PolicySource {
kind,
path: path.to_path_buf(),
status: "missing",
content: None,
}),
Err(error) => Err(error).with_context(|| format!("failed to read {}", path.display())),
}
}
fn authored_policy_view(sources: &[PolicySource]) -> AuthoredPolicyView {
let content = joined_content(sources);
let status = if content.is_empty() {
if sources.iter().any(|source| source.status == "loaded") {
"empty"
} else {
"missing"
}
} else {
"loaded"
};
AuthoredPolicyView {
status,
effective_content_sha256: (!content.is_empty()).then(|| sha256_string(&content)),
parts: sources
.iter()
.map(|source| PolicySourceView {
kind: source.kind,
path: source.path.display().to_string(),
status: source.status,
})
.collect(),
}
}
fn joined_content(sources: &[PolicySource]) -> String {
sources
.iter()
.filter_map(|source| source.content.as_deref())
.filter(|content| !content.is_empty())
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.join("\n\n")
}
fn fingerprint_sources(sources: &[PolicySource]) -> String {
let mut hasher = Sha256::new();
for source in sources {
hasher.update(source.kind.as_bytes());
hasher.update([0]);
hasher.update(source.path.as_os_str().to_string_lossy().as_bytes());
hasher.update([0]);
hasher.update(source.status.as_bytes());
hasher.update([0]);
if let Some(content) = &source.content {
hasher.update(content.as_bytes());
}
hasher.update([0xff]);
}
format!("{:x}", hasher.finalize())
}
fn default_action_families() -> Vec<ActionFamilyDecisionView> {
DEFAULT_AUTONOMOUS_POLICY
.iter()
.map(|(family, default_decision)| ActionFamilyDecisionView {
family: *family,
default_decision: *default_decision,
})
.collect()
}
fn default_decision_for_family(family: PolicyActionFamily) -> PolicyDecision {
DEFAULT_AUTONOMOUS_POLICY
.iter()
.find(|(candidate, _)| *candidate == family)
.map(|(_, decision)| *decision)
.unwrap_or(PolicyDecision::Deny)
}
fn default_reason_code(family: PolicyActionFamily, decision: PolicyDecision) -> &'static str {
match (family, decision) {
(PolicyActionFamily::ReadOnly, PolicyDecision::Allow) => "read_only_allowed",
(PolicyActionFamily::LocalExecution, PolicyDecision::Allow) => "local_execution_allowed",
(PolicyActionFamily::LocalIsolation, PolicyDecision::Allow) => "local_isolation_allowed",
(PolicyActionFamily::LocalFinalize, PolicyDecision::ApprovalRequired) => {
"local_finalize_requires_approval"
}
(PolicyActionFamily::PublishExternal, PolicyDecision::ApprovalRequired) => {
"publish_requires_approval"
}
(PolicyActionFamily::SharedQueueMutation, PolicyDecision::ApprovalRequired) => {
"shared_queue_requires_approval"
}
(PolicyActionFamily::DurableMemoryMutation, PolicyDecision::ApprovalRequired) => {
"durable_memory_requires_approval"
}
(PolicyActionFamily::RepairDestructive, PolicyDecision::Deny) => {
"repair_destructive_denied"
}
(PolicyActionFamily::ProtectedSurfaceMutation, PolicyDecision::ApprovalRequired) => {
"protected_surface_requires_approval"
}
(_, PolicyDecision::Deny) => "action_denied",
(_, PolicyDecision::ApprovalRequired) => "approval_required",
(_, PolicyDecision::EscalationRequired) => "escalation_required",
(_, PolicyDecision::Allow) => "allowed",
}
}
fn default_summary(family: PolicyActionFamily, decision: PolicyDecision) -> &'static str {
match (family, decision) {
(PolicyActionFamily::ReadOnly, PolicyDecision::Allow) => {
"Read-only inspection stays allowed."
}
(PolicyActionFamily::LocalExecution, PolicyDecision::Allow) => {
"In-scope local execution is allowed under the default guarded posture."
}
(PolicyActionFamily::LocalIsolation, PolicyDecision::Allow) => {
"Local isolation work such as branch or workspace setup is allowed by default."
}
(PolicyActionFamily::LocalFinalize, PolicyDecision::ApprovalRequired) => {
"Local finalization still needs explicit approval by default."
}
(PolicyActionFamily::PublishExternal, PolicyDecision::ApprovalRequired) => {
"External publication stays behind explicit approval by default."
}
(PolicyActionFamily::SharedQueueMutation, PolicyDecision::ApprovalRequired) => {
"Shared queue mutations need explicit approval by default."
}
(PolicyActionFamily::DurableMemoryMutation, PolicyDecision::ApprovalRequired) => {
"Durable memory mutation needs explicit approval by default."
}
(PolicyActionFamily::RepairDestructive, PolicyDecision::Deny) => {
"Repair and destructive actions are denied in the default autonomous posture."
}
(PolicyActionFamily::ProtectedSurfaceMutation, PolicyDecision::ApprovalRequired) => {
"Protected-surface mutation never auto-runs under the default posture."
}
(_, PolicyDecision::Deny) => "This action family is denied.",
(_, PolicyDecision::ApprovalRequired) => "This action family needs explicit approval.",
(_, PolicyDecision::EscalationRequired) => {
"This action family is blocked pending escalation."
}
(_, PolicyDecision::Allow) => "This action family is allowed.",
}
}
fn sha256_string(content: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(content.as_bytes());
format!("{:x}", hasher.finalize())
}
impl PolicyActionFamily {
pub(crate) fn label(self) -> &'static str {
match self {
Self::ReadOnly => "read-only inspection",
Self::LocalExecution => "local execution",
Self::LocalIsolation => "local isolation work",
Self::LocalFinalize => "local finalization",
Self::PublishExternal => "external publication",
Self::SharedQueueMutation => "shared queue mutation",
Self::DurableMemoryMutation => "durable memory mutation",
Self::RepairDestructive => "repair or destructive work",
Self::ProtectedSurfaceMutation => "protected-surface mutation",
}
}
}