Initial Agentsd project commit

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 15:09:56 +08:00
commit 1bf1f08f9a
41 changed files with 8106 additions and 0 deletions

23
core/agentsd/Cargo.toml Normal file
View File

@@ -0,0 +1,23 @@
[package]
name = "agentsd"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "agentsd"
path = "src/main.rs"
[dependencies]
agentsd-proto = { path = "../proto" }
tokio = { workspace = true }
tokio-stream = { workspace = true }
tonic = { workspace = true }
prost = { workspace = true }
rusqlite = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
anyhow = { workspace = true }
uuid = { workspace = true }
notify = { workspace = true }

View File

@@ -0,0 +1,47 @@
use std::collections::HashMap;
use std::sync::Mutex;
use tokio::sync::mpsc;
/// A fired timer event delivered to the plugin that created it.
#[derive(Clone, Debug)]
pub struct TimerEvent {
pub timer_id: String,
pub callback_id: String,
}
/// Manages per-plugin event channels for timer callbacks.
pub struct CallbackBus {
senders: Mutex<HashMap<String, mpsc::Sender<TimerEvent>>>,
}
impl CallbackBus {
pub fn new() -> Self {
Self {
senders: Mutex::new(HashMap::new()),
}
}
/// Register a plugin and get a receiver for its timer events.
pub fn subscribe(&self, plugin_id: &str) -> mpsc::Receiver<TimerEvent> {
let (tx, rx) = mpsc::channel(64);
self.senders
.lock()
.unwrap()
.insert(plugin_id.to_string(), tx);
rx
}
/// Deliver a timer event to the target plugin.
pub fn fire(&self, plugin_id: &str, event: TimerEvent) {
let senders = self.senders.lock().unwrap();
if let Some(tx) = senders.get(plugin_id) {
// Non-blocking send; drop if channel full
let _ = tx.try_send(event);
}
}
/// Remove a plugin's channel (on unregister/disconnect).
pub fn remove(&self, plugin_id: &str) {
self.senders.lock().unwrap().remove(plugin_id);
}
}

22
core/agentsd/src/main.rs Normal file
View File

@@ -0,0 +1,22 @@
mod syscall;
mod plugin;
mod server;
pub mod paths;
pub mod callback;
pub mod permission;
use anyhow::Result;
use tracing_subscriber::EnvFilter;
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env().add_directive("agentsd=info".parse()?))
.init();
tracing::info!("agentsd starting");
tracing::info!("data dir: {}", paths::data_dir().display());
let kernel = syscall::Kernel::new()?;
server::run(kernel).await
}

12
core/agentsd/src/paths.rs Normal file
View File

@@ -0,0 +1,12 @@
use std::path::PathBuf;
/// Returns the `data/` directory next to the running executable.
/// Creates it if it does not exist.
pub fn data_dir() -> PathBuf {
let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("."));
let dir = exe.parent().unwrap_or_else(|| std::path::Path::new(".")).join("data");
if !dir.exists() {
std::fs::create_dir_all(&dir).ok();
}
dir
}

View File

@@ -0,0 +1,82 @@
use crate::plugin::{PluginManager, PluginRecord, PluginStatus};
use std::sync::Arc;
use tonic::{Request, Status};
/// Extract plugin_id from gRPC metadata.
pub fn plugin_id_from_metadata(req_metadata: &tonic::metadata::MetadataMap) -> Option<String> {
req_metadata
.get("x-plugin-id")
.and_then(|v| v.to_str().ok())
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
}
/// Load a plugin and require it to be currently running.
pub fn require_running_plugin(
plugins: &Arc<PluginManager>,
plugin_id: &str,
) -> Result<PluginRecord, Status> {
let record = plugins.get(plugin_id).ok_or_else(|| {
Status::permission_denied(format!("unknown plugin: {}", plugin_id))
})?;
if record.status != PluginStatus::Running {
return Err(Status::permission_denied(format!(
"plugin '{}' is not running",
plugin_id
)));
}
Ok(record)
}
/// Verify that the caller has permission to call an exact syscall name,
/// e.g. `fs.list`, `memory.write`, `timer.once`.
///
/// Supported grants:
/// - `*` grants everything
/// - `fs` grants all fs syscalls
/// - `fs.*` grants all fs syscalls
/// - `fs.list` grants only that method
pub fn check_permission(
plugins: &Arc<PluginManager>,
req_metadata: &tonic::metadata::MetadataMap,
syscall_name: &str,
) -> Result<String, Status> {
let Some(plugin_id) = plugin_id_from_metadata(req_metadata) else {
// Direct/internal/manual testing call.
return Ok(String::new());
};
let record = require_running_plugin(plugins, &plugin_id)?;
// System plugins have unrestricted access.
if record.plugin_type == "system" {
return Ok(plugin_id);
}
let service_name = syscall_name.split('.').next().unwrap_or(syscall_name);
let service_wildcard = format!("{}.*", service_name);
let allowed = record.syscalls.iter().any(|grant| {
grant == "*" || grant == syscall_name || grant == service_name || grant == &service_wildcard
});
if !allowed {
return Err(Status::permission_denied(format!(
"plugin '{}' has no permission for syscall '{}'",
plugin_id, syscall_name
)));
}
Ok(plugin_id)
}
/// Helper to extract plugin_id and check permission in one call within a service method.
pub fn guard<T>(
plugins: &Arc<PluginManager>,
req: &Request<T>,
syscall_name: &str,
) -> Result<String, Status> {
check_permission(plugins, req.metadata(), syscall_name)
}

139
core/agentsd/src/plugin.rs Normal file
View File

