Compare commits
2 Commits
3299468df6
...
92075209fb
| Author | SHA1 | Date | |
|---|---|---|---|
| 92075209fb | |||
| e75cfc6812 |
20
Cargo.lock
generated
20
Cargo.lock
generated
@@ -17,6 +17,7 @@ dependencies = [
|
|||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_yaml",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tonic",
|
"tonic",
|
||||||
@@ -1631,6 +1632,19 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_yaml"
|
||||||
|
version = "0.9.34+deprecated"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap 2.14.0",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
"unsafe-libyaml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sharded-slab"
|
name = "sharded-slab"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@@ -2048,6 +2062,12 @@ version = "0.2.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unsafe-libyaml"
|
||||||
|
version = "0.2.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
AI Agent 的 init 系统 + 内核。对标 Linux 内核 + systemd。
|
AI Agent 的 init 系统 + 内核。对标 Linux 内核 + systemd。
|
||||||
|
|
||||||
> 状态:设计草案
|
> 状态:阶段 1(内核骨架)已完成,阶段 2/3 部分完成。详见 docs/03-实施路线.md
|
||||||
|
|
||||||
## 对标
|
## 对标
|
||||||
|
|
||||||
|
|||||||
@@ -25,3 +25,4 @@ reqwest = { workspace = true }
|
|||||||
libc = { workspace = true }
|
libc = { workspace = true }
|
||||||
cron = "0.12"
|
cron = "0.12"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
|
serde_yaml = "0.9"
|
||||||
|
|||||||
53
core/agentsd/src/admin.rs
Normal file
53
core/agentsd/src/admin.rs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
use crate::manifest::PluginManifest;
|
||||||
|
use crate::plugin::PluginManager;
|
||||||
|
use crate::supervisor::Supervisor;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// Admin service: implements the gRPC Admin contract (proto/agentsd.proto)
|
||||||
|
/// wiring manifest + resolver + supervisor + PluginManager together.
|
||||||
|
///
|
||||||
|
/// CONTRACT:
|
||||||
|
/// - Installed-plugin registry persisted at `data/installed.json`:
|
||||||
|
/// [{ id, dir, enabled }] — `dir` is the directory containing plugin.yaml.
|
||||||
|
/// - install(path): load+validate manifest, reject duplicate id, add to registry.
|
||||||
|
/// - uninstall(id): must be stopped first; removes from registry (files untouched).
|
||||||
|
/// - enable/disable(id): flips `enabled`; disable does NOT stop a running plugin
|
||||||
|
/// (matches systemctl semantics); start refuses when disabled.
|
||||||
|
/// - start(id): resolve transitive deps (resolver), start each in topo order,
|
||||||
|
/// skipping already-running ones; waits briefly for gRPC registration.
|
||||||
|
/// - stop(id): warn (in message) if running dependents exist, then stop.
|
||||||
|
/// - restart(id) = stop + start.
|
||||||
|
/// - status/list: merge installed registry + PluginManager records + Supervisor state.
|
||||||
|
/// - logs(id, n): Supervisor::tail_log.
|
||||||
|
/// - autostart(): called from server startup — start all enabled+autoload plugins
|
||||||
|
/// in global topo order; failures are logged, not fatal.
|
||||||
|
pub struct AdminState {
|
||||||
|
pub plugins: Arc<PluginManager>,
|
||||||
|
pub supervisor: Arc<Supervisor>,
|
||||||
|
pub installed_path: PathBuf, // data/installed.json
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct InstalledPlugin {
|
||||||
|
pub id: String,
|
||||||
|
pub dir: PathBuf,
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AdminState {
|
||||||
|
pub fn new(plugins: Arc<PluginManager>, supervisor: Arc<Supervisor>) -> Self {
|
||||||
|
todo!("subagent C: implement")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_manifest(&self, id: &str) -> Result<PluginManifest, String> {
|
||||||
|
todo!("subagent C: implement")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn autostart(self: &Arc<Self>) {
|
||||||
|
todo!("subagent C: implement")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// subagent C: implement `pub struct AdminSvc` + `#[tonic::async_trait] impl AdminService for AdminSvc`
|
||||||
|
// and add `.add_service(AdminServer::new(...))` wiring in server.rs.
|
||||||
29
core/agentsd/src/cli.rs
Normal file
29
core/agentsd/src/cli.rs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/// CLI subcommands (= systemctl). Invoked when argv[1] is a known command;
|
||||||
|
/// otherwise main.rs falls through to daemon mode.
|
||||||
|
///
|
||||||
|
/// CONTRACT:
|
||||||
|
/// - Talks to a RUNNING agentsd daemon via gRPC Admin service at
|
||||||
|
/// AGENTSD_ADDR (default http://[::1]:50051). If the daemon is not
|
||||||
|
/// reachable, print a clear error and exit code 2.
|
||||||
|
/// - Commands:
|
||||||
|
/// agentsd start <id> agentsd stop <id> agentsd restart <id>
|
||||||
|
/// agentsd status [<id>] agentsd list agentsd logs <id> [-n N]
|
||||||
|
/// agentsd install <path> agentsd uninstall <id>
|
||||||
|
/// agentsd enable <id> agentsd disable <id>
|
||||||
|
/// - `status` without id behaves like `list`.
|
||||||
|
/// - Exit codes: 0 ok, 1 operation failed (server said no), 2 cannot connect/usage.
|
||||||
|
/// - Output: human-readable; `list` is an aligned table:
|
||||||
|
/// ID TYPE STATUS ENABLED PID RESTARTS VERSION
|
||||||
|
pub const COMMANDS: &[&str] = &[
|
||||||
|
"start", "stop", "restart", "status", "list", "logs", "install", "uninstall", "enable",
|
||||||
|
"disable",
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn is_cli_invocation(args: &[String]) -> bool {
|
||||||
|
args.first().is_some_and(|c| COMMANDS.contains(&c.as_str()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the CLI. Returns the process exit code.
|
||||||
|
pub async fn run(args: Vec<String>) -> i32 {
|
||||||
|
todo!("subagent D: implement")
|
||||||
|
}
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
|
pub mod admin;
|
||||||
pub mod callback;
|
pub mod callback;
|
||||||
|
pub mod cli;
|
||||||
|
pub mod manifest;
|
||||||
pub mod paths;
|
pub mod paths;
|
||||||
pub mod permission;
|
pub mod permission;
|
||||||
mod plugin;
|
mod plugin;
|
||||||
|
pub mod resolver;
|
||||||
mod server;
|
mod server;
|
||||||
|
pub mod supervisor;
|
||||||
mod syscall;
|
mod syscall;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@@ -10,6 +15,11 @@ use tracing_subscriber::EnvFilter;
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||||
|
if cli::is_cli_invocation(&args) {
|
||||||
|
std::process::exit(cli::run(args).await);
|
||||||
|
}
|
||||||
|
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_env_filter(EnvFilter::from_default_env().add_directive("agentsd=info".parse()?))
|
.with_env_filter(EnvFilter::from_default_env().add_directive("agentsd=info".parse()?))
|
||||||
.init();
|
.init();
|
||||||
|
|||||||
75
core/agentsd/src/manifest.rs
Normal file
75
core/agentsd/src/manifest.rs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
/// Parsed plugin.yaml manifest.
|
||||||
|
///
|
||||||
|
/// CONTRACT (do not change field names/types without updating admin.rs, cli.rs, supervisor.rs):
|
||||||
|
/// - `exports` accepts either a YAML list of {name, description} or the literal string "auto"
|
||||||
|
/// (auto means exports are discovered at runtime via PluginBridge.Register).
|
||||||
|
/// - `enabled`/`autoload` default to true when absent.
|
||||||
|
/// - `entry` is the shell command line used by the supervisor to launch the plugin.
|
||||||
|
/// - `port` is informational (plugins bind via PLUGIN_LISTEN_ADDR env or their own default).
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct PluginManifest {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub version: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub plugin_type: String, // "system" | "basic" | "standard"
|
||||||
|
#[serde(default)]
|
||||||
|
pub syscalls: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub depends: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub exports: ManifestExports,
|
||||||
|
pub entry: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub port: Option<u16>,
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub enabled: bool,
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub autoload: bool,
|
||||||
|
/// Directory containing plugin.yaml. Not part of the YAML itself; set by the loader.
|
||||||
|
#[serde(skip)]
|
||||||
|
pub dir: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_true() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum ManifestExports {
|
||||||
|
#[default]
|
||||||
|
#[serde(skip)]
|
||||||
|
None,
|
||||||
|
Auto(String), // must be the literal "auto"
|
||||||
|
List(Vec<ManifestExport>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct ManifestExport {
|
||||||
|
pub name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginManifest {
|
||||||
|
/// Load and validate `<dir>/plugin.yaml`.
|
||||||
|
pub fn load(dir: &Path) -> Result<Self, String> {
|
||||||
|
todo!("subagent A: implement")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validation: non-empty id/name/version/entry, type in {system,basic,standard},
|
||||||
|
/// exports Auto variant only allows the literal "auto", id charset [a-zA-Z0-9_-]+.
|
||||||
|
pub fn validate(&self) -> Result<(), String> {
|
||||||
|
todo!("subagent A: implement")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scan a directory of plugin dirs (e.g. `plugins/`), loading every subdir with a plugin.yaml.
|
||||||
|
/// Invalid manifests are returned as (dir, error) so callers can report instead of dying.
|
||||||
|
pub fn scan_plugins_dir(root: &Path) -> (Vec<PluginManifest>, Vec<(PathBuf, String)>) {
|
||||||
|
todo!("subagent A: implement")
|
||||||
|
}
|
||||||
40
core/agentsd/src/resolver.rs
Normal file
40
core/agentsd/src/resolver.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// Dependency resolution for plugin start ordering.
|
||||||
|
///
|
||||||
|
/// CONTRACT:
|
||||||
|
/// - Input: map of plugin id -> list of dependency ids (the `depends` field).
|
||||||
|
/// - `resolve_order(ids, deps)` returns the full topological order for starting
|
||||||
|
/// ALL given plugins: dependencies come BEFORE dependents. Deterministic:
|
||||||
|
/// ties broken by lexicographic id order.
|
||||||
|
/// - `start_order_for(target, deps)` returns the chain needed to start `target`:
|
||||||
|
/// its transitive deps (topo-sorted) followed by `target` itself. Plugins not
|
||||||
|
/// reachable from `target` are excluded.
|
||||||
|
/// - Errors are returned as human-readable strings:
|
||||||
|
/// - cycle: "dependency cycle: a -> b -> a"
|
||||||
|
/// - missing: "plugin 'x' depends on 'y' which is not installed"
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DependencyGraph {
|
||||||
|
deps: HashMap<String, Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DependencyGraph {
|
||||||
|
pub fn new(deps: HashMap<String, Vec<String>>) -> Self {
|
||||||
|
Self { deps }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Topological order over all known plugins (deps before dependents).
|
||||||
|
pub fn resolve_order(&self) -> Result<Vec<String>, String> {
|
||||||
|
todo!("subagent A: implement (Kahn's algorithm, lexicographic tie-break)")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transitive-dependency start chain for one target (deps first, target last).
|
||||||
|
pub fn start_order_for(&self, target: &str) -> Result<Vec<String>, String> {
|
||||||
|
todo!("subagent A: implement")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plugins that (transitively) depend on `target` — used to warn on stop.
|
||||||
|
pub fn dependents_of(&self, target: &str) -> Vec<String> {
|
||||||
|
todo!("subagent A: implement")
|
||||||
|
}
|
||||||
|
}
|
||||||
78
core/agentsd/src/supervisor.rs
Normal file
78
core/agentsd/src/supervisor.rs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
/// Process supervisor: spawns plugin processes from their manifest `entry`,
|
||||||
|
/// captures stdout/stderr to `data/logs/<id>.log`, watches for exit, and
|
||||||
|
/// restarts according to policy.
|
||||||
|
///
|
||||||
|
/// CONTRACT (admin.rs depends on these signatures):
|
||||||
|
/// - All methods are cheap to call from async context (no blocking locks held
|
||||||
|
/// across awaits; spawn/wait runs on tokio tasks).
|
||||||
|
/// - Supervisor does NOT know about gRPC registration; it manages OS processes
|
||||||
|
/// only. admin.rs correlates "process running" with "plugin registered".
|
||||||
|
/// - On Windows, stop() must kill the whole process tree (entry commands are
|
||||||
|
/// `cargo run ...` / `python ...` which spawn children).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum RestartPolicy {
|
||||||
|
Always,
|
||||||
|
OnFailure,
|
||||||
|
Never,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SupervisedState {
|
||||||
|
pub pid: u32,
|
||||||
|
pub started_at_ms: u64,
|
||||||
|
pub restart_count: u32,
|
||||||
|
/// true while the watchdog considers this plugin managed (stop() clears it)
|
||||||
|
pub active: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Supervisor {
|
||||||
|
inner: Arc<Mutex<HashMap<String, SupervisedState>>>,
|
||||||
|
logs_dir: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Supervisor {
|
||||||
|
pub fn new(logs_dir: PathBuf) -> Self {
|
||||||
|
todo!("subagent B: implement")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawn `entry` (shell command line) with cwd = plugin dir.
|
||||||
|
/// stdout+stderr appended to `<logs_dir>/<id>.log` with timestamps.
|
||||||
|
/// A watchdog task waits for exit and applies `policy`:
|
||||||
|
/// - exponential backoff starting at 1s, doubling, capped at 30s
|
||||||
|
/// - after 5 consecutive failures within a window, give up and call `on_give_up`
|
||||||
|
/// - clean stop via stop() must NOT trigger restart
|
||||||
|
pub fn start(
|
||||||
|
&self,
|
||||||
|
id: &str,
|
||||||
|
entry: &str,
|
||||||
|
cwd: &PathBuf,
|
||||||
|
env: Vec<(String, String)>,
|
||||||
|
policy: RestartPolicy,
|
||||||
|
on_exit: Box<dyn Fn(&str, Option<i32>) + Send + Sync>,
|
||||||
|
) -> Result<u32, String> {
|
||||||
|
todo!("subagent B: implement")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kill the process tree. Marks state inactive first so the watchdog
|
||||||
|
/// doesn't restart it. No-op error if not running.
|
||||||
|
pub fn stop(&self, id: &str) -> Result<(), String> {
|
||||||
|
todo!("subagent B: implement")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn state(&self, id: &str) -> Option<SupervisedState> {
|
||||||
|
todo!("subagent B: implement")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_running(&self, id: &str) -> bool {
|
||||||
|
todo!("subagent B: implement")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read last `n` lines of `<logs_dir>/<id>.log`.
|
||||||
|
pub fn tail_log(&self, id: &str, n: usize) -> Result<Vec<String>, String> {
|
||||||
|
todo!("subagent B: implement")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ agentsd 定义 9 个 syscall 接口。类比 Linux 内核 syscall 表——接
|
|||||||
|
|
||||||
```
|
```
|
||||||
display.text(content)
|
display.text(content)
|
||||||
display.rich(markup)
|
display.rich(markup, format)
|
||||||
display.image(data, format)
|
display.image(data, format)
|
||||||
display.clear()
|
display.clear()
|
||||||
display.notify(message)
|
display.notify(message)
|
||||||
@@ -27,12 +27,12 @@ display.notify(message)
|
|||||||
**管**:扬声器输出、麦克风输入 | **不管**:TTS/STT(插件的事)
|
**管**:扬声器输出、麦克风输入 | **不管**:TTS/STT(插件的事)
|
||||||
|
|
||||||
```
|
```
|
||||||
audio.play(data, format)
|
audio.play(stream AudioChunk{data, format}) # 客户端流式上传音频帧
|
||||||
audio.stop()
|
audio.stop()
|
||||||
audio.volume(level)
|
audio.volume(level)
|
||||||
audio.record.start(format)
|
audio.record_start(format)
|
||||||
audio.record.stop() -> data
|
audio.record_stop() -> data
|
||||||
audio.record.stream(callback)
|
audio.record_stream(format) -> stream AudioChunk # 服务端流式返回录音帧
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -44,13 +44,13 @@ audio.record.stream(callback)
|
|||||||
**管**:磁盘文件 | **不管**:结构化数据(Memory)
|
**管**:磁盘文件 | **不管**:结构化数据(Memory)
|
||||||
|
|
||||||
```
|
```
|
||||||
fs.read(path) -> data
|
fs.read(path, offset?, length?) -> data
|
||||||
fs.write(path, data)
|
fs.write(path, data, append?)
|
||||||
fs.delete(path)
|
fs.delete(path)
|
||||||
fs.rename(from, to)
|
fs.rename(from, to)
|
||||||
fs.stat(path) -> metadata
|
fs.stat(path) -> metadata
|
||||||
fs.list(dir) -> entries
|
fs.list(dir) -> entries
|
||||||
fs.watch(path, callback)
|
fs.watch(path) -> stream FsWatchEvent{path, kind}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -65,8 +65,8 @@ fs.watch(path, callback)
|
|||||||
memory.read(key) -> value
|
memory.read(key) -> value
|
||||||
memory.write(key, value)
|
memory.write(key, value)
|
||||||
memory.delete(key)
|
memory.delete(key)
|
||||||
memory.list(prefix?) -> keys
|
memory.list(prefix?, limit?) -> keys
|
||||||
memory.search(query) -> results
|
memory.search(query, limit?) -> results
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -99,13 +99,13 @@ gRPC 流式语义:OpenConn/Listen 返回 `stream ConnEvent{conn_id, data, kind}`
|
|||||||
**管**:子进程生命周期、stdio | **不管**:插件间路由(内核调度)
|
**管**:子进程生命周期、stdio | **不管**:插件间路由(内核调度)
|
||||||
|
|
||||||
```
|
```
|
||||||
proc.spawn(cmd, args, env?) -> pid
|
proc.spawn(cmd, args, env?, cwd?) -> pid
|
||||||
proc.kill(pid)
|
proc.kill(pid)
|
||||||
proc.wait(pid) -> exitcode
|
proc.wait(pid) -> exitcode
|
||||||
proc.stdin(pid, data)
|
proc.stdin(pid, data)
|
||||||
proc.stdout(pid) -> data
|
proc.stdout(pid) -> stream DataChunk # 服务端流式输出
|
||||||
proc.signal(pid, sig)
|
proc.signal(pid, sig)
|
||||||
proc.list() -> [pid]
|
proc.list() -> [ProcInfo{pid, cmd, status}]
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -134,10 +134,10 @@ timer.now() -> timestamp
|
|||||||
**管**:屏幕读取、输入模拟 | **不管**:向用户展示(Display)、用户主动输入(Input)
|
**管**:屏幕读取、输入模拟 | **不管**:向用户展示(Display)、用户主动输入(Input)
|
||||||
|
|
||||||
```
|
```
|
||||||
hid.capture(region?) -> image
|
hid.capture(x, y, width, height) -> image
|
||||||
hid.mouse(x, y, action)
|
hid.mouse(x, y, action, button)
|
||||||
hid.keyboard(key, action)
|
hid.keyboard(key, action, modifiers?)
|
||||||
hid.ocr(region?) -> text
|
hid.ocr(x, y, width, height) -> text
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -152,7 +152,7 @@ hid.ocr(region?) -> text
|
|||||||
input.text() -> string
|
input.text() -> string
|
||||||
input.key() -> keyevent
|
input.key() -> keyevent
|
||||||
input.clipboard() -> data
|
input.clipboard() -> data
|
||||||
input.event() -> event
|
input.events() -> stream InputEvent # 服务端流式事件
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -19,21 +19,23 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 系统插件(= 内核模块 / 驱动)
|
### 系统插件(= 内核模块 / 驱动)⏳ 设计目标,未实现
|
||||||
|
|
||||||
为 syscall 提供具体实现。类比 `insmod alsa_driver.ko`。
|
为 syscall 提供具体实现。类比 `insmod alsa_driver.ko`。
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
# 设计示意(provides 字段与 FFI 加载尚未实现)
|
||||||
id: alsa-audio
|
id: alsa-audio
|
||||||
type: system
|
type: system
|
||||||
provides: audio
|
provides: audio
|
||||||
entry: ./libalsa_audio.so
|
entry: ./libalsa_audio.so
|
||||||
```
|
```
|
||||||
|
|
||||||
- FFI 加载(.so/.dylib/.dll),零开销
|
- FFI 加载(.so/.dylib/.dll),零开销 —— **未实现**,当前无动态库加载代码
|
||||||
- 只限 Rust / C ABI
|
- 只限 Rust / C ABI
|
||||||
- 同一 syscall 可有多个驱动,同时只激活一个
|
- 同一 syscall 可有多个驱动,同时只激活一个
|
||||||
- 无依赖
|
- 无依赖
|
||||||
|
- 当前内核仅拒绝外部进程注册 `type: system`(防止权限提升),加载机制本身待实现
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -43,12 +45,16 @@ entry: ./libalsa_audio.so
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
id: tts-edge
|
id: tts-edge
|
||||||
|
name: Edge TTS
|
||||||
|
version: 0.1.0
|
||||||
type: basic
|
type: basic
|
||||||
syscalls:
|
syscalls:
|
||||||
- audio.play
|
- audio.play
|
||||||
- network.request
|
- network.request
|
||||||
exports:
|
exports:
|
||||||
- name: speak
|
- name: speak
|
||||||
|
description: Text to speech
|
||||||
|
entry: python tts_plugin.py
|
||||||
```
|
```
|
||||||
|
|
||||||
- 独立进程,gRPC/TCP 通信(后续切 UDS/Named Pipe)
|
- 独立进程,gRPC/TCP 通信(后续切 UDS/Named Pipe)
|
||||||
@@ -63,6 +69,8 @@ exports:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
id: voice-agent
|
id: voice-agent
|
||||||
|
name: Voice Agent
|
||||||
|
version: 0.1.0
|
||||||
type: standard
|
type: standard
|
||||||
syscalls:
|
syscalls:
|
||||||
- input.text
|
- input.text
|
||||||
@@ -73,12 +81,18 @@ depends:
|
|||||||
- chat-agent
|
- chat-agent
|
||||||
exports:
|
exports:
|
||||||
- name: voice_chat
|
- name: voice_chat
|
||||||
|
description: Voice conversation loop
|
||||||
|
entry: cargo run -p voice-agent
|
||||||
```
|
```
|
||||||
|
|
||||||
- 可依赖基础插件 + 其他标准插件
|
- 可依赖基础插件 + 其他标准插件
|
||||||
- 按依赖拓扑排序加载(= systemd ordering)
|
- 按依赖拓扑排序加载(= systemd ordering)—— **未实现**:`depends` 当前仅随注册存储,不做解析/排序/校验(阶段 4)
|
||||||
- 任意语言
|
- 任意语言
|
||||||
|
|
||||||
|
> **plugin.yaml 的当前角色**:内核**不会**扫描或解析 plugin.yaml——它目前是给开发者看的清单(entry/port 供手动启动用)。
|
||||||
|
> 实际注册机制是插件进程启动后主动调用 `PluginBridge.Register`(gRPC),传 protobuf `PluginInfo{id, name, version, plugin_type, syscalls, depends, exports, endpoint}`。
|
||||||
|
> 基于 YAML 的自动发现/自动加载属于阶段 4 的插件管理目标。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 通信:gRPC(= D-Bus)
|
## 通信:gRPC(= D-Bus)
|
||||||
@@ -96,7 +110,7 @@ exports:
|
|||||||
|
|
||||||
| 插件类型 | 传输 | 延迟 | Linux 类比 |
|
| 插件类型 | 传输 | 延迟 | Linux 类比 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 系统插件 | FFI | ~纳秒 | 内核态函数调用 |
|
| 系统插件(未实现) | FFI | ~纳秒 | 内核态函数调用 |
|
||||||
| 基础/标准(本机,当前) | gRPC/TCP | ~毫秒 | D-Bus over TCP |
|
| 基础/标准(本机,当前) | gRPC/TCP | ~毫秒 | D-Bus over TCP |
|
||||||
| 基础/标准(目标) | gRPC/Unix socket / Windows Named Pipe | ~微秒 | D-Bus over UDS |
|
| 基础/标准(目标) | gRPC/Unix socket / Windows Named Pipe | ~微秒 | D-Bus over UDS |
|
||||||
| 远程(可选) | gRPC/TCP | ~毫秒 | D-Bus over TCP |
|
| 远程(可选) | gRPC/TCP | ~毫秒 | D-Bus over TCP |
|
||||||
@@ -137,7 +151,9 @@ message CallRequest {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 生命周期管理(= systemctl)
|
## 生命周期管理(= systemctl)⏳ 阶段 4 目标,未实现
|
||||||
|
|
||||||
|
以下命令为规划中的 CLI,当前内核仅提供 gRPC server,无任何子命令:
|
||||||
|
|
||||||
| agentsd 命令 | systemd 对标 | 作用 |
|
| agentsd 命令 | systemd 对标 | 作用 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -153,7 +169,9 @@ message CallRequest {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 加载顺序(= systemd boot)
|
## 加载顺序(= systemd boot)⏳ 设计目标,未实现
|
||||||
|
|
||||||
|
当前插件由开发者手动启动,启动后自行注册。规划中的加载顺序:
|
||||||
|
|
||||||
```
|
```
|
||||||
1. 系统插件(FFI,注册 syscall 实现) ← 类比内核模块加载
|
1. 系统插件(FFI,注册 syscall 实现) ← 类比内核模块加载
|
||||||
@@ -163,7 +181,7 @@ message CallRequest {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 看门狗(= Restart=always)
|
## 看门狗(= Restart=always)⏳ 阶段 4 目标,未实现
|
||||||
|
|
||||||
插件崩溃 → agentsd 自动重启(可配置策略):
|
插件崩溃 → agentsd 自动重启(可配置策略):
|
||||||
- `always`:总是重启
|
- `always`:总是重启
|
||||||
@@ -175,8 +193,8 @@ message CallRequest {
|
|||||||
## 权限(= seccomp + cgroup)
|
## 权限(= seccomp + cgroup)
|
||||||
|
|
||||||
- 系统插件:内核信任,无限制
|
- 系统插件:内核信任,无限制
|
||||||
- 基础插件:按 `syscalls` 白名单精确到方法(如 `fs.list`)校验,也支持 `fs` / `fs.*` / `*` 授权(= seccomp filter)
|
- 基础/标准插件:按 `syscalls` 白名单精确到方法(如 `fs.list`)校验,也支持 `fs` / `fs.*` / `*` 授权(= seccomp filter)
|
||||||
- 标准插件:syscalls + depends 白名单,未声明的一律拒绝
|
- `depends` 字段当前仅声明、随注册存储,不参与权限校验(依赖校验属阶段 4)
|
||||||
|
|
||||||
### 身份认证
|
### 身份认证
|
||||||
|
|
||||||
@@ -184,7 +202,7 @@ message CallRequest {
|
|||||||
这是第一道安全边界——即使 server 监听 TCP,未注册的进程也无法调用任何 syscall。
|
这是第一道安全边界——即使 server 监听 TCP,未注册的进程也无法调用任何 syscall。
|
||||||
|
|
||||||
认证流程:
|
认证流程:
|
||||||
1. 插件通过 `PluginBridge.Register` 注册,获得 `session_id`
|
1. 插件通过 `PluginBridge.Register` 注册(返回的 `session_id` 目前仅作回执,不参与后续校验)
|
||||||
2. 后续所有 syscall 请求须在 gRPC metadata 中带 `x-plugin-id: <plugin_id>`
|
2. 后续所有 syscall 请求须在 gRPC metadata 中带 `x-plugin-id: <plugin_id>`
|
||||||
3. 内核校验:plugin_id 存在 → status=Running → syscall 在该插件的白名单内 → 放行
|
3. 内核校验:plugin_id 存在 → status=Running → syscall 在该插件的白名单内 → 放行
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
**验证结果**:agentsd 启动 → Rust 插件通过 gRPC 注册 → ai_test 调用 Display/Memory/FS/Process/Timer syscall 并验证权限拒绝。
|
**验证结果**:agentsd 启动 → Rust 插件通过 gRPC 注册 → ai_test 调用 Display/Memory/FS/Process/Timer syscall 并验证权限拒绝。
|
||||||
|
|
||||||
## 阶段 2:完善核心 syscall ✅ 部分完成
|
## 阶段 2:完善核心 syscall 🔶 部分完成
|
||||||
|
|
||||||
- [x] Network:reqwest 原生 HTTP client(阶段 1 已完成)
|
- [x] Network:reqwest 原生 HTTP client(阶段 1 已完成)
|
||||||
- [x] Network TCP 流:open_conn/send/close/listen 双向 TCP 通信
|
- [x] Network TCP 流:open_conn/send/close/listen 双向 TCP 通信
|
||||||
@@ -33,10 +33,11 @@
|
|||||||
- [ ] HID:接入屏幕截取(Windows: win32 API)
|
- [ ] HID:接入屏幕截取(Windows: win32 API)
|
||||||
- [ ] Unix socket / Named pipe 传输(替代 TCP)
|
- [ ] Unix socket / Named pipe 传输(替代 TCP)
|
||||||
|
|
||||||
## 阶段 3:接入 Agent
|
## 阶段 3:接入 Agent 🔶 部分完成
|
||||||
|
|
||||||
- [ ] Agent 插件:Input → LLM(Network) → Display
|
- [ ] Agent 插件:Input → LLM(Network) → Display
|
||||||
- [ ] Hermes adapter(Python gRPC 插件)
|
- [x] Hermes adapter:`hermes_plugin.py` 桥接 Hermes 工具注册表;Rust 版 hermes 插件提供 echo 验证路由,bridge.call 路由已验证
|
||||||
|
- [x] Claude Code 插件(Rust,standard 类型,depends: hermes,导出 chat/code/review/ask/run)
|
||||||
- [ ] 本地模型插件(llama.cpp via Process syscall)
|
- [ ] 本地模型插件(llama.cpp via Process syscall)
|
||||||
|
|
||||||
## 阶段 4:插件管理(= systemctl)
|
## 阶段 4:插件管理(= systemctl)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ Agentsd/
|
|||||||
│ ├── hid.rs
|
│ ├── hid.rs
|
||||||
│ └── input.rs
|
│ └── input.rs
|
||||||
│
|
│
|
||||||
├── data/ # 运行时数据(程序启动后自动创建)
|
├── data/ # 运行时数据(实际位于可执行文件旁,即 target/debug|release/data/)
|
||||||
│ ├── memory.db # Memory syscall SQLite 数据库
|
│ ├── memory.db # Memory syscall SQLite 数据库
|
||||||
│ └── plugins.json # 插件注册状态
|
│ └── plugins.json # 插件注册状态
|
||||||
│
|
│
|
||||||
@@ -51,19 +51,28 @@ Agentsd/
|
|||||||
│ ├── agentsd_pb2.py
|
│ ├── agentsd_pb2.py
|
||||||
│ └── agentsd_pb2_grpc.py
|
│ └── agentsd_pb2_grpc.py
|
||||||
│
|
│
|
||||||
|
├── plugin-sdk/ # Rust 插件 SDK crate(连接/注册/元数据辅助)
|
||||||
|
│ ├── Cargo.toml
|
||||||
|
│ └── src/lib.rs
|
||||||
|
│
|
||||||
├── echo/ # 测试插件(Rust)
|
├── echo/ # 测试插件(Rust)
|
||||||
|
│ ├── Cargo.toml
|
||||||
│ ├── plugin.yaml
|
│ ├── plugin.yaml
|
||||||
│ └── src/main.rs
|
│ └── src/main.rs
|
||||||
│
|
│
|
||||||
├── ai_test/ # AI 手动验证插件(Rust,enabled=false,autoload=false)
|
├── ai_test/ # AI 手动验证插件(Rust,enabled=false,autoload=false)
|
||||||
|
│ ├── Cargo.toml
|
||||||
│ ├── plugin.yaml
|
│ ├── plugin.yaml
|
||||||
│ └── src/main.rs
|
│ └── src/main.rs
|
||||||
│
|
│
|
||||||
├── hermes/ # Hermes Agent 桥接(Python,Rust 版仅 stub)
|
├── hermes/ # Hermes Agent 桥接(Python 桥接工具注册表;Rust 版提供 echo 验证路由)
|
||||||
|
│ ├── Cargo.toml
|
||||||
│ ├── plugin.yaml
|
│ ├── plugin.yaml
|
||||||
│ └── hermes_plugin.py
|
│ ├── hermes_plugin.py
|
||||||
|
│ └── src/main.rs
|
||||||
│
|
│
|
||||||
└── claudecode/ # Claude Code CLI 桥接(Rust)
|
└── claudecode/ # Claude Code CLI 桥接(Rust)
|
||||||
|
├── Cargo.toml
|
||||||
├── plugin.yaml
|
├── plugin.yaml
|
||||||
└── src/main.rs
|
└── src/main.rs
|
||||||
```
|
```
|
||||||
@@ -114,3 +123,5 @@ python -m grpc_tools.protoc \
|
|||||||
--grpc_python_out=plugins/_sdk \
|
--grpc_python_out=plugins/_sdk \
|
||||||
proto/agentsd.proto
|
proto/agentsd.proto
|
||||||
```
|
```
|
||||||
|
|
||||||
|
注:protoc 只生成 `agentsd_pb2.py` 和 `agentsd_pb2_grpc.py`;`plugins/_sdk/__init__.py` 是手工维护的包标记文件,重新生成后不要删除。
|
||||||
|
|||||||
@@ -214,5 +214,53 @@ message CallResponse { bytes result = 1; bool ok = 2; string error = 3; }
|
|||||||
message HeartbeatRequest { string plugin_id = 1; }
|
message HeartbeatRequest { string plugin_id = 1; }
|
||||||
message HeartbeatResponse { bool ok = 1; }
|
message HeartbeatResponse { bool ok = 1; }
|
||||||
|
|
||||||
|
// ========== Admin (plugin lifecycle management, = systemctl) ==========
|
||||||
|
service Admin {
|
||||||
|
rpc Start(AdminIdRequest) returns (AdminResponse);
|
||||||
|
rpc Stop(AdminIdRequest) returns (AdminResponse);
|
||||||
|
rpc Restart(AdminIdRequest) returns (AdminResponse);
|
||||||
|
rpc Status(AdminIdRequest) returns (AdminStatusResponse);
|
||||||
|
rpc ListPlugins(Empty) returns (AdminListResponse);
|
||||||
|
rpc Logs(AdminLogsRequest) returns (AdminLogsResponse);
|
||||||
|
rpc Install(AdminInstallRequest) returns (AdminResponse);
|
||||||
|
rpc Uninstall(AdminIdRequest) returns (AdminResponse);
|
||||||
|
rpc Enable(AdminIdRequest) returns (AdminResponse);
|
||||||
|
rpc Disable(AdminIdRequest) returns (AdminResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message AdminIdRequest { string id = 1; }
|
||||||
|
message AdminResponse { bool ok = 1; string error = 2; string message = 3; }
|
||||||
|
message AdminStatusResponse {
|
||||||
|
bool ok = 1;
|
||||||
|
string error = 2;
|
||||||
|
AdminPluginState plugin = 3;
|
||||||
|
}
|
||||||
|
message AdminListResponse { repeated AdminPluginState plugins = 1; }
|
||||||
|
message AdminPluginState {
|
||||||
|
string id = 1;
|
||||||
|
string name = 2;
|
||||||
|
string version = 3;
|
||||||
|
string plugin_type = 4;
|
||||||
|
string status = 5; // "registered" | "running" | "stopped" | "failed: <reason>"
|
||||||
|
bool enabled = 6;
|
||||||
|
bool installed = 7;
|
||||||
|
uint32 pid = 8; // 0 if not running under supervisor
|
||||||
|
uint64 started_at_ms = 9; // 0 if not running
|
||||||
|
uint32 restart_count = 10;
|
||||||
|
repeated string depends = 11;
|
||||||
|
}
|
||||||
|
message AdminLogsRequest {
|
||||||
|
string id = 1;
|
||||||
|
uint32 tail_lines = 2; // 0 = default (100)
|
||||||
|
}
|
||||||
|
message AdminLogsResponse {
|
||||||
|
bool ok = 1;
|
||||||
|
string error = 2;
|
||||||
|
repeated string lines = 3;
|
||||||
|
}
|
||||||
|
message AdminInstallRequest {
|
||||||
|
string path = 1; // directory containing plugin.yaml
|
||||||
|
}
|
||||||
|
|
||||||
// ========== Common ==========
|
// ========== Common ==========
|
||||||
message Empty {}
|
message Empty {}
|
||||||
|
|||||||
Reference in New Issue
Block a user