use std::collections::BTreeMap;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::fs::VirtualFileSystem;
#[derive(Default)]
pub struct AgentOsConfig {
pub software: Vec<SoftwareInput>,
pub loopback_exempt_ports: Vec<u16>,
pub allowed_node_builtins: Option<Vec<String>>,
pub module_access_cwd: Option<String>,
pub root_filesystem: RootFilesystemConfig,
pub mounts: Vec<MountConfig>,
pub additional_instructions: Option<String>,
pub schedule_driver: Option<Arc<dyn ScheduleDriver>>,
pub tool_kits: Vec<ToolKit>,
pub permissions: Option<Permissions>,
pub sidecar: Option<AgentOsSidecarConfig>,
}
#[derive(Default)]
pub struct AgentOsConfigBuilder {
config: AgentOsConfig,
}
impl AgentOsConfigBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn software(mut self, software: Vec<SoftwareInput>) -> Self {
self.config.software = software;
self
}
pub fn loopback_exempt_ports(mut self, ports: Vec<u16>) -> Self {
self.config.loopback_exempt_ports = ports;
self
}
pub fn allowed_node_builtins(mut self, builtins: Vec<String>) -> Self {
self.config.allowed_node_builtins = Some(builtins);
self
}
pub fn module_access_cwd(mut self, cwd: impl Into<String>) -> Self {
self.config.module_access_cwd = Some(cwd.into());
self
}
pub fn root_filesystem(mut self, root: RootFilesystemConfig) -> Self {
self.config.root_filesystem = root;
self
}
pub fn mounts(mut self, mounts: Vec<MountConfig>) -> Self {
self.config.mounts = mounts;
self
}
pub fn additional_instructions(mut self, instructions: impl Into<String>) -> Self {
self.config.additional_instructions = Some(instructions.into());
self
}
pub fn schedule_driver(mut self, driver: Arc<dyn ScheduleDriver>) -> Self {
self.config.schedule_driver = Some(driver);
self
}
pub fn tool_kits(mut self, tool_kits: Vec<ToolKit>) -> Self {
self.config.tool_kits = tool_kits;
self
}
pub fn permissions(mut self, permissions: Permissions) -> Self {
self.config.permissions = Some(permissions);
self
}
pub fn sidecar(mut self, sidecar: AgentOsSidecarConfig) -> Self {
self.config.sidecar = Some(sidecar);
self
}
pub fn build(self) -> AgentOsConfig {
self.config
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum SoftwareKind {
#[default]
WasmCommands,
Agent,
Tool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SoftwareInput {
pub package: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(default)]
pub kind: SoftwareKind,
}
pub type ToolCallback = Arc<
dyn Fn(serde_json::Value) -> futures::future::BoxFuture<'static, Result<serde_json::Value, String>>
+ Send
+ Sync,
>;
#[derive(Clone)]
pub struct HostTool {
pub name: String,
pub description: String,
pub input_schema: serde_json::Value,
pub timeout_ms: Option<u64>,
pub execute: ToolCallback,
}
#[derive(Clone)]
pub struct ToolKit {
pub name: String,
pub description: String,
pub tools: Vec<HostTool>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Permissions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fs: Option<FsPermissions>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub network: Option<PatternPermissions>,
#[serde(default, rename = "childProcess", skip_serializing_if = "Option::is_none")]
pub child_process: Option<PatternPermissions>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub process: Option<PatternPermissions>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub env: Option<PatternPermissions>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool: Option<PatternPermissions>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PermissionMode {
Allow,
Deny,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum FsPermissions {
Mode(PermissionMode),
Rules(RulePermissions<FsPermissionRule>),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PatternPermissions {
Mode(PermissionMode),
Rules(RulePermissions<PatternPermissionRule>),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RulePermissions<T> {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default: Option<PermissionMode>,
pub rules: Vec<T>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FsPermissionRule {
pub mode: PermissionMode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub operations: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub paths: Option<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PatternPermissionRule {
pub mode: PermissionMode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub operations: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub patterns: Option<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RootFilesystemConfig {
#[serde(default, rename = "type")]
pub kind: RootFilesystemKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mode: Option<RootFilesystemMode>,
#[serde(default, rename = "disableDefaultBaseLayer")]
pub disable_default_base_layer: bool,
#[serde(default)]
pub lowers: Vec<RootLowerInput>,
}
impl Default for RootFilesystemConfig {
fn default() -> Self {
Self {
kind: RootFilesystemKind::Overlay,
mode: None,
disable_default_base_layer: false,
lowers: Vec::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RootFilesystemKind {
#[default]
Overlay,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum RootFilesystemMode {
Ephemeral,
ReadOnly,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum RootLowerInput {
BundledBaseFilesystem,
#[serde(untagged)]
SnapshotExport(crate::fs::RootSnapshotExport),
}
pub enum MountConfig {
Plain {
path: String,
driver: Arc<dyn VirtualFileSystem>,
read_only: bool,
},
Native {
path: String,
plugin: MountPlugin,
read_only: bool,
},
Overlay {
path: String,
filesystem: OverlayMountConfig,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MountPlugin {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OverlayMountConfig {
#[serde(rename = "type")]
pub kind: String,
pub store: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mode: Option<RootFilesystemMode>,
pub lowers: Vec<RootLowerInput>,
}
pub enum AgentOsSidecarConfig {
Shared { pool: Option<String> },
Explicit {
handle: Arc<crate::sidecar::AgentOsSidecar>,
},
}
pub type ScheduleCallback =
Arc<dyn Fn() -> futures::future::BoxFuture<'static, ()> + Send + Sync>;
#[derive(Clone)]
pub struct ScheduleEntry {
pub id: String,
pub schedule: String,
pub callback: ScheduleCallback,
}
pub trait ScheduleDriver: Send + Sync {
fn schedule(&self, entry: ScheduleEntry) -> ScheduleHandle;
fn cancel(&self, handle: &ScheduleHandle);
fn dispose(&self);
}
#[derive(Clone)]
pub struct ScheduleHandle {
pub id: String,
}
#[derive(Default)]
pub struct TimerScheduleDriver {
timers: Arc<scc::HashMap<String, tokio_util::sync::CancellationToken>>,
}
impl TimerScheduleDriver {
pub fn new() -> Self {
Self {
timers: Arc::new(scc::HashMap::new()),
}
}
fn schedule_next(
timers: Arc<scc::HashMap<String, tokio_util::sync::CancellationToken>>,
entry: ScheduleEntry,
cancel: tokio_util::sync::CancellationToken,
) {
let now = chrono::Utc::now();
let parsed = match crate::cron::parse_schedule(&entry.schedule) {
Ok(parsed) => parsed,
Err(_) => {
let _ = timers.remove(&entry.id);
return;
}
};
let is_cron = parsed.is_cron();
let next = match crate::cron::resolve_next_run(&parsed, now) {
Some(next) => next,
None => {
let _ = timers.remove(&entry.id);
return;
}
};
let delay = (next - now)
.to_std()
.unwrap_or(std::time::Duration::ZERO);
tokio::spawn(async move {
tokio::select! {
_ = cancel.cancelled() => {
return;
}
_ = tokio::time::sleep(delay) => {}
}
if cancel.is_cancelled() {
return;
}
(entry.callback)().await;
if is_cron && timers.contains(&entry.id) {
Self::schedule_next(Arc::clone(&timers), entry, cancel);
} else {
let _ = timers.remove(&entry.id);
}
});
}
}
impl ScheduleDriver for TimerScheduleDriver {
fn schedule(&self, entry: ScheduleEntry) -> ScheduleHandle {
let id = entry.id.clone();
let cancel = tokio_util::sync::CancellationToken::new();
if let Some((_, old)) = self.timers.remove(&id) {
old.cancel();
}
let _ = self.timers.insert(id.clone(), cancel.clone());
Self::schedule_next(Arc::clone(&self.timers), entry, cancel);
ScheduleHandle { id }
}
fn cancel(&self, handle: &ScheduleHandle) {
if let Some((_, cancel)) = self.timers.remove(&handle.id) {
cancel.cancel();
}
}
fn dispose(&self) {
self.timers.scan(|_, cancel| cancel.cancel());
self.timers.clear();
}
}
pub(crate) fn empty_metadata() -> BTreeMap<String, String> {
BTreeMap::new()
}