@@ -0,0 +1,139 @@
use std::collections::HashMap;
use std::sync::Mutex;
/// Plugin registry: tracks loaded plugins and their exports.
/// Persists state to `<exe_dir>/data/plugins.json`.
pub struct PluginManager {
plugins: Mutex<HashMap<String, PluginRecord>>,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct PluginRecord {
pub id: String,
pub name: String,
pub version: String,
pub plugin_type: String, // "system" | "basic" | "standard"
pub syscalls: Vec<String>,
pub depends: Vec<String>,
pub exports: Vec<(String, String)>, // (name, description)
pub status: PluginStatus,
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum PluginStatus {
Registered,
Running,
Stopped,
Failed(String),
}
impl PluginManager {
pub fn new() -> Self {
let mut mgr = Self {
plugins: Mutex::new(HashMap::new()),
};
mgr.load_from_disk();
mgr
}
pub fn register(&self, record: PluginRecord) -> Result<(), String> {
let mut plugins = self.plugins.lock().unwrap();
if plugins.contains_key(&record.id) {
tracing::info!(
"plugin re-registered: id={} type={} exports={:?}",
record.id,
record.plugin_type,
record.exports.iter().map(|(n, _)| n).collect::<Vec<_>>()
);
plugins.insert(record.id.clone(), record);
drop(plugins);
self.save_to_disk();
return Ok(());
}
tracing::info!(
"plugin registered: id={} type={} exports={:?}",
record.id,
record.plugin_type,
record.exports.iter().map(|(n, _)| n).collect::<Vec<_>>()
);
plugins.insert(record.id.clone(), record);
drop(plugins);
self.save_to_disk();
Ok(())
}
pub fn get(&self, id: &str) -> Option<PluginRecord> {
self.plugins.lock().unwrap().get(id).cloned()
}
pub fn list(&self) -> Vec<PluginRecord> {
self.plugins.lock().unwrap().values().cloned().collect()
}
pub fn set_status(&self, id: &str, status: PluginStatus) {
let mut plugins = self.plugins.lock().unwrap();
if let Some(record) = plugins.get_mut(id) {
record.status = status;
}
drop(plugins);
self.save_to_disk();
}
pub fn find_export(&self, target: &str, fn_name: &str) -> Option<String> {
let plugins = self.plugins.lock().unwrap();
if let Some(record) = plugins.get(target) {
if record.exports.iter().any(|(n, _)| n == fn_name) {
return Some(target.to_string());
}
}
// Search all plugins for the export
for record in plugins.values() {
if record.exports.iter().any(|(n, _)| n == fn_name) {
return Some(record.id.clone());
}
}
None
}
pub fn unregister(&self, id: &str) -> Result<(), String> {
let mut plugins = self.plugins.lock().unwrap();
plugins
.remove(id)
.map(|_| ())
.ok_or_else(|| format!("plugin '{}' not found", id))?;
drop(plugins);
self.save_to_disk();
Ok(())
}
// --- Persistence ---
fn state_path() -> std::path::PathBuf {
crate::paths::data_dir().join("plugins.json")
}
fn save_to_disk(&self) {
let plugins = self.plugins.lock().unwrap();
let records: Vec<&PluginRecord> = plugins.values().collect();
if let Ok(json) = serde_json::to_string_pretty(&records) {
let _ = std::fs::write(Self::state_path(), json);
}
}
fn load_from_disk(&mut self) {
let path = Self::state_path();
if let Ok(data) = std::fs::read_to_string(&path) {
if let Ok(records) = serde_json::from_str::<Vec<PluginRecord>>(&data) {
let mut plugins = self.plugins.lock().unwrap();
for mut record in records {
// Mark previously-running plugins as stopped on reload
if record.status == PluginStatus::Running {
record.status = PluginStatus::Stopped;
}
plugins.insert(record.id.clone(), record);
}
tracing::info!("loaded {} plugins from {}", plugins.len(), path.display());
}
}
}
}

706
core/agentsd/src/server.rs Normal file
View File

@@ -0,0 +1,706 @@
use crate::permission;
use crate::plugin::{PluginManager, PluginRecord, PluginStatus};
use crate::syscall::Kernel;
use agentsd_proto::agentsd::*;
use agentsd_proto::agentsd::{
audio_server::Audio as AudioService, display_server::Display as DisplayService,
fs_server::Fs as FsService, hid_server::Hid as HidService,
input_server::Input as InputService, memory_server::Memory as MemoryService,
network_server::Network as NetworkService,
plugin_bridge_server::PluginBridge as PluginBridgeService,
process_server::Process as ProcessService, timer_server::Timer as TimerService,
};
use std::sync::Arc;
use tonic::{Request, Response, Status};
pub struct AgentsdServer {
kernel: Arc<Kernel>,
plugins: Arc<PluginManager>,
}
impl AgentsdServer {
fn new(kernel: Kernel) -> Self {
Self {
kernel: Arc::new(kernel),
plugins: Arc::new(PluginManager::new()),
}
}
}
fn auth<T>(plugins: &Arc<PluginManager>, req: &Request<T>, syscall: &str) -> Result<String, Status> {
permission::guard(plugins, req, syscall)
}
// ===== Display =====
pub struct DisplaySvc {
kernel: Arc<Kernel>,
plugins: Arc<PluginManager>,
}
#[tonic::async_trait]
impl DisplayService for DisplaySvc {
async fn text(&self, req: Request<DisplayTextRequest>) -> Result<Response<DisplayResponse>, Status> {
auth(&self.plugins, &req, "display.text")?;
let r = req.into_inner();
match self.kernel.display.text(&r.content) {
Ok(()) => Ok(Response::new(DisplayResponse { ok: true, error: String::new() })),
Err(e) => Ok(Response::new(DisplayResponse { ok: false, error: e })),
}
}
async fn rich(&self, req: Request<DisplayRichRequest>) -> Result<Response<DisplayResponse>, Status> {
auth(&self.plugins, &req, "display.rich")?;
let r = req.into_inner();
match self.kernel.display.rich(&r.markup, &r.format) {
Ok(()) => Ok(Response::new(DisplayResponse { ok: true, error: String::new() })),
Err(e) => Ok(Response::new(DisplayResponse { ok: false, error: e })),
}
}
async fn image(&self, req: Request<DisplayImageRequest>) -> Result<Response<DisplayResponse>, Status> {
auth(&self.plugins, &req, "display.image")?;
let r = req.into_inner();
match self.kernel.display.image(&r.data, &r.format) {
Ok(()) => Ok(Response::new(DisplayResponse { ok: true, error: String::new() })),
Err(e) => Ok(Response::new(DisplayResponse { ok: false, error: e })),
}
}
async fn clear(&self, req: Request<Empty>) -> Result<Response<DisplayResponse>, Status> {
auth(&self.plugins, &req, "display.clear")?;
match self.kernel.display.clear() {
Ok(()) => Ok(Response::new(DisplayResponse { ok: true, error: String::new() })),
Err(e) => Ok(Response::new(DisplayResponse { ok: false, error: e })),
}
}
async fn notify(&self, req: Request<DisplayNotifyRequest>) -> Result<Response<DisplayResponse>, Status> {
auth(&self.plugins, &req, "display.notify")?;
let r = req.into_inner();
match self.kernel.display.notify(&r.message) {
Ok(()) => Ok(Response::new(DisplayResponse { ok: true, error: String::new() })),
Err(e) => Ok(Response::new(DisplayResponse { ok: false, error: e })),
}
}
}
// ===== Audio =====
pub struct AudioSvc {
kernel: Arc<Kernel>,
plugins: Arc<PluginManager>,
}
#[tonic::async_trait]
impl AudioService for AudioSvc {
async fn play(&self, req: Request<tonic::Streaming<AudioChunk>>) -> Result<Response<AudioResponse>, Status> {
auth(&self.plugins, &req, "audio.play")?;
use tokio_stream::StreamExt;
let mut stream = req.into_inner();
let mut all_data = Vec::new();
let mut format = String::new();
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
if format.is_empty() {
format = chunk.format;
}
all_data.extend(chunk.data);
}
match self.kernel.audio.play(&all_data, &format) {
Ok(()) => Ok(Response::new(AudioResponse { ok: true, error: String::new() })),
Err(e) => Ok(Response::new(AudioResponse { ok: false, error: e })),
}
}
async fn stop(&self, req: Request<Empty>) -> Result<Response<AudioResponse>, Status> {
auth(&self.plugins, &req, "audio.stop")?;
match self.kernel.audio.stop() {
Ok(()) => Ok(Response::new(AudioResponse { ok: true, error: String::new() })),
Err(e) => Ok(Response::new(AudioResponse { ok: false, error: e })),
}
}
async fn volume(&self, req: Request<VolumeRequest>) -> Result<Response<AudioResponse>, Status> {
auth(&self.plugins, &req, "audio.volume")?;
let r = req.into_inner();
match self.kernel.audio.volume(r.level) {
Ok(()) => Ok(Response::new(AudioResponse { ok: true, error: String::new() })),
Err(e) => Ok(Response::new(AudioResponse { ok: false, error: e })),
}
}
async fn record_start(&self, req: Request<RecordStartRequest>) -> Result<Response<AudioResponse>, Status> {
auth(&self.plugins, &req, "audio.record_start")?;
let r = req.into_inner();
match self.kernel.audio.record_start(&r.format) {
Ok(()) => Ok(Response::new(AudioResponse { ok: true, error: String::new() })),
Err(e) => Ok(Response::new(AudioResponse { ok: false, error: e })),
}
}
async fn record_stop(&self, req: Request<Empty>) -> Result<Response<AudioData>, Status> {
auth(&self.plugins, &req, "audio.record_stop")?;
match self.kernel.audio.record_stop() {
Ok(data) => Ok(Response::new(AudioData { data, format: String::new() })),
Err(e) => Err(Status::internal(e)),
}
}
type RecordStreamStream = tokio_stream::wrappers::ReceiverStream<Result<AudioChunk, Status>>;
async fn record_stream(&self, req: Request<RecordStartRequest>) -> Result<Response<Self::RecordStreamStream>, Status> {
auth(&self.plugins, &req, "audio.record_stream")?;
let (_tx, rx) = tokio::sync::mpsc::channel(16);
Ok(Response::new(tokio_stream::wrappers::ReceiverStream::new(rx)))
}
}
// ===== FS =====
pub struct FsSvc {
kernel: Arc<Kernel>,
plugins: Arc<PluginManager>,
}
#[tonic::async_trait]
impl FsService for FsSvc {
async fn read(&self, req: Request<FsReadRequest>) -> Result<Response<FsReadResponse>, Status> {
auth(&self.plugins, &req, "fs.read")?;
let r = req.into_inner();
match self.kernel.fs.read(&r.path, r.offset, r.length).await {
Ok(data) => Ok(Response::new(FsReadResponse { data, ok: true, error: String::new() })),
Err(e) => Ok(Response::new(FsReadResponse { data: vec![], ok: false, error: e })),
}
}
async fn write(&self, req: Request<FsWriteRequest>) -> Result<Response<FsResponse>, Status> {
auth(&self.plugins, &req, "fs.write")?;
let r = req.into_inner();
match self.kernel.fs.write(&r.path, &r.data, r.append).await {
Ok(()) => Ok(Response::new(FsResponse { ok: true, error: String::new() })),
Err(e) => Ok(Response::new(FsResponse { ok: false, error: e })),
}
}
async fn delete(&self, req: Request<FsPathRequest>) -> Result<Response<FsResponse>, Status> {
auth(&self.plugins, &req, "fs.delete")?;
let r = req.into_inner();
match self.kernel.fs.delete(&r.path).await {
Ok(()) => Ok(Response::new(FsResponse { ok: true, error: String::new() })),
Err(e) => Ok(Response::new(FsResponse { ok: false, error: e })),
}
}
async fn rename(&self, req: Request<FsRenameRequest>) -> Result<Response<FsResponse>, Status> {
auth(&self.plugins, &req, "fs.rename")?;
let r = req.into_inner();
match self.kernel.fs.rename(&r.from, &r.to).await {
Ok(()) => Ok(Response::new(FsResponse { ok: true, error: String::new() })),
Err(e) => Ok(Response::new(FsResponse { ok: false, error: e })),
}
}
async fn stat(&self, req: Request<FsPathRequest>) -> Result<Response<FsStatResponse>, Status> {
auth(&self.plugins, &req, "fs.stat")?;
let r = req.into_inner();
match self.kernel.fs.stat(&r.path).await {
Ok(info) => Ok(Response::new(FsStatResponse {
ok: true, error: String::new(),
size: info.size, is_dir: info.is_dir, is_file: info.is_file,
modified: info.modified, created: info.created, permissions: 0,
})),
Err(e) => Ok(Response::new(FsStatResponse {
ok: false, error: e, size: 0, is_dir: false, is_file: false,
modified: 0, created: 0, permissions: 0,
})),
}
}
async fn list(&self, req: Request<FsPathRequest>) -> Result<Response<FsListResponse>, Status> {
auth(&self.plugins, &req, "fs.list")?;
let r = req.into_inner();
match self.kernel.fs.list(&r.path).await {
Ok(entries) => Ok(Response::new(FsListResponse {
ok: true, error: String::new(),
entries: entries.into_iter().map(|e| FsEntry {
name: e.name, is_dir: e.is_dir, size: e.size,
}).collect(),
})),
Err(e) => Ok(Response::new(FsListResponse { ok: false, error: e, entries: vec![] })),
}
}
type WatchStream = tokio_stream::wrappers::ReceiverStream<Result<FsWatchEvent, Status>>;
async fn watch(&self, req: Request<FsPathRequest>) -> Result<Response<Self::WatchStream>, Status> {
auth(&self.plugins, &req, "fs.watch")?;
let (_tx, rx) = tokio::sync::mpsc::channel(16);
Ok(Response::new(tokio_stream::wrappers::ReceiverStream::new(rx)))
}
}
// ===== Memory =====
pub struct MemorySvc {
kernel: Arc<Kernel>,
plugins: Arc<PluginManager>,
}
#[tonic::async_trait]
impl MemoryService for MemorySvc {
async fn read(&self, req: Request<MemoryReadRequest>) -> Result<Response<MemoryReadResponse>, Status> {
auth(&self.plugins, &req, "memory.read")?;
let r = req.into_inner();
match self.kernel.memory.read(&r.key) {
Ok(Some(v)) => Ok(Response::new(MemoryReadResponse { value: v, found: true, error: String::new() })),
Ok(None) => Ok(Response::new(MemoryReadResponse { value: vec![], found: false, error: String::new() })),
Err(e) => Ok(Response::new(MemoryReadResponse { value: vec![], found: false, error: e })),
}
}
async fn write(&self, req: Request<MemoryWriteRequest>) -> Result<Response<MemoryResponse>, Status> {
auth(&self.plugins, &req, "memory.write")?;
let r = req.into_inner();
match self.kernel.memory.write(&r.key, &r.value) {
Ok(()) => Ok(Response::new(MemoryResponse { ok: true, error: String::new() })),
Err(e) => Ok(Response::new(MemoryResponse { ok: false, error: e })),
}
}
async fn delete(&self, req: Request<MemoryKeyRequest>) -> Result<Response<MemoryResponse>, Status> {
auth(&self.plugins, &req, "memory.delete")?;
let r = req.into_inner();
match self.kernel.memory.delete(&r.key) {
Ok(()) => Ok(Response::new(MemoryResponse { ok: true, error: String::new() })),
Err(e) => Ok(Response::new(MemoryResponse { ok: false, error: e })),
}
}
async fn list(&self, req: Request<MemoryListRequest>) -> Result<Response<MemoryListResponse>, Status> {
auth(&self.plugins, &req, "memory.list")?;
let r = req.into_inner();
match self.kernel.memory.list(&r.prefix, r.limit) {
Ok(keys) => Ok(Response::new(MemoryListResponse { keys, error: String::new() })),
Err(e) => Ok(Response::new(MemoryListResponse { keys: vec![], error: e })),
}
}
async fn search(&self, req: Request<MemorySearchRequest>) -> Result<Response<MemorySearchResponse>, Status> {
auth(&self.plugins, &req, "memory.search")?;
let r = req.into_inner();
match self.kernel.memory.search(&r.query, r.limit) {
Ok(hits) => Ok(Response::new(MemorySearchResponse {
hits: hits.into_iter().map(|(k, s, sc)| MemorySearchHit { key: k, snippet: s, score: sc }).collect(),
error: String::new(),
})),
Err(e) => Ok(Response::new(MemorySearchResponse { hits: vec![], error: e })),
}
}
}
// ===== Network =====
pub struct NetworkSvc {
kernel: Arc<Kernel>,
plugins: Arc<PluginManager>,
}
#[tonic::async_trait]
impl NetworkService for NetworkSvc {
async fn request(&self, req: Request<HttpRequest>) -> Result<Response<HttpResponse>, Status> {
auth(&self.plugins, &req, "network.request")?;
let r = req.into_inner();
match self.kernel.network.request(&r.url, &r.method, &r.headers, &r.body).await {
Ok((status, headers, body)) => Ok(Response::new(HttpResponse { status, headers, body, error: String::new() })),
Err(e) => Ok(Response::new(HttpResponse { status: 0, headers: Default::default(), body: vec![], error: e })),
}
}
type OpenConnStream = tokio_stream::wrappers::ReceiverStream<Result<DataChunk, Status>>;
async fn open_conn(&self, req: Request<ConnectRequest>) -> Result<Response<Self::OpenConnStream>, Status> {
auth(&self.plugins, &req, "network.open_conn")?;
let (_tx, rx) = tokio::sync::mpsc::channel(16);
Ok(Response::new(tokio_stream::wrappers::ReceiverStream::new(rx)))
}
type ListenStream = tokio_stream::wrappers::ReceiverStream<Result<ConnEvent, Status>>;
async fn listen(&self, req: Request<ListenRequest>) -> Result<Response<Self::ListenStream>, Status> {
auth(&self.plugins, &req, "network.listen")?;
let (_tx, rx) = tokio::sync::mpsc::channel(16);
Ok(Response::new(tokio_stream::wrappers::ReceiverStream::new(rx)))
}
async fn send(&self, req: Request<SendRequest>) -> Result<Response<NetResponse>, Status> {
auth(&self.plugins, &req, "network.send")?;
Ok(Response::new(NetResponse { ok: false, error: "send: not yet implemented".to_string() }))
}
async fn close(&self, req: Request<CloseRequest>) -> Result<Response<NetResponse>, Status> {
auth(&self.plugins, &req, "network.close")?;
Ok(Response::new(NetResponse { ok: false, error: "close: not yet implemented".to_string() }))
}
async fn available(&self, req: Request<Empty>) -> Result<Response<NetAvailableResponse>, Status> {
auth(&self.plugins, &req, "network.available")?;
Ok(Response::new(NetAvailableResponse { available: self.kernel.network.available() }))
}
}
// ===== Input =====
pub struct InputSvc {
kernel: Arc<Kernel>,
plugins: Arc<PluginManager>,
}
#[tonic::async_trait]
impl InputService for InputSvc {
async fn text(&self, req: Request<Empty>) -> Result<Response<InputTextResponse>, Status> {
auth(&self.plugins, &req, "input.text")?;
match self.kernel.input.text().await {
Ok(t) => Ok(Response::new(InputTextResponse { text: t, ok: true, error: String::new() })),
Err(e) => Ok(Response::new(InputTextResponse { text: String::new(), ok: false, error: e })),
}
}
async fn key(&self, req: Request<Empty>) -> Result<Response<InputKeyResponse>, Status> {
auth(&self.plugins, &req, "input.key")?;
Ok(Response::new(InputKeyResponse { key: String::new(), action: String::new(), modifiers: vec![] }))
}
async fn clipboard(&self, req: Request<Empty>) -> Result<Response<InputClipboardResponse>, Status> {
auth(&self.plugins, &req, "input.clipboard")?;
Ok(Response::new(InputClipboardResponse {
data: vec![], mime: String::new(), ok: false, error: "not implemented".to_string(),
}))
}
type EventsStream = tokio_stream::wrappers::ReceiverStream<Result<InputEvent, Status>>;
async fn events(&self, req: Request<Empty>) -> Result<Response<Self::EventsStream>, Status> {
auth(&self.plugins, &req, "input.events")?;
let (_tx, rx) = tokio::sync::mpsc::channel(16);
Ok(Response::new(tokio_stream::wrappers::ReceiverStream::new(rx)))
}
}
// ===== Process =====
pub struct ProcessSvc {
kernel: Arc<Kernel>,
plugins: Arc<PluginManager>,
}
#[tonic::async_trait]
impl ProcessService for ProcessSvc {
async fn spawn(&self, req: Request<SpawnRequest>) -> Result<Response<SpawnResponse>, Status> {
auth(&self.plugins, &req, "process.spawn")?;
let r = req.into_inner();
let args: Vec<String> = r.args;
match self.kernel.process.spawn(&r.cmd, &args, &r.env, &r.cwd).await {
Ok(pid) => Ok(Response::new(SpawnResponse { pid, ok: true, error: String::new() })),
Err(e) => Ok(Response::new(SpawnResponse { pid: 0, ok: false, error: e })),
}
}
async fn kill(&self, req: Request<PidRequest>) -> Result<Response<ProcResponse>, Status> {
auth(&self.plugins, &req, "process.kill")?;
let r = req.into_inner();
match self.kernel.process.kill(r.pid).await {
Ok(()) => Ok(Response::new(ProcResponse { ok: true, error: String::new() })),
Err(e) => Ok(Response::new(ProcResponse { ok: false, error: e })),
}
}
async fn wait(&self, req: Request<PidRequest>) -> Result<Response<WaitResponse>, Status> {
auth(&self.plugins, &req, "process.wait")?;
let r = req.into_inner();
match self.kernel.process.wait(r.pid).await {
Ok(code) => Ok(Response::new(WaitResponse { exit_code: code, ok: true, error: String::new() })),
Err(e) => Ok(Response::new(WaitResponse { exit_code: -1, ok: false, error: e })),
}
}
async fn stdin(&self, req: Request<StdinRequest>) -> Result<Response<ProcResponse>, Status> {
auth(&self.plugins, &req, "process.stdin")?;
let r = req.into_inner();
match self.kernel.process.write_stdin(r.pid, &r.data).await {
Ok(()) => Ok(Response::new(ProcResponse { ok: true, error: String::new() })),
Err(e) => Ok(Response::new(ProcResponse { ok: false, error: e })),
}
}
type StdoutStream = tokio_stream::wrappers::ReceiverStream<Result<DataChunk, Status>>;
async fn stdout(&self, req: Request<PidRequest>) -> Result<Response<Self::StdoutStream>, Status> {
auth(&self.plugins, &req, "process.stdout")?;
let r = req.into_inner();
let (tx, rx) = tokio::sync::mpsc::channel(64);
let kernel = self.kernel.clone();
let pid = r.pid;
tokio::spawn(async move {
loop {
let mut buf = vec![0u8; 4096];
let result = kernel.process.read_stdout(pid, &mut buf).await;
match result {
Ok(0) => break,
Ok(n) => {
if tx.send(Ok(DataChunk { data: buf[..n].to_vec() })).await.is_err() {
break;
}
}
Err(_) => break,
}
}
});
Ok(Response::new(tokio_stream::wrappers::ReceiverStream::new(rx)))
}
async fn signal(&self, req: Request<SignalRequest>) -> Result<Response<ProcResponse>, Status> {
auth(&self.plugins, &req, "process.signal")?;
let r = req.into_inner();
match self.kernel.process.signal(r.pid, r.signal).await {
Ok(()) => Ok(Response::new(ProcResponse { ok: true, error: String::new() })),
Err(e) => Ok(Response::new(ProcResponse { ok: false, error: e })),
}
}
async fn list_proc(&self, req: Request<Empty>) -> Result<Response<ProcListResponse>, Status> {
auth(&self.plugins, &req, "process.list_proc")?;
let procs = self.kernel.process.list();
Ok(Response::new(ProcListResponse {
procs: procs.into_iter().map(|(pid, cmd, status)| ProcInfo { pid, cmd, status }).collect(),
}))
}
}
// ===== Timer =====
pub struct TimerSvc {
kernel: Arc<Kernel>,
plugins: Arc<PluginManager>,
}
#[tonic::async_trait]
impl TimerService for TimerSvc {
async fn once(&self, req: Request<TimerOnceRequest>) -> Result<Response<TimerResponse>, Status> {
let plugin_id = auth(&self.plugins, &req, "timer.once")?;
let r = req.into_inner();
let id = self.kernel.timer.once(r.delay_ms, r.callback_id, plugin_id);
Ok(Response::new(TimerResponse { timer_id: id, ok: true, error: String::new() }))
}
async fn cron(&self, req: Request<TimerCronRequest>) -> Result<Response<TimerResponse>, Status> {
auth(&self.plugins, &req, "timer.cron")?;
Ok(Response::new(TimerResponse {
timer_id: String::new(), ok: false, error: "cron: not yet implemented".to_string(),
}))
}
async fn cancel(&self, req: Request<TimerCancelRequest>) -> Result<Response<TimerResponse>, Status> {
auth(&self.plugins, &req, "timer.cancel")?;
let r = req.into_inner();
match self.kernel.timer.cancel(&r.timer_id) {
Ok(()) => Ok(Response::new(TimerResponse { timer_id: r.timer_id, ok: true, error: String::new() })),
Err(e) => Ok(Response::new(TimerResponse { timer_id: r.timer_id, ok: false, error: e })),
}
}
async fn now(&self, req: Request<Empty>) -> Result<Response<TimerNowResponse>, Status> {
auth(&self.plugins, &req, "timer.now")?;
Ok(Response::new(TimerNowResponse { timestamp_ms: self.kernel.timer.now() }))
}
}
// ===== HID =====
pub struct HidSvc {
kernel: Arc<Kernel>,
plugins: Arc<PluginManager>,
}
#[tonic::async_trait]
impl HidService for HidSvc {
async fn capture(&self, req: Request<CaptureRequest>) -> Result<Response<CaptureResponse>, Status> {
auth(&self.plugins, &req, "hid.capture")?;
let r = req.into_inner();
match self.kernel.hid.capture(r.x, r.y, r.width, r.height) {
Ok(image) => Ok(Response::new(CaptureResponse { image, format: "png".to_string(), ok: true, error: String::new() })),
Err(e) => Ok(Response::new(CaptureResponse { image: vec![], format: String::new(), ok: false, error: e })),
}
}
async fn mouse(&self, req: Request<MouseRequest>) -> Result<Response<HidResponse>, Status> {
auth(&self.plugins, &req, "hid.mouse")?;
let r = req.into_inner();
match self.kernel.hid.mouse(r.x, r.y, &r.action, &r.button) {
Ok(()) => Ok(Response::new(HidResponse { ok: true, error: String::new() })),
Err(e) => Ok(Response::new(HidResponse { ok: false, error: e })),
}
}
async fn keyboard(&self, req: Request<KeyboardRequest>) -> Result<Response<HidResponse>, Status> {
auth(&self.plugins, &req, "hid.keyboard")?;
let r = req.into_inner();
match self.kernel.hid.keyboard(&r.key, &r.action) {
Ok(()) => Ok(Response::new(HidResponse { ok: true, error: String::new() })),
Err(e) => Ok(Response::new(HidResponse { ok: false, error: e })),
}
}
async fn ocr(&self, req: Request<CaptureRequest>) -> Result<Response<OcrResponse>, Status> {
auth(&self.plugins, &req, "hid.ocr")?;
let r = req.into_inner();
match self.kernel.hid.ocr(r.x, r.y, r.width, r.height) {
Ok(text) => Ok(Response::new(OcrResponse { text, ok: true, error: String::new() })),
Err(e) => Ok(Response::new(OcrResponse { text: String::new(), ok: false, error: e })),
}
}
}
// ===== Plugin Bridge =====
pub struct BridgeSvc {
kernel: Arc<Kernel>,
plugins: Arc<PluginManager>,
}
#[tonic::async_trait]
impl PluginBridgeService for BridgeSvc {
async fn register(&self, req: Request<PluginInfo>) -> Result<Response<RegisterResponse>, Status> {
let r = req.into_inner();
let record = PluginRecord {
id: r.id,
name: r.name,
version: r.version,
plugin_type: r.plugin_type,
syscalls: r.syscalls,
depends: r.depends,
exports: r.exports.into_iter().map(|e| (e.name, e.description)).collect(),
status: PluginStatus::Running,
};
match self.plugins.register(record) {
Ok(()) => Ok(Response::new(RegisterResponse { ok: true, error: String::new(), session_id: uuid::Uuid::new_v4().to_string() })),
Err(e) => Ok(Response::new(RegisterResponse { ok: false, error: e, session_id: String::new() })),
}
}
async fn call(&self, req: Request<CallRequest>) -> Result<Response<CallResponse>, Status> {
let r = req.into_inner();
let fn_name = &r.r#fn;
match self.plugins.find_export(&r.target, fn_name) {
Some(_plugin_id) => Ok(Response::new(CallResponse {
result: vec![], ok: false,
error: format!("call routing not yet implemented: {}.{}", r.target, fn_name),
})),
None => Ok(Response::new(CallResponse {
result: vec![], ok: false,
error: format!("export not found: {}.{}", r.target, fn_name),
})),
}
}
type StreamCallStream = tokio_stream::wrappers::ReceiverStream<Result<CallResponse, Status>>;
async fn stream_call(&self, req: Request<CallRequest>) -> Result<Response<Self::StreamCallStream>, Status> {
let metadata_plugin_id = permission::plugin_id_from_metadata(req.metadata());
let r = req.into_inner();
let target = r.target;
if let Some(ref metadata_id) = metadata_plugin_id {
if !target.is_empty() && metadata_id != &target {
return Err(Status::permission_denied(format!(
"plugin '{}' cannot subscribe to '{}' events",
metadata_id, target
)));
}
}
let plugin_id = metadata_plugin_id.unwrap_or(target);
if plugin_id.is_empty() {
return Err(Status::invalid_argument("stream_call requires target or x-plugin-id"));
}
permission::require_running_plugin(&self.plugins, &plugin_id)?;
let mut events = self.kernel.callback_bus.subscribe(&plugin_id);
let (tx, rx) = tokio::sync::mpsc::channel(16);
let bus = self.kernel.callback_bus.clone();
let subscribed_plugin_id = plugin_id.clone();
tokio::spawn(async move {
while let Some(event) = events.recv().await {
let payload = serde_json::json!({
"event": "timer",
"timer_id": event.timer_id,
"callback_id": event.callback_id,
});
let response = CallResponse { result: payload.to_string().into_bytes(), ok: true, error: String::new() };
if tx.send(Ok(response)).await.is_err() {
break;
}
}
bus.remove(&subscribed_plugin_id);
});
Ok(Response::new(tokio_stream::wrappers::ReceiverStream::new(rx)))
}
async fn heartbeat(&self, _req: Request<HeartbeatRequest>) -> Result<Response<HeartbeatResponse>, Status> {
Ok(Response::new(HeartbeatResponse { ok: true }))
}
}
fn display_svc(kernel: &Arc<Kernel>, plugins: &Arc<PluginManager>) -> DisplaySvc {
DisplaySvc { kernel: kernel.clone(), plugins: plugins.clone() }
}
fn audio_svc(kernel: &Arc<Kernel>, plugins: &Arc<PluginManager>) -> AudioSvc {
AudioSvc { kernel: kernel.clone(), plugins: plugins.clone() }
}
fn fs_svc(kernel: &Arc<Kernel>, plugins: &Arc<PluginManager>) -> FsSvc {
FsSvc { kernel: kernel.clone(), plugins: plugins.clone() }
}
fn memory_svc(kernel: &Arc<Kernel>, plugins: &Arc<PluginManager>) -> MemorySvc {
MemorySvc { kernel: kernel.clone(), plugins: plugins.clone() }
}
fn network_svc(kernel: &Arc<Kernel>, plugins: &Arc<PluginManager>) -> NetworkSvc {
NetworkSvc { kernel: kernel.clone(), plugins: plugins.clone() }
}
fn input_svc(kernel: &Arc<Kernel>, plugins: &Arc<PluginManager>) -> InputSvc {
InputSvc { kernel: kernel.clone(), plugins: plugins.clone() }
}
fn process_svc(kernel: &Arc<Kernel>, plugins: &Arc<PluginManager>) -> ProcessSvc {
ProcessSvc { kernel: kernel.clone(), plugins: plugins.clone() }
}
fn timer_svc(kernel: &Arc<Kernel>, plugins: &Arc<PluginManager>) -> TimerSvc {
TimerSvc { kernel: kernel.clone(), plugins: plugins.clone() }
}
fn hid_svc(kernel: &Arc<Kernel>, plugins: &Arc<PluginManager>) -> HidSvc {
HidSvc { kernel: kernel.clone(), plugins: plugins.clone() }
}
// ===== Server startup =====
pub async fn run(kernel: Kernel) -> anyhow::Result<()> {
use agentsd_proto::agentsd::{
audio_server::AudioServer, display_server::DisplayServer, fs_server::FsServer,
hid_server::HidServer, input_server::InputServer, memory_server::MemoryServer,
network_server::NetworkServer, plugin_bridge_server::PluginBridgeServer,
process_server::ProcessServer, timer_server::TimerServer,
};
use tonic::transport::Server;
let server = AgentsdServer::new(kernel);
let addr = "[::1]:50051".parse()?;
tracing::info!("agentsd listening on {}", addr);
Server::builder()
.add_service(DisplayServer::new(display_svc(&server.kernel, &server.plugins)))
.add_service(AudioServer::new(audio_svc(&server.kernel, &server.plugins)))
.add_service(FsServer::new(fs_svc(&server.kernel, &server.plugins)))
.add_service(MemoryServer::new(memory_svc(&server.kernel, &server.plugins)))
.add_service(NetworkServer::new(network_svc(&server.kernel, &server.plugins)))
.add_service(InputServer::new(input_svc(&server.kernel, &server.plugins)))
.add_service(ProcessServer::new(process_svc(&server.kernel, &server.plugins)))
.add_service(TimerServer::new(timer_svc(&server.kernel, &server.plugins)))
.add_service(HidServer::new(hid_svc(&server.kernel, &server.plugins)))
.add_service(PluginBridgeServer::new(BridgeSvc { kernel: server.kernel.clone(), plugins: server.plugins.clone() }))
.serve_with_shutdown(addr, async {
tokio::signal::ctrl_c().await.ok();
tracing::info!("shutdown signal received, stopping...");
})
.await?;
tracing::info!("agentsd stopped");
Ok(())
}

View File

@@ -0,0 +1,54 @@
/// Audio syscall: playback and recording.
/// Actual implementation delegated to system plugins.
/// This is the kernel-side stub that dispatches to the active audio driver.
pub struct AudioSyscall {
available: bool,
}
impl AudioSyscall {
pub fn new() -> Self {
// Audio availability detected at runtime
Self { available: false }
}
#[allow(dead_code)]
pub fn is_available(&self) -> bool {
self.available
}
pub fn play(&self, _data: &[u8], _format: &str) -> Result<(), String> {
if !self.available {
return Err("audio: no driver loaded".to_string());
}
// Dispatched to system plugin via FFI
Ok(())
}
pub fn stop(&self) -> Result<(), String> {
if !self.available {
return Err("audio: no driver loaded".to_string());
}
Ok(())
}
pub fn volume(&self, _level: f32) -> Result<(), String> {
if !self.available {
return Err("audio: no driver loaded".to_string());
}
Ok(())
}
pub fn record_start(&self, _format: &str) -> Result<(), String> {
if !self.available {
return Err("audio: no driver loaded".to_string());
}
Ok(())
}
pub fn record_stop(&self) -> Result<Vec<u8>, String> {
if !self.available {
return Err("audio: no driver loaded".to_string());
}
Ok(Vec::new())
}
}

View File

@@ -0,0 +1,39 @@
use std::io::Write as IoWrite;
/// Display syscall: output to user.
pub struct DisplaySyscall;
impl DisplaySyscall {
pub fn new() -> Self {
Self
}
pub fn text(&self, content: &str) -> Result<(), String> {
let stdout = std::io::stdout();
let mut handle = stdout.lock();
handle
.write_all(content.as_bytes())
.map_err(|e| e.to_string())?;
handle.write_all(b"\n").map_err(|e| e.to_string())?;
handle.flush().map_err(|e| e.to_string())
}
pub fn rich(&self, markup: &str, _format: &str) -> Result<(), String> {
// For now, render as plain text. Rich rendering is a system plugin concern.
self.text(markup)
}
pub fn image(&self, _data: &[u8], _format: &str) -> Result<(), String> {
self.text("[image]")
}
pub fn clear(&self) -> Result<(), String> {
// ANSI clear
print!("\x1b[2J\x1b[H");
std::io::stdout().flush().map_err(|e| e.to_string())
}
pub fn notify(&self, message: &str) -> Result<(), String> {
self.text(&format!("[notify] {}", message))
}
}

View File

@@ -0,0 +1,114 @@
use std::path::Path;
use tokio::fs as async_fs;
/// FS syscall: file system operations.
pub struct FsSyscall;
impl FsSyscall {
pub fn new() -> Self {
Self
}
pub async fn read(&self, path: &str, offset: i64, length: i64) -> Result<Vec<u8>, String> {
let data = async_fs::read(path).await.map_err(|e| e.to_string())?;
let start = if offset > 0 { offset as usize } else { 0 };
let end = if length > 0 {
std::cmp::min(start + length as usize, data.len())
} else {
data.len()
};
if start >= data.len() {
return Ok(Vec::new());
}
Ok(data[start..end].to_vec())
}
pub async fn write(&self, path: &str, data: &[u8], append: bool) -> Result<(), String> {
if let Some(parent) = Path::new(path).parent() {
async_fs::create_dir_all(parent)
.await
.map_err(|e| e.to_string())?;
}
if append {
use tokio::io::AsyncWriteExt;
let mut file = tokio::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.await
.map_err(|e| e.to_string())?;
file.write_all(data).await.map_err(|e| e.to_string())
} else {
async_fs::write(path, data).await.map_err(|e| e.to_string())
}
}
pub async fn delete(&self, path: &str) -> Result<(), String> {
let meta = async_fs::metadata(path).await.map_err(|e| e.to_string())?;
if meta.is_dir() {
async_fs::remove_dir_all(path)
.await
.map_err(|e| e.to_string())
} else {
async_fs::remove_file(path)
.await
.map_err(|e| e.to_string())
}
}
pub async fn rename(&self, from: &str, to: &str) -> Result<(), String> {
async_fs::rename(from, to).await.map_err(|e| e.to_string())
}
pub async fn stat(&self, path: &str) -> Result<FsStatInfo, String> {
let meta = async_fs::metadata(path).await.map_err(|e| e.to_string())?;
let modified = meta
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_millis() as i64)
.unwrap_or(0);
let created = meta
.created()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_millis() as i64)
.unwrap_or(0);
Ok(FsStatInfo {
size: meta.len() as i64,
is_dir: meta.is_dir(),
is_file: meta.is_file(),
modified,
created,
})
}
pub async fn list(&self, dir: &str) -> Result<Vec<FsEntryInfo>, String> {
let mut entries = Vec::new();
let mut read_dir = async_fs::read_dir(dir).await.map_err(|e| e.to_string())?;
while let Some(entry) = read_dir.next_entry().await.map_err(|e| e.to_string())? {
let meta = entry.metadata().await.map_err(|e| e.to_string())?;
entries.push(FsEntryInfo {
name: entry.file_name().to_string_lossy().to_string(),
is_dir: meta.is_dir(),
size: meta.len() as i64,
});
}
Ok(entries)
}
}
pub struct FsStatInfo {
pub size: i64,
pub is_dir: bool,
pub is_file: bool,
pub modified: i64,
pub created: i64,
}
pub struct FsEntryInfo {
pub name: String,
pub is_dir: bool,
pub size: i64,
}

