pamin-kit 0.1.0

The framework every Pamin module is built on: the Module trait, operation registration with entitlements, MCP serving over stdio and Streamable HTTP, and a conformance suite.
Documentation
//! Where the platform plugs in: who is calling, what they may use, and what
//! they used. Each seam has an open-source default; the SaaS implements them in
//! `pamin-platform`.

use std::future::Future;
use std::sync::Arc;
use std::time::Duration;

/// Who a call is for and what it works on. It comes only from the caller's
/// identity, never from tool arguments.
#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct Space {
    pub tenant: String,
    pub user: String,
    pub project: String,
}

/// What an operation needs the caller to be entitled to.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Feature {
    /// Part of every edition.
    Core,
    /// A named paid feature.
    Pro(&'static str),
}

/// Resolves a request to the space it works on.
pub trait Identity: Send + Sync + 'static {
    /// The caller's space, or `None` to refuse the call. `bearer` is the
    /// request's bearer token, absent over stdio.
    fn identify(&self, bearer: Option<&str>) -> impl Future<Output = Option<Arc<Space>>> + Send;
}

/// Decides whether a space may use a feature.
pub trait Entitlements: Send + Sync + 'static {
    fn allows(&self, space: &Space, feature: Feature) -> bool;
}

/// Records what each call used.
pub trait Metering: Send + Sync + 'static {
    fn record(&self, space: &Space, usage: Usage);
}

/// One call, as metering sees it.
#[derive(Clone, Copy, Debug)]
pub struct Usage {
    pub operation: &'static str,
    pub feature: Feature,
    pub elapsed: Duration,
    /// Whether the operation succeeded.
    pub ok: bool,
}

/// The open-source identity: one local user working on one project, whatever
/// the request carries.
#[derive(Clone, Debug)]
pub struct Local {
    space: Arc<Space>,
}

impl Local {
    /// The project named by the working directory, canonicalized so that
    /// two paths to one directory are one project.
    pub fn from_working_directory() -> std::io::Result<Self> {
        let directory = std::env::current_dir()?.canonicalize()?;
        Ok(Self::new(directory.to_string_lossy()))
    }

    pub fn new(project: impl Into<String>) -> Self {
        Self {
            space: Arc::new(Space {
                tenant: "local".into(),
                user: "local".into(),
                project: project.into(),
            }),
        }
    }
}

impl Identity for Local {
    async fn identify(&self, _bearer: Option<&str>) -> Option<Arc<Space>> {
        Some(self.space.clone())
    }
}

/// The open-source entitlements: every core feature, no paid one.
#[derive(Clone, Copy, Debug, Default)]
pub struct Open;

impl Entitlements for Open {
    fn allows(&self, _space: &Space, feature: Feature) -> bool {
        feature == Feature::Core
    }
}

/// The open-source metering: records nothing.
#[derive(Clone, Copy, Debug, Default)]
pub struct Unmetered;

impl Metering for Unmetered {
    fn record(&self, _space: &Space, _usage: Usage) {}
}

/// A resource that changed, in the space whose call changed it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Change {
    pub space: Arc<Space>,
    pub uri: String,
}

/// Carries resource changes from the calls that make them to the clients
/// listening for them (`subscriptions/listen`). Used when a call changes a
/// resource and when a listener starts, never on the call path otherwise.
pub trait ChangeFeed: Send + Sync + 'static {
    /// Publishes a change, without waiting on listeners.
    fn publish(&self, change: Change);

    /// Every change from now on, in every space; the kit keeps each
    /// listener to its own space.
    fn subscribe(&self) -> tokio::sync::broadcast::Receiver<Change>;
}

/// The open-source feed: changes reach listeners in this process. Enough for
/// one local server, and for a module routed `sharded_by_project`, whose
/// project's writers and listeners share an instance; a feed across
/// instances (PostgreSQL `LISTEN`, Redis) implements [`ChangeFeed`].
#[derive(Debug)]
pub struct InProcessChanges(tokio::sync::broadcast::Sender<Change>);

/// How many changes a slow listener may fall behind before it skips some.
const BACKLOG: usize = 1024;

impl Default for InProcessChanges {
    fn default() -> Self {
        Self(tokio::sync::broadcast::channel(BACKLOG).0)
    }
}

impl ChangeFeed for InProcessChanges {
    fn publish(&self, change: Change) {
        // Nobody listening is not an error.
        let _ = self.0.send(change);
    }

    fn subscribe(&self) -> tokio::sync::broadcast::Receiver<Change> {
        self.0.subscribe()
    }
}