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

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
}
}