View File

@@ -0,0 +1,50 @@
/// HID syscall: screen capture and input simulation.
/// Capability can be absent (headless environments).
pub struct HidSyscall {
available: bool,
}
impl HidSyscall {
pub fn new() -> Self {
// Detect if desktop environment is available
let available = std::env::var("DISPLAY").is_ok()
|| std::env::var("WAYLAND_DISPLAY").is_ok()
|| cfg!(target_os = "windows")
|| cfg!(target_os = "macos");
Self { available }
}
#[allow(dead_code)]
pub fn is_available(&self) -> bool {
self.available
}
pub fn capture(&self, _x: i32, _y: i32, _w: i32, _h: i32) -> Result<Vec<u8>, String> {
if !self.available {
return Err("hid: no desktop environment".to_string());
}
// Stub: actual implementation via system plugin
Err("hid.capture: not implemented (requires system plugin)".to_string())
}
pub fn mouse(&self, _x: i32, _y: i32, _action: &str, _button: &str) -> Result<(), String> {
if !self.available {
return Err("hid: no desktop environment".to_string());
}
Err("hid.mouse: not implemented (requires system plugin)".to_string())
}
pub fn keyboard(&self, _key: &str, _action: &str) -> Result<(), String> {
if !self.available {
return Err("hid: no desktop environment".to_string());
}
Err("hid.keyboard: not implemented (requires system plugin)".to_string())
}
pub fn ocr(&self, _x: i32, _y: i32, _w: i32, _h: i32) -> Result<String, String> {
if !self.available {
return Err("hid: no desktop environment".to_string());
}
Err("hid.ocr: not implemented (requires system plugin)".to_string())
}
}

View File

@@ -0,0 +1,21 @@
use tokio::io::{self, AsyncBufReadExt, BufReader};
/// Input syscall: receive user input.
pub struct InputSyscall;
impl InputSyscall {
pub fn new() -> Self {
Self
}
pub async fn text(&self) -> Result<String, String> {
let stdin = io::stdin();
let mut reader = BufReader::new(stdin);
let mut line = String::new();
reader
.read_line(&mut line)
.await
.map_err(|e| e.to_string())?;
Ok(line.trim_end().to_string())
}
}

View File

@@ -0,0 +1,111 @@
use rusqlite::{params, Connection};
use std::sync::Mutex;
/// Memory syscall: structured KV storage with FTS5 full-text search.
/// Data is persisted to `<exe_dir>/data/memory.db`.
pub struct MemorySyscall {
conn: Mutex<Connection>,
}
impl MemorySyscall {
pub fn new() -> anyhow::Result<Self> {
let db_path = crate::paths::data_dir().join("memory.db");
let conn = Connection::open(&db_path)?;
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS kv (
key TEXT PRIMARY KEY,
value BLOB NOT NULL,
text_value TEXT,
updated_at INTEGER DEFAULT (strftime('%s','now'))
);
CREATE VIRTUAL TABLE IF NOT EXISTS kv_fts USING fts5(
key, text_value, tokenize='trigram'
);
CREATE TRIGGER IF NOT EXISTS kv_ai AFTER INSERT ON kv BEGIN
INSERT INTO kv_fts(key, text_value) VALUES (NEW.key, NEW.text_value);
END;
CREATE TRIGGER IF NOT EXISTS kv_au AFTER UPDATE ON kv BEGIN
DELETE FROM kv_fts WHERE key = OLD.key;
INSERT INTO kv_fts(key, text_value) VALUES (NEW.key, NEW.text_value);
END;
CREATE TRIGGER IF NOT EXISTS kv_ad AFTER DELETE ON kv BEGIN
DELETE FROM kv_fts WHERE key = OLD.key;
END;",
)?;
tracing::info!("memory: opened {}", db_path.display());
Ok(Self {
conn: Mutex::new(conn),
})
}
pub fn read(&self, key: &str) -> Result<Option<Vec<u8>>, String> {
let conn = self.conn.lock().unwrap();
let mut stmt = conn
.prepare("SELECT value FROM kv WHERE key = ?1")
.map_err(|e| e.to_string())?;
let result = stmt
.query_row(params![key], |row| row.get::<_, Vec<u8>>(0))
.ok();
Ok(result)
}
pub fn write(&self, key: &str, value: &[u8]) -> Result<(), String> {
let conn = self.conn.lock().unwrap();
let text_value = String::from_utf8(value.to_vec()).ok();
conn.execute(
"INSERT OR REPLACE INTO kv (key, value, text_value, updated_at) VALUES (?1, ?2, ?3, strftime('%s','now'))",
params![key, value, text_value],
)
.map_err(|e| e.to_string())?;
Ok(())
}
pub fn delete(&self, key: &str) -> Result<(), String> {
let conn = self.conn.lock().unwrap();
conn.execute("DELETE FROM kv WHERE key = ?1", params![key])
.map_err(|e| e.to_string())?;
Ok(())
}
pub fn list(&self, prefix: &str, limit: i32) -> Result<Vec<String>, String> {
let conn = self.conn.lock().unwrap();
let lim = if limit > 0 { limit } else { 1000 };
let mut stmt = conn
.prepare("SELECT key FROM kv WHERE key LIKE ?1 ORDER BY key LIMIT ?2")
.map_err(|e| e.to_string())?;
let pattern = format!("{}%", prefix);
let rows = stmt
.query_map(params![pattern, lim], |row| row.get::<_, String>(0))
.map_err(|e| e.to_string())?;
let mut keys = Vec::new();
for row in rows {
keys.push(row.map_err(|e| e.to_string())?);
}
Ok(keys)
}
pub fn search(&self, query: &str, limit: i32) -> Result<Vec<(String, String, f32)>, String> {
let conn = self.conn.lock().unwrap();
let lim = if limit > 0 { limit } else { 20 };
let mut stmt = conn
.prepare(
"SELECT key, snippet(kv_fts, 1, '<b>', '</b>', '...', 32), rank
FROM kv_fts WHERE kv_fts MATCH ?1 ORDER BY rank LIMIT ?2",
)
.map_err(|e| e.to_string())?;
let rows = stmt
.query_map(params![query, lim], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, f64>(2)? as f32,
))
})
.map_err(|e| e.to_string())?;
let mut hits = Vec::new();
for row in rows {
hits.push(row.map_err(|e| e.to_string())?);
}
Ok(hits)
}
}

View File

@@ -0,0 +1,46 @@
pub mod display;
pub mod audio;
pub mod fs;
pub mod memory;
pub mod network;
pub mod process;
pub mod timer;
pub mod hid;
pub mod input;
use anyhow::Result;
use std::sync::Arc;
use crate::callback::CallbackBus;
/// The kernel holds all syscall implementations.
pub struct Kernel {
pub display: Arc<display::DisplaySyscall>,
pub audio: Arc<audio::AudioSyscall>,
pub fs: Arc<fs::FsSyscall>,
pub memory: Arc<memory::MemorySyscall>,
pub network: Arc<network::NetworkSyscall>,
pub process: Arc<process::ProcessSyscall>,
pub timer: Arc<timer::TimerSyscall>,
pub hid: Arc<hid::HidSyscall>,
pub input: Arc<input::InputSyscall>,
pub callback_bus: Arc<CallbackBus>,
}
impl Kernel {
pub fn new() -> Result<Self> {
let callback_bus = Arc::new(CallbackBus::new());
Ok(Self {
display: Arc::new(display::DisplaySyscall::new()),
audio: Arc::new(audio::AudioSyscall::new()),
fs: Arc::new(fs::FsSyscall::new()),
memory: Arc::new(memory::MemorySyscall::new()?),
network: Arc::new(network::NetworkSyscall::new()),
process: Arc::new(process::ProcessSyscall::new()),
timer: Arc::new(timer::TimerSyscall::new(callback_bus.clone())),
hid: Arc::new(hid::HidSyscall::new()),
input: Arc::new(input::InputSyscall::new()),
callback_bus,
})
}
}

View File

@@ -0,0 +1,60 @@
/// Network syscall: remote communication.
pub struct NetworkSyscall;
impl NetworkSyscall {
pub fn new() -> Self {
Self
}
pub fn available(&self) -> bool {
// Simple connectivity check: try to resolve a known host
std::net::TcpStream::connect_timeout(
&std::net::SocketAddr::from(([8, 8, 8, 8], 53)),
std::time::Duration::from_secs(2),
)
.is_ok()
}
pub async fn request(
&self,
url: &str,
method: &str,
headers: &std::collections::HashMap<String, String>,
body: &[u8],
) -> Result<(i32, std::collections::HashMap<String, String>, Vec<u8>), String> {
// Minimal HTTP client using tokio TcpStream + manual HTTP.
// For production, use hyper or reqwest. For now, shell out to a basic impl.
use tokio::process::Command;
let mut cmd = Command::new("curl");
cmd.arg("-s")
.arg("-X")
.arg(method)
.arg("-o")
.arg("-")
.arg("-w")
.arg("\n%{http_code}");
for (k, v) in headers {
cmd.arg("-H").arg(format!("{}: {}", k, v));
}
if !body.is_empty() {
cmd.arg("-d").arg(String::from_utf8_lossy(body).to_string());
}
cmd.arg(url);
let output = cmd.output().await.map_err(|e| e.to_string())?;
let raw = String::from_utf8_lossy(&output.stdout);
let lines: Vec<&str> = raw.rsplitn(2, '\n').collect();
let status = lines
.first()
.and_then(|s| s.parse::<i32>().ok())
.unwrap_or(0);
let response_body = lines.get(1).unwrap_or(&"").as_bytes().to_vec();
Ok((status, std::collections::HashMap::new(), response_body))
}
}

View File

@@ -0,0 +1,124 @@
use std::collections::HashMap;
use std::sync::Mutex;
use tokio::process::Command;
struct ManagedProcess {
cmd: String,
}
/// Process syscall: spawn and manage subprocesses.
/// Uses the real OS PID as the identifier.
pub struct ProcessSyscall {
procs: Mutex<HashMap<u64, ManagedProcess>>,
children: tokio::sync::Mutex<HashMap<u64, tokio::process::Child>>,
}
impl ProcessSyscall {
pub fn new() -> Self {
Self {
procs: Mutex::new(HashMap::new()),
children: tokio::sync::Mutex::new(HashMap::new()),
}
}
pub async fn spawn(
&self,
cmd: &str,
args: &[String],
env: &HashMap<String, String>,
cwd: &str,
) -> Result<u64, String> {
let mut command = Command::new(cmd);
command.args(args);
if !cwd.is_empty() {
command.current_dir(cwd);
}
for (k, v) in env {
command.env(k, v);
}
command
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
let child = command.spawn().map_err(|e| e.to_string())?;
let pid = child
.id()
.ok_or_else(|| "spawned process has no OS pid".to_string())? as u64;
self.procs.lock().unwrap().insert(
pid,
ManagedProcess {
cmd: cmd.to_string(),
},
);
self.children.lock().await.insert(pid, child);
tracing::info!("process spawned: pid={} cmd={}", pid, cmd);
Ok(pid)
}
pub async fn kill(&self, pid: u64) -> Result<(), String> {
let mut child = {
let mut children = self.children.lock().await;
children.remove(&pid).ok_or_else(|| format!("pid {} not found", pid))?
};
child.kill().await.map_err(|e| e.to_string())?;
self.procs.lock().unwrap().remove(&pid);
tracing::info!("process killed: pid={}", pid);
Ok(())
}
pub async fn wait(&self, pid: u64) -> Result<i32, String> {
let mut child = {
let mut children = self.children.lock().await;
children.remove(&pid).ok_or_else(|| format!("pid {} not found", pid))?
};
self.procs.lock().unwrap().remove(&pid);
let status = child.wait().await.map_err(|e| e.to_string())?;
Ok(status.code().unwrap_or(-1))
}
pub async fn write_stdin(&self, pid: u64, data: &[u8]) -> Result<(), String> {
use tokio::io::AsyncWriteExt;
let mut children = self.children.lock().await;
if let Some(child) = children.get_mut(&pid) {
if let Some(stdin) = child.stdin.as_mut() {
stdin.write_all(data).await.map_err(|e| e.to_string())?;
stdin.flush().await.map_err(|e| e.to_string())?;
Ok(())
} else {
Err("stdin not available".to_string())
}
} else {
Err(format!("pid {} not found", pid))
}
}
pub async fn read_stdout(&self, pid: u64, buf: &mut [u8]) -> Result<usize, String> {
use tokio::io::AsyncReadExt;
let mut children = self.children.lock().await;
if let Some(child) = children.get_mut(&pid) {
if let Some(stdout) = child.stdout.as_mut() {
stdout.read(buf).await.map_err(|e| e.to_string())
} else {
Err("stdout not available".to_string())
}
} else {
Err(format!("pid {} not found", pid))
}
}
pub async fn signal(&self, pid: u64, _signal: i32) -> Result<(), String> {
self.kill(pid).await
}
pub fn list(&self) -> Vec<(u64, String, String)> {
let procs = self.procs.lock().unwrap();
procs
.iter()
.map(|(pid, mp)| (*pid, mp.cmd.clone(), "running".to_string()))
.collect()
}
}

View File

@@ -0,0 +1,80 @@
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tokio::time::{self, Duration};
use crate::callback::{CallbackBus, TimerEvent};
struct TimerEntry {
handle: tokio::task::JoinHandle<()>,
}
/// Timer syscall: delayed and periodic execution with callback delivery.
pub struct TimerSyscall {
timers: Arc<Mutex<HashMap<String, TimerEntry>>>,
next_id: Mutex<u64>,
bus: Arc<CallbackBus>,
}
impl TimerSyscall {
pub fn new(bus: Arc<CallbackBus>) -> Self {
Self {
timers: Arc::new(Mutex::new(HashMap::new())),
next_id: Mutex::new(1),
bus,
}
}
/// Schedule a one-shot timer. When it fires, a TimerEvent is sent to the plugin.
pub fn once(&self, delay_ms: u64, callback_id: String, plugin_id: String) -> String {
let timer_id = {
let mut next = self.next_id.lock().unwrap();
let id = format!("timer_{}", *next);
*next += 1;
id
};
let tid = timer_id.clone();
let cb = callback_id.clone();
let pid = plugin_id.clone();
let bus = self.bus.clone();
let timers = self.timers.clone();
let handle = tokio::spawn(async move {
time::sleep(Duration::from_millis(delay_ms)).await;
tracing::info!("timer fired: id={} callback={} plugin={}", tid, cb, pid);
bus.fire(
&pid,
TimerEvent {
timer_id: tid.clone(),
callback_id: cb,
},
);
timers.lock().unwrap().remove(&tid);
});
self.timers.lock().unwrap().insert(
timer_id.clone(),
TimerEntry {
handle,
},
);
timer_id
}
pub fn cancel(&self, timer_id: &str) -> Result<(), String> {
let mut timers = self.timers.lock().unwrap();
if let Some(entry) = timers.remove(timer_id) {
entry.handle.abort();
Ok(())
} else {
Err(format!("timer {} not found", timer_id))
}
}
pub fn now(&self) -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64
}
}