Refactor syscall modules to use async/await patterns

- Updated `DisplaySyscall` to use `tokio::io::AsyncWriteExt` for asynchronous text output.
- Refactored `FsSyscall` to read files asynchronously with offset and length handling.
- Modified `MemorySyscall` to use `tokio::task::spawn_blocking` for database operations, allowing async access to SQLite.
- Enhanced `NetworkSyscall` to utilize `reqwest` for HTTP requests, replacing the previous `curl` command execution.
- Improved `ProcessSyscall` to manage subprocesses with async tasks for stdin, stdout, and stderr handling.
- Updated `TimerSyscall` to simplify timer management.
- Adjusted plugin implementations for better async support and error handling.
- Added `tokio-stream` and `tracing-subscriber` dependencies to `Cargo.toml` for enhanced async stream handling and logging.
This commit is contained in:
2026-06-10 01:26:46 +08:00
parent f7919eca40
commit d533d0a30e
21 changed files with 1950 additions and 436 deletions

726
Cargo.lock generated
View File

@@ -8,8 +8,10 @@ version = "0.1.0"
dependencies = [ dependencies = [
"agentsd-proto", "agentsd-proto",
"anyhow", "anyhow",
"libc",
"notify", "notify",
"prost", "prost",
"reqwest",
"rusqlite", "rusqlite",
"serde", "serde",
"serde_json", "serde_json",
@@ -21,6 +23,76 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "agentsd-plugin-ai-test"
version = "0.1.0"
dependencies = [
"agentsd-plugin-sdk",
"agentsd-proto",
"anyhow",
"serde",
"serde_json",
"tokio",
"tokio-stream",
"tonic",
]
[[package]]
name = "agentsd-plugin-claudecode"
version = "2.1.169"
dependencies = [
"agentsd-plugin-sdk",
"agentsd-proto",
"anyhow",
"futures-core",
"serde",
"serde_json",
"tokio",
"tokio-stream",
"tonic",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "agentsd-plugin-echo"
version = "0.1.0"
dependencies = [
"agentsd-plugin-sdk",
"agentsd-proto",
"anyhow",
"tokio",
"tonic",
]
[[package]]
name = "agentsd-plugin-hermes"
version = "0.16.0"
dependencies = [
"agentsd-plugin-sdk",
"agentsd-proto",
"anyhow",
"serde",
"serde_json",
"tokio",
"tokio-stream",
"tonic",
]
[[package]]
name = "agentsd-plugin-sdk"
version = "0.1.0"
dependencies = [
"agentsd-proto",
"anyhow",
"serde",
"serde_json",
"tokio",
"tokio-stream",
"tonic",
"tracing",
]
[[package]] [[package]]
name = "agentsd-proto" name = "agentsd-proto"
version = "0.1.0" version = "0.1.0"
@@ -195,6 +267,23 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "displaydoc"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "either" name = "either"
version = "1.16.0" version = "1.16.0"
@@ -269,6 +358,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "form_urlencoded"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
"percent-encoding",
]
[[package]] [[package]]
name = "fsevent-sys" name = "fsevent-sys"
version = "4.1.0" version = "4.1.0"
@@ -324,8 +422,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"wasi", "wasi",
"wasm-bindgen",
]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi 5.3.0",
"wasip2",
"wasm-bindgen",
] ]
[[package]] [[package]]
@@ -336,7 +450,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"r-efi", "r-efi 6.0.0",
"wasip2", "wasip2",
"wasip3", "wasip3",
] ]
@@ -472,6 +586,22 @@ dependencies = [
"want", "want",
] ]
[[package]]
name = "hyper-rustls"
version = "0.27.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
dependencies = [
"http",
"hyper",
"hyper-util",
"rustls",
"tokio",
"tokio-rustls",
"tower-service",
"webpki-roots",
]
[[package]] [[package]]
name = "hyper-timeout" name = "hyper-timeout"
version = "0.5.2" version = "0.5.2"
@@ -491,13 +621,16 @@ version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [ dependencies = [
"base64",
"bytes", "bytes",
"futures-channel", "futures-channel",
"futures-util", "futures-util",
"http", "http",
"http-body", "http-body",
"hyper", "hyper",
"ipnet",
"libc", "libc",
"percent-encoding",
"pin-project-lite", "pin-project-lite",
"socket2 0.6.4", "socket2 0.6.4",
"tokio", "tokio",
@@ -505,12 +638,115 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "icu_collections"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
dependencies = [
"displaydoc",
"potential_utf",
"utf8_iter",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locale_core"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_normalizer"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
dependencies = [
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
[[package]]
name = "icu_properties"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
dependencies = [
"icu_collections",
"icu_locale_core",
"icu_properties_data",
"icu_provider",
"zerotrie",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
[[package]]
name = "icu_provider"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
dependencies = [
"displaydoc",
"icu_locale_core",
"writeable",
"yoke",
"zerofrom",
"zerotrie",
"zerovec",
]
[[package]] [[package]]
name = "id-arena" name = "id-arena"
version = "2.3.0" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "idna"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
dependencies = [
"idna_adapter",
"smallvec",
"utf8_iter",
]
[[package]]
name = "idna_adapter"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
dependencies = [
"icu_normalizer",
"icu_properties",
]
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.9.3" version = "1.9.3"
@@ -562,6 +798,12 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "ipnet"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.14.0" version = "0.14.0"
@@ -643,6 +885,12 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "litemap"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.14" version = "0.4.14"
@@ -658,6 +906,12 @@ version = "0.4.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
[[package]]
name = "lru-slab"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]] [[package]]
name = "matchers" name = "matchers"
version = "0.2.0" version = "0.2.0"
@@ -817,6 +1071,15 @@ version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]]
name = "potential_utf"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
dependencies = [
"zerovec",
]
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.21" version = "0.2.21"
@@ -897,6 +1160,61 @@ dependencies = [
"prost", "prost",
] ]
[[package]]
name = "quinn"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
dependencies = [
"bytes",
"cfg_aliases",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls",
"socket2 0.6.4",
"thiserror",
"tokio",
"tracing",
"web-time",
]
[[package]]
name = "quinn-proto"
version = "0.11.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
dependencies = [
"bytes",
"getrandom 0.3.4",
"lru-slab",
"rand 0.9.4",
"ring",
"rustc-hash",
"rustls",
"rustls-pki-types",
"slab",
"thiserror",
"tinyvec",
"tracing",
"web-time",
]
[[package]]
name = "quinn-udp"
version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2 0.6.4",
"tracing",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.45" version = "1.0.45"
@@ -906,6 +1224,12 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]] [[package]]
name = "r-efi" name = "r-efi"
version = "6.0.0" version = "6.0.0"
@@ -919,8 +1243,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
dependencies = [ dependencies = [
"libc", "libc",
"rand_chacha", "rand_chacha 0.3.1",
"rand_core", "rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.5",
] ]
[[package]] [[package]]
@@ -930,7 +1264,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [ dependencies = [
"ppv-lite86", "ppv-lite86",
"rand_core", "rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.5",
] ]
[[package]] [[package]]
@@ -942,6 +1286,15 @@ dependencies = [
"getrandom 0.2.17", "getrandom 0.2.17",
] ]
[[package]]
name = "rand_core"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom 0.3.4",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.18" version = "0.5.18"
@@ -980,6 +1333,58 @@ version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "reqwest"
version = "0.12.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64",
"bytes",
"futures-core",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-rustls",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls",
"rustls-pki-types",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tokio-rustls",
"tower 0.5.3",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webpki-roots",
]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.17",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "rusqlite" name = "rusqlite"
version = "0.32.1" version = "0.32.1"
@@ -994,6 +1399,12 @@ dependencies = [
"smallvec", "smallvec",
] ]
[[package]]
name = "rustc-hash"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "1.1.4" version = "1.1.4"
@@ -1007,12 +1418,53 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "rustls"
version = "0.23.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pki-types"
version = "1.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
dependencies = [
"web-time",
"zeroize",
]
[[package]]
name = "rustls-webpki"
version = "0.103.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.22" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]] [[package]]
name = "same-file" name = "same-file"
version = "1.0.6" version = "1.0.6"
@@ -1077,6 +1529,18 @@ dependencies = [
"zmij", "zmij",
] ]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]] [[package]]
name = "sharded-slab" name = "sharded-slab"
version = "0.1.7" version = "0.1.7"
@@ -1134,6 +1598,18 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.117" version = "2.0.117"
@@ -1150,6 +1626,20 @@ name = "sync_wrapper"
version = "1.0.2" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
dependencies = [
"futures-core",
]
[[package]]
name = "synstructure"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "tempfile" name = "tempfile"
@@ -1164,6 +1654,26 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "thread_local" name = "thread_local"
version = "1.1.9" version = "1.1.9"
@@ -1173,6 +1683,31 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "tinystr"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
dependencies = [
"displaydoc",
"zerovec",
]
[[package]]
name = "tinyvec"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.52.3" version = "1.52.3"
@@ -1201,6 +1736,16 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "tokio-rustls"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
"rustls",
"tokio",
]
[[package]] [[package]]
name = "tokio-stream" name = "tokio-stream"
version = "0.1.18" version = "0.1.18"
@@ -1280,7 +1825,7 @@ dependencies = [
"indexmap 1.9.3", "indexmap 1.9.3",
"pin-project", "pin-project",
"pin-project-lite", "pin-project-lite",
"rand", "rand 0.8.6",
"slab", "slab",
"tokio", "tokio",
"tokio-util", "tokio-util",
@@ -1299,10 +1844,29 @@ dependencies = [
"futures-util", "futures-util",
"pin-project-lite", "pin-project-lite",
"sync_wrapper", "sync_wrapper",
"tokio",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
] ]
[[package]]
name = "tower-http"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
dependencies = [
"bitflags 2.13.0",
"bytes",
"futures-util",
"http",
"http-body",
"pin-project-lite",
"tower 0.5.3",
"tower-layer",
"tower-service",
"url",
]
[[package]] [[package]]
name = "tower-layer" name = "tower-layer"
version = "0.3.3" version = "0.3.3"
@@ -1394,6 +1958,30 @@ 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 = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
"serde",
]
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.23.2" version = "1.23.2"
@@ -1479,6 +2067,16 @@ dependencies = [
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54568702fabf5d4849ce2b90fadfa64168a097eaf4b351ce9df8b687a0086aaf"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.123" version = "0.2.123"
@@ -1545,6 +2143,35 @@ dependencies = [
"semver", "semver",
] ]
[[package]]
name = "web-sys"
version = "0.3.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e0871acf327f283dc6da28a1696cdc64fb355ba9f935d052021fa77f35cce69"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webpki-roots"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d"
dependencies = [
"rustls-pki-types",
]
[[package]] [[package]]
name = "winapi-util" name = "winapi-util"
version = "0.1.11" version = "0.1.11"
@@ -1736,6 +2363,35 @@ dependencies = [
"wasmparser", "wasmparser",
] ]
[[package]]
name = "writeable"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
[[package]]
name = "yoke"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
dependencies = [
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.50" version = "0.8.50"
@@ -1756,6 +2412,66 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "zerofrom"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
[[package]]
name = "zerotrie"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
]
[[package]]
name = "zerovec"
version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "zmij" name = "zmij"
version = "1.0.21" version = "1.0.21"

View File

@@ -25,3 +25,5 @@ anyhow = "1"
uuid = { version = "1", features = ["v4"] } uuid = { version = "1", features = ["v4"] }
notify = "7" notify = "7"
futures-core = "0.3" futures-core = "0.3"
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
libc = "0.2"

View File

@@ -21,3 +21,5 @@ tracing-subscriber = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
uuid = { workspace = true } uuid = { workspace = true }
notify = { workspace = true } notify = { workspace = true }
reqwest = { workspace = true }
libc = { workspace = true }

View File

@@ -14,6 +14,12 @@ pub struct CallbackBus {
senders: Mutex<HashMap<String, mpsc::Sender<TimerEvent>>>, senders: Mutex<HashMap<String, mpsc::Sender<TimerEvent>>>,
} }
impl Default for CallbackBus {
fn default() -> Self {
Self::new()
}
}
impl CallbackBus { impl CallbackBus {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@@ -35,8 +41,18 @@ impl CallbackBus {
pub fn fire(&self, plugin_id: &str, event: TimerEvent) { pub fn fire(&self, plugin_id: &str, event: TimerEvent) {
let senders = self.senders.lock().unwrap(); let senders = self.senders.lock().unwrap();
if let Some(tx) = senders.get(plugin_id) { if let Some(tx) = senders.get(plugin_id) {
// Non-blocking send; drop if channel full match tx.try_send(event) {
let _ = tx.try_send(event); Ok(()) => {}
Err(mpsc::error::TrySendError::Full(_)) => {
tracing::warn!("timer event dropped: channel full for plugin={}", plugin_id);
}
Err(mpsc::error::TrySendError::Closed(_)) => {
tracing::debug!(
"timer event dropped: channel closed for plugin={}",
plugin_id
);
}
}
} }
} }

View File

@@ -1,9 +1,9 @@
mod syscall; pub mod callback;
pub mod paths;
pub mod permission;
mod plugin; mod plugin;
mod server; mod server;
pub mod paths; mod syscall;
pub mod callback;
pub mod permission;
use anyhow::Result; use anyhow::Result;
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;

View File

@@ -4,7 +4,10 @@ use std::path::PathBuf;
/// Creates it if it does not exist. /// Creates it if it does not exist.
pub fn data_dir() -> PathBuf { pub fn data_dir() -> PathBuf {
let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from(".")); let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("."));
let dir = exe.parent().unwrap_or_else(|| std::path::Path::new(".")).join("data"); let dir = exe
.parent()
.unwrap_or_else(|| std::path::Path::new("."))
.join("data");
if !dir.exists() { if !dir.exists() {
std::fs::create_dir_all(&dir).ok(); std::fs::create_dir_all(&dir).ok();
} }

View File

@@ -12,13 +12,14 @@ pub fn plugin_id_from_metadata(req_metadata: &tonic::metadata::MetadataMap) -> O
} }
/// Load a plugin and require it to be currently running. /// Load a plugin and require it to be currently running.
#[allow(clippy::result_large_err)]
pub fn require_running_plugin( pub fn require_running_plugin(
plugins: &Arc<PluginManager>, plugins: &Arc<PluginManager>,
plugin_id: &str, plugin_id: &str,
) -> Result<PluginRecord, Status> { ) -> Result<PluginRecord, Status> {
let record = plugins.get(plugin_id).ok_or_else(|| { let record = plugins
Status::permission_denied(format!("unknown plugin: {}", plugin_id)) .get(plugin_id)
})?; .ok_or_else(|| Status::permission_denied(format!("unknown plugin: {}", plugin_id)))?;
if record.status != PluginStatus::Running { if record.status != PluginStatus::Running {
return Err(Status::permission_denied(format!( return Err(Status::permission_denied(format!(
@@ -38,6 +39,7 @@ pub fn require_running_plugin(
/// - `fs` grants all fs syscalls /// - `fs` grants all fs syscalls
/// - `fs.*` grants all fs syscalls /// - `fs.*` grants all fs syscalls
/// - `fs.list` grants only that method /// - `fs.list` grants only that method
#[allow(clippy::result_large_err)]
pub fn check_permission( pub fn check_permission(
plugins: &Arc<PluginManager>, plugins: &Arc<PluginManager>,
req_metadata: &tonic::metadata::MetadataMap, req_metadata: &tonic::metadata::MetadataMap,
@@ -73,6 +75,7 @@ pub fn check_permission(
} }
/// Helper to extract plugin_id and check permission in one call within a service method. /// Helper to extract plugin_id and check permission in one call within a service method.
#[allow(clippy::result_large_err)]
pub fn guard<T>( pub fn guard<T>(
plugins: &Arc<PluginManager>, plugins: &Arc<PluginManager>,
req: &Request<T>, req: &Request<T>,

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
use std::io::Write as IoWrite; use tokio::io::AsyncWriteExt;
/// Display syscall: output to user. /// Display syscall: output to user.
pub struct DisplaySyscall; pub struct DisplaySyscall;
@@ -8,32 +8,34 @@ impl DisplaySyscall {
Self Self
} }
pub fn text(&self, content: &str) -> Result<(), String> { pub async fn text(&self, content: &str) -> Result<(), String> {
let stdout = std::io::stdout(); let mut stdout = tokio::io::stdout();
let mut handle = stdout.lock(); stdout
handle
.write_all(content.as_bytes()) .write_all(content.as_bytes())
.await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
handle.write_all(b"\n").map_err(|e| e.to_string())?; stdout.write_all(b"\n").await.map_err(|e| e.to_string())?;
handle.flush().map_err(|e| e.to_string()) stdout.flush().await.map_err(|e| e.to_string())
} }
pub fn rich(&self, markup: &str, _format: &str) -> Result<(), String> { pub async 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).await
self.text(markup)
} }
pub fn image(&self, _data: &[u8], _format: &str) -> Result<(), String> { pub async fn image(&self, _data: &[u8], _format: &str) -> Result<(), String> {
self.text("[image]") self.text("[image]").await
} }
pub fn clear(&self) -> Result<(), String> { pub async fn clear(&self) -> Result<(), String> {
// ANSI clear let mut stdout = tokio::io::stdout();
print!("\x1b[2J\x1b[H"); stdout
std::io::stdout().flush().map_err(|e| e.to_string()) .write_all(b"\x1b[2J\x1b[H")
.await
.map_err(|e| e.to_string())?;
stdout.flush().await.map_err(|e| e.to_string())
} }
pub fn notify(&self, message: &str) -> Result<(), String> { pub async fn notify(&self, message: &str) -> Result<(), String> {
self.text(&format!("[notify] {}", message)) self.text(&format!("[notify] {}", message)).await
} }
} }

View File

@@ -1,5 +1,8 @@
use std::path::Path; use std::path::Path;
use tokio::fs as async_fs; use tokio::fs as async_fs;
use tokio::io::{AsyncReadExt, AsyncSeekExt};
const MAX_READ_SIZE: usize = 64 * 1024 * 1024; // 64 MB
/// FS syscall: file system operations. /// FS syscall: file system operations.
pub struct FsSyscall; pub struct FsSyscall;
@@ -10,17 +13,25 @@ impl FsSyscall {
} }
pub async fn read(&self, path: &str, offset: i64, length: i64) -> Result<Vec<u8>, String> { 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 mut file = tokio::fs::File::open(path)
let start = if offset > 0 { offset as usize } else { 0 }; .await
let end = if length > 0 { .map_err(|e| e.to_string())?;
std::cmp::min(start + length as usize, data.len()) if offset > 0 {
} else { file.seek(std::io::SeekFrom::Start(offset as u64))
data.len() .await
}; .map_err(|e| e.to_string())?;
if start >= data.len() {
return Ok(Vec::new());
} }
Ok(data[start..end].to_vec()) let len = if length > 0 {
std::cmp::min(length as usize, MAX_READ_SIZE)
} else {
let meta = file.metadata().await.map_err(|e| e.to_string())?;
let remaining = (meta.len() as usize).saturating_sub(offset.max(0) as usize);
std::cmp::min(remaining, MAX_READ_SIZE)
};
let mut buf = vec![0u8; len];
let n = file.read(&mut buf).await.map_err(|e| e.to_string())?;
buf.truncate(n);
Ok(buf)
} }
pub async fn write(&self, path: &str, data: &[u8], append: bool) -> Result<(), String> { pub async fn write(&self, path: &str, data: &[u8], append: bool) -> Result<(), String> {
@@ -50,9 +61,7 @@ impl FsSyscall {
.await .await
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} else { } else {
async_fs::remove_file(path) async_fs::remove_file(path).await.map_err(|e| e.to_string())
.await
.map_err(|e| e.to_string())
} }
} }

View File

@@ -1,10 +1,10 @@
use rusqlite::{params, Connection}; use rusqlite::{params, Connection};
use std::sync::Mutex; use std::sync::{Arc, Mutex};
/// Memory syscall: structured KV storage with FTS5 full-text search. /// Memory syscall: structured KV storage with FTS5 full-text search.
/// Data is persisted to `<exe_dir>/data/memory.db`. /// Data is persisted to `<exe_dir>/data/memory.db`.
pub struct MemorySyscall { pub struct MemorySyscall {
conn: Mutex<Connection>, conn: Arc<Mutex<Connection>>,
} }
impl MemorySyscall { impl MemorySyscall {
@@ -12,7 +12,9 @@ impl MemorySyscall {
let db_path = crate::paths::data_dir().join("memory.db"); let db_path = crate::paths::data_dir().join("memory.db");
let conn = Connection::open(&db_path)?; let conn = Connection::open(&db_path)?;
conn.execute_batch( conn.execute_batch(
"CREATE TABLE IF NOT EXISTS kv ( "PRAGMA journal_mode=WAL;
PRAGMA busy_timeout=5000;
CREATE TABLE IF NOT EXISTS kv (
key TEXT PRIMARY KEY, key TEXT PRIMARY KEY,
value BLOB NOT NULL, value BLOB NOT NULL,
text_value TEXT, text_value TEXT,
@@ -34,78 +36,114 @@ impl MemorySyscall {
)?; )?;
tracing::info!("memory: opened {}", db_path.display()); tracing::info!("memory: opened {}", db_path.display());
Ok(Self { Ok(Self {
conn: Mutex::new(conn), conn: Arc::new(Mutex::new(conn)),
}) })
} }
pub fn read(&self, key: &str) -> Result<Option<Vec<u8>>, String> { pub async fn read(&self, key: &str) -> Result<Option<Vec<u8>>, String> {
let conn = self.conn.lock().unwrap(); let conn = self.conn.clone();
let mut stmt = conn let key = key.to_string();
.prepare("SELECT value FROM kv WHERE key = ?1") tokio::task::spawn_blocking(move || {
.map_err(|e| e.to_string())?; let conn = conn.lock().unwrap();
let result = stmt let mut stmt = conn
.query_row(params![key], |row| row.get::<_, Vec<u8>>(0)) .prepare("SELECT value FROM kv WHERE key = ?1")
.ok(); .map_err(|e| e.to_string())?;
Ok(result) let result = stmt
.query_row(params![key], |row| row.get::<_, Vec<u8>>(0))
.ok();
Ok(result)
})
.await
.map_err(|e| e.to_string())?
} }
pub fn write(&self, key: &str, value: &[u8]) -> Result<(), String> { pub async fn write(&self, key: &str, value: &[u8]) -> Result<(), String> {
let conn = self.conn.lock().unwrap(); let conn = self.conn.clone();
let text_value = String::from_utf8(value.to_vec()).ok(); let key = key.to_string();
conn.execute( let value = value.to_vec();
"INSERT OR REPLACE INTO kv (key, value, text_value, updated_at) VALUES (?1, ?2, ?3, strftime('%s','now'))", tokio::task::spawn_blocking(move || {
params![key, value, text_value], let conn = conn.lock().unwrap();
) let text_value: Option<String> =
.map_err(|e| e.to_string())?; std::str::from_utf8(&value).ok().map(str::to_owned);
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],
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())?; .map_err(|e| e.to_string())?;
let rows = stmt Ok(())
.query_map(params![query, lim], |row| { })
Ok(( .await
row.get::<_, String>(0)?, .map_err(|e| e.to_string())?
row.get::<_, String>(1)?, }
row.get::<_, f64>(2)? as f32,
)) pub async fn delete(&self, key: &str) -> Result<(), String> {
}) let conn = self.conn.clone();
.map_err(|e| e.to_string())?; let key = key.to_string();
let mut hits = Vec::new(); tokio::task::spawn_blocking(move || {
for row in rows { let conn = conn.lock().unwrap();
hits.push(row.map_err(|e| e.to_string())?); conn.execute("DELETE FROM kv WHERE key = ?1", params![key])
} .map_err(|e| e.to_string())?;
Ok(hits) Ok(())
})
.await
.map_err(|e| e.to_string())?
}
pub async fn list(&self, prefix: &str, limit: i32) -> Result<Vec<String>, String> {
let conn = self.conn.clone();
let prefix = prefix.to_string();
tokio::task::spawn_blocking(move || {
let conn = 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)
})
.await
.map_err(|e| e.to_string())?
}
pub async fn search(
&self,
query: &str,
limit: i32,
) -> Result<Vec<(String, String, f32)>, String> {
let conn = self.conn.clone();
let query = query.to_string();
tokio::task::spawn_blocking(move || {
let conn = 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)
})
.await
.map_err(|e| e.to_string())?
} }
} }

View File

@@ -1,12 +1,12 @@
pub mod display;
pub mod audio; pub mod audio;
pub mod display;
pub mod fs; pub mod fs;
pub mod hid;
pub mod input;
pub mod memory; pub mod memory;
pub mod network; pub mod network;
pub mod process; pub mod process;
pub mod timer; pub mod timer;
pub mod hid;
pub mod input;
use anyhow::Result; use anyhow::Result;
use std::sync::Arc; use std::sync::Arc;

View File

@@ -1,60 +1,94 @@
use std::collections::HashMap;
use std::net::SocketAddr;
use std::time::Duration;
const MAX_RESPONSE_BODY: u64 = 16 * 1024 * 1024; // 16 MB
/// Network syscall: remote communication. /// Network syscall: remote communication.
pub struct NetworkSyscall; pub struct NetworkSyscall {
client: reqwest::Client,
}
impl NetworkSyscall { impl NetworkSyscall {
pub fn new() -> Self { pub fn new() -> Self {
Self let client = reqwest::Client::builder()
.connect_timeout(Duration::from_secs(5))
.read_timeout(Duration::from_secs(30))
.timeout(Duration::from_secs(60))
.redirect(reqwest::redirect::Policy::limited(5))
.build()
.unwrap_or_else(|_| reqwest::Client::new());
Self { client }
} }
pub fn available(&self) -> bool { pub async fn available(&self) -> bool {
// Simple connectivity check: try to resolve a known host tokio::time::timeout(
std::net::TcpStream::connect_timeout( Duration::from_secs(2),
&std::net::SocketAddr::from(([8, 8, 8, 8], 53)), tokio::net::TcpStream::connect(SocketAddr::from(([8, 8, 8, 8], 53))),
std::time::Duration::from_secs(2),
) )
.is_ok() .await
.map(|result| result.is_ok())
.unwrap_or(false)
} }
pub async fn request( pub async fn request(
&self, &self,
url: &str, url: &str,
method: &str, method: &str,
headers: &std::collections::HashMap<String, String>, headers: &HashMap<String, String>,
body: &[u8], body: &[u8],
) -> Result<(i32, std::collections::HashMap<String, String>, Vec<u8>), String> { ) -> Result<(i32, HashMap<String, String>, Vec<u8>), String> {
// Minimal HTTP client using tokio TcpStream + manual HTTP. let method = method
// For production, use hyper or reqwest. For now, shell out to a basic impl. .parse::<reqwest::Method>()
use tokio::process::Command; .map_err(|e| format!("invalid HTTP method {method:?}: {e}"))?;
let mut request = self.client.request(method, url);
let mut cmd = Command::new("curl"); for (name, value) in headers {
cmd.arg("-s") let header_name = name
.arg("-X") .parse::<reqwest::header::HeaderName>()
.arg(method) .map_err(|e| format!("invalid HTTP header name {name:?}: {e}"))?;
.arg("-o") let header_value = value
.arg("-") .parse::<reqwest::header::HeaderValue>()
.arg("-w") .map_err(|e| format!("invalid HTTP header value for {name}: {e}"))?;
.arg("\n%{http_code}"); request = request.header(header_name, header_value);
for (k, v) in headers {
cmd.arg("-H").arg(format!("{}: {}", k, v));
} }
if !body.is_empty() { if !body.is_empty() {
cmd.arg("-d").arg(String::from_utf8_lossy(body).to_string()); request = request.body(body.to_vec());
} }
cmd.arg(url); let response = request.send().await.map_err(|e| e.to_string())?;
let output = cmd.output().await.map_err(|e| e.to_string())?; // Check content-length before downloading body
let raw = String::from_utf8_lossy(&output.stdout); if let Some(len) = response.content_length() {
let lines: Vec<&str> = raw.rsplitn(2, '\n').collect(); if len > MAX_RESPONSE_BODY {
return Err(format!(
"response body too large: {} bytes (limit {} bytes)",
len, MAX_RESPONSE_BODY
));
}
}
let status = lines let status = i32::from(response.status().as_u16());
.first() let resp_headers = response
.and_then(|s| s.parse::<i32>().ok()) .headers()
.unwrap_or(0); .iter()
let response_body = lines.get(1).unwrap_or(&"").as_bytes().to_vec(); .map(|(name, value)| {
(
name.to_string(),
value.to_str().unwrap_or_default().to_string(),
)
})
.collect();
let resp_body = response.bytes().await.map_err(|e| e.to_string())?;
if resp_body.len() as u64 > MAX_RESPONSE_BODY {
return Err(format!(
"response body too large: {} bytes (limit {} bytes)",
resp_body.len(),
MAX_RESPONSE_BODY
));
}
Ok((status, std::collections::HashMap::new(), response_body)) Ok((status, resp_headers, resp_body.to_vec()))
} }
} }

View File

@@ -1,23 +1,31 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Mutex; use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::process::Command; use tokio::process::Command;
use tokio::sync::{mpsc, Mutex, RwLock};
/// Per-process managed state, decoupled from the Child handle.
struct ManagedProcess { struct ManagedProcess {
cmd: String, cmd: String,
status: Arc<Mutex<String>>,
stdin_tx: Option<mpsc::Sender<Vec<u8>>>,
stdout_rx: Arc<Mutex<mpsc::Receiver<Vec<u8>>>>,
/// Notified when the process exits. Clone the receiver to wait.
exit_notify: Arc<tokio::sync::Notify>,
/// Handle to abort background tasks on kill.
_tasks: Vec<tokio::task::JoinHandle<()>>,
} }
/// Process syscall: spawn and manage subprocesses. /// Process syscall: spawn and manage subprocesses.
/// Uses the real OS PID as the identifier. /// Uses the real OS PID as the identifier.
pub struct ProcessSyscall { pub struct ProcessSyscall {
procs: Mutex<HashMap<u64, ManagedProcess>>, procs: RwLock<HashMap<u64, Arc<ManagedProcess>>>,
children: tokio::sync::Mutex<HashMap<u64, tokio::process::Child>>,
} }
impl ProcessSyscall { impl ProcessSyscall {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
procs: Mutex::new(HashMap::new()), procs: RwLock::new(HashMap::new()),
children: tokio::sync::Mutex::new(HashMap::new()),
} }
} }
@@ -39,86 +47,244 @@ impl ProcessSyscall {
command command
.stdin(std::process::Stdio::piped()) .stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped()); .stderr(std::process::Stdio::piped())
.kill_on_drop(true);
let child = command.spawn().map_err(|e| e.to_string())?; let mut child = command.spawn().map_err(|e| e.to_string())?;
let pid = child let pid = child
.id() .id()
.ok_or_else(|| "spawned process has no OS pid".to_string())? as u64; .ok_or_else(|| "spawned process has no OS pid".to_string())? as u64;
self.procs.lock().unwrap().insert( // Take ownership of stdio handles
pid, let child_stdin = child.stdin.take();
ManagedProcess { let child_stdout = child.stdout.take();
cmd: cmd.to_string(), let child_stderr = child.stderr.take();
},
);
self.children.lock().await.insert(pid, child);
let status = Arc::new(Mutex::new("running".to_string()));
let exit_notify = Arc::new(tokio::sync::Notify::new());
// Stdin writer task
let (stdin_tx, mut stdin_rx) = mpsc::channel::<Vec<u8>>(64);
let stdin_task = tokio::spawn(async move {
if let Some(mut stdin) = child_stdin {
while let Some(data) = stdin_rx.recv().await {
if stdin.write_all(&data).await.is_err() {
break;
}
if stdin.flush().await.is_err() {
break;
}
}
}
});
// Stdout reader task
let (stdout_tx, stdout_rx) = mpsc::channel::<Vec<u8>>(64);
let stdout_task = tokio::spawn(async move {
if let Some(mut stdout) = child_stdout {
loop {
let mut buf = vec![0u8; 4096];
match stdout.read(&mut buf).await {
Ok(0) => break,
Ok(n) => {
buf.truncate(n);
if stdout_tx.send(buf).await.is_err() {
break;
}
}
Err(_) => break,
}
}
}
});
// Stderr drain task (prevent pipe fill-up)
let stderr_task = tokio::spawn(async move {
if let Some(mut stderr) = child_stderr {
let mut buf = vec![0u8; 4096];
loop {
match stderr.read(&mut buf).await {
Ok(0) | Err(_) => break,
Ok(_) => {} // discard
}
}
}
});
// Reaper task: waits for exit and updates status
let status_clone = status.clone();
let exit_notify_clone = exit_notify.clone();
let reaper_task = tokio::spawn(async move {
let exit_status = child.wait().await;
let code = exit_status.map(|s| s.code().unwrap_or(-1)).unwrap_or(-1);
*status_clone.lock().await = format!("exited({})", code);
exit_notify_clone.notify_waiters();
});
let managed = Arc::new(ManagedProcess {
cmd: cmd.to_string(),
status,
stdin_tx: Some(stdin_tx),
stdout_rx: Arc::new(Mutex::new(stdout_rx)),
exit_notify,
_tasks: vec![stdin_task, stdout_task, stderr_task, reaper_task],
});
self.procs.write().await.insert(pid, managed);
tracing::info!("process spawned: pid={} cmd={}", pid, cmd); tracing::info!("process spawned: pid={} cmd={}", pid, cmd);
Ok(pid) Ok(pid)
} }
pub async fn kill(&self, pid: u64) -> Result<(), String> { pub async fn kill(&self, pid: u64) -> Result<(), String> {
let mut child = { let procs = self.procs.read().await;
let mut children = self.children.lock().await; let proc = procs
children.remove(&pid).ok_or_else(|| format!("pid {} not found", pid))? .get(&pid)
}; .ok_or_else(|| format!("pid {} not found", pid))?
.clone();
drop(procs);
child.kill().await.map_err(|e| e.to_string())?; // Abort all background tasks (stdin writer, stdout reader, stderr drain, reaper)
self.procs.lock().unwrap().remove(&pid); for task in proc._tasks.iter() {
task.abort();
}
*proc.status.lock().await = "killed".to_string();
proc.exit_notify.notify_waiters();
// Also send OS kill
#[cfg(unix)]
{
let os_pid = pid as i32;
unsafe { libc::kill(os_pid as libc::pid_t, 9) };
}
#[cfg(not(unix))]
{
// On Windows, aborting the reaper task triggers kill_on_drop on the Child
}
self.procs.write().await.remove(&pid);
tracing::info!("process killed: pid={}", pid); tracing::info!("process killed: pid={}", pid);
Ok(()) Ok(())
} }
pub async fn wait(&self, pid: u64) -> Result<i32, String> { pub async fn wait(&self, pid: u64) -> Result<i32, String> {
let mut child = { let procs = self.procs.read().await;
let mut children = self.children.lock().await; let proc = procs
children.remove(&pid).ok_or_else(|| format!("pid {} not found", pid))? .get(&pid)
}; .ok_or_else(|| format!("pid {} not found", pid))?
self.procs.lock().unwrap().remove(&pid); .clone();
let status = child.wait().await.map_err(|e| e.to_string())?; drop(procs);
Ok(status.code().unwrap_or(-1))
// Check if already exited before waiting
{
let status = proc.status.lock().await;
if status.starts_with("exited") || *status == "killed" {
let s = status.clone();
drop(status);
self.procs.write().await.remove(&pid);
return parse_exit_code(&s);
}
}
// Wait for exit notification
proc.exit_notify.notified().await;
let status = proc.status.lock().await.clone();
// Clean up from map
self.procs.write().await.remove(&pid);
parse_exit_code(&status)
} }
pub async fn write_stdin(&self, pid: u64, data: &[u8]) -> Result<(), String> { pub async fn write_stdin(&self, pid: u64, data: &[u8]) -> Result<(), String> {
use tokio::io::AsyncWriteExt; let procs = self.procs.read().await;
let mut children = self.children.lock().await; let proc = procs
if let Some(child) = children.get_mut(&pid) { .get(&pid)
if let Some(stdin) = child.stdin.as_mut() { .ok_or_else(|| format!("pid {} not found", pid))?
stdin.write_all(data).await.map_err(|e| e.to_string())?; .clone();
stdin.flush().await.map_err(|e| e.to_string())?; drop(procs);
Ok(())
} else { if let Some(ref tx) = proc.stdin_tx {
Err("stdin not available".to_string()) tx.send(data.to_vec())
} .await
.map_err(|_| "stdin channel closed".to_string())
} else { } else {
Err(format!("pid {} not found", pid)) Err("stdin not available".to_string())
} }
} }
pub async fn read_stdout(&self, pid: u64, buf: &mut [u8]) -> Result<usize, String> { pub async fn read_stdout(&self, pid: u64, buf: &mut [u8]) -> Result<usize, String> {
use tokio::io::AsyncReadExt; let procs = self.procs.read().await;
let mut children = self.children.lock().await; let proc = procs
if let Some(child) = children.get_mut(&pid) { .get(&pid)
if let Some(stdout) = child.stdout.as_mut() { .ok_or_else(|| format!("pid {} not found", pid))?
stdout.read(buf).await.map_err(|e| e.to_string()) .clone();
} else { drop(procs);
Err("stdout not available".to_string())
let mut rx = proc.stdout_rx.lock().await;
match rx.recv().await {
Some(data) => {
let n = std::cmp::min(data.len(), buf.len());
buf[..n].copy_from_slice(&data[..n]);
Ok(n)
} }
} else { None => Ok(0), // EOF
Err(format!("pid {} not found", pid))
} }
} }
pub async fn signal(&self, pid: u64, _signal: i32) -> Result<(), String> { pub async fn signal(&self, pid: u64, signal: i32) -> Result<(), String> {
self.kill(pid).await if signal == 9 {
return self.kill(pid).await;
}
let procs = self.procs.read().await;
if !procs.contains_key(&pid) {
return Err(format!("pid {} not found", pid));
}
drop(procs);
#[cfg(unix)]
{
let os_pid = i32::try_from(pid)
.map_err(|_| format!("pid {} out of range for platform signal API", pid))?;
let result = unsafe { libc::kill(os_pid as libc::pid_t, signal as libc::c_int) };
if result == 0 {
Ok(())
} else {
Err(format!(
"signal {} to pid {} failed: {}",
signal,
pid,
std::io::Error::last_os_error()
))
}
}
#[cfg(not(unix))]
{
Err(format!(
"process.signal only supports signal 9 on this platform, got {}",
signal
))
}
} }
pub fn list(&self) -> Vec<(u64, String, String)> { pub async fn list(&self) -> Vec<(u64, String, String)> {
let procs = self.procs.lock().unwrap(); let procs = self.procs.read().await;
procs let mut result = Vec::with_capacity(procs.len());
.iter() for (pid, mp) in procs.iter() {
.map(|(pid, mp)| (*pid, mp.cmd.clone(), "running".to_string())) let status = mp.status.lock().await.clone();
.collect() result.push((*pid, mp.cmd.clone(), status));
}
result
}
}
fn parse_exit_code(status: &str) -> Result<i32, String> {
if let Some(code_str) = status
.strip_prefix("exited(")
.and_then(|s| s.strip_suffix(')'))
{
code_str.parse::<i32>().map_err(|e| e.to_string())
} else {
Ok(-1)
} }
} }

View File

@@ -52,12 +52,10 @@ impl TimerSyscall {
timers.lock().unwrap().remove(&tid); timers.lock().unwrap().remove(&tid);
}); });
self.timers.lock().unwrap().insert( self.timers
timer_id.clone(), .lock()
TimerEntry { .unwrap()
handle, .insert(timer_id.clone(), TimerEntry { handle });
},
);
timer_id timer_id
} }

View File

@@ -304,7 +304,7 @@ impl CheckRunner {
.into_inner(); .into_inner();
let names: Vec<&str> = list.entries.iter().map(|e| e.name.as_str()).collect(); let names: Vec<&str> = list.entries.iter().map(|e| e.name.as_str()).collect();
ensure!( ensure!(
list.ok && names.iter().any(|name| *name == "sample.txt"), list.ok && names.contains(&"sample.txt"),
"list mismatch ok={} names={:?} error={:?}", "list mismatch ok={} names={:?} error={:?}",
list.ok, list.ok,
names, names,
@@ -341,12 +341,13 @@ impl CheckRunner {
async fn process_roundtrip(&mut self) -> Result<String> { async fn process_roundtrip(&mut self) -> Result<String> {
let mut process = ProcessClient::new(self.channel.clone()); let mut process = ProcessClient::new(self.channel.clone());
let (cmd, args) = process_command();
let resp = process let resp = process
.spawn(request_with_plugin( .spawn(request_with_plugin(
PLUGIN_ID, PLUGIN_ID,
SpawnRequest { SpawnRequest {
cmd: "cmd".to_string(), cmd,
args: vec!["/C".to_string(), format!("echo {PROCESS_OK}")], args,
env: HashMap::new(), env: HashMap::new(),
cwd: executable_dir() cwd: executable_dir()
.map(|path| path_string(&path)) .map(|path| path_string(&path))
@@ -504,6 +505,20 @@ fn executable_data_dir() -> PathBuf {
.join("data") .join("data")
} }
fn process_command() -> (String, Vec<String>) {
if cfg!(windows) {
(
"cmd".to_string(),
vec!["/C".to_string(), format!("echo {PROCESS_OK}")],
)
} else {
(
"sh".to_string(),
vec!["-c".to_string(), format!("printf %s {PROCESS_OK}")],
)
}
}
fn path_string(path: &std::path::Path) -> String { fn path_string(path: &std::path::Path) -> String {
path.to_string_lossy().to_string() path.to_string_lossy().to_string()
} }

View File

@@ -14,6 +14,8 @@ anyhow = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
tokio-stream = { workspace = true }
tonic = { workspace = true } tonic = { workspace = true }
futures-core = { workspace = true } futures-core = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
tracing-subscriber = { workspace = true }

View File

@@ -12,7 +12,7 @@ use tonic::{transport::Server, Request, Response, Status};
const PLUGIN_ID: &str = "claudecode"; const PLUGIN_ID: &str = "claudecode";
const DEFAULT_PORT: u16 = 50053; const DEFAULT_PORT: u16 = 50053;
#[derive(Debug, Deserialize)] #[derive(Debug, Default, Deserialize)]
struct CallArgs { struct CallArgs {
#[serde(default)] #[serde(default)]
prompt: String, prompt: String,
@@ -32,26 +32,22 @@ fn find_claude_bin() -> Option<String> {
return Some(bin); return Some(bin);
} }
let candidates = vec![ let appdata_claude = std::env::var("APPDATA")
"claude", .ok()
"claude.cmd", .map(|p| format!("{}\\npm\\claude.cmd", p));
&std::env::var("APPDATA") let mut candidates = vec!["claude".to_string(), "claude.cmd".to_string()];
.map(|p| format!("{}\\npm\\claude.cmd", p)) if let Some(path) = appdata_claude {
.unwrap_or_default(), candidates.push(path);
]; }
for candidate in candidates { candidates.into_iter().find(|candidate| {
if std::process::Command::new(candidate) std::process::Command::new(candidate)
.arg("--version") .arg("--version")
.stdout(Stdio::null()) .stdout(Stdio::null())
.stderr(Stdio::null()) .stderr(Stdio::null())
.status() .status()
.is_ok() .is_ok()
{ })
return Some(candidate.to_string());
}
}
None
} }
async fn call_claude( async fn call_claude(
@@ -97,9 +93,12 @@ async fn call_claude(
drop(stdin); drop(stdin);
} }
let output = tokio::time::timeout(std::time::Duration::from_secs(300), child.wait_with_output()) let output = tokio::time::timeout(
.await std::time::Duration::from_secs(300),
.context("claude timeout (300s)")??; child.wait_with_output(),
)
.await
.context("claude timeout (300s)")??;
if !output.status.success() { if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr); let stderr = String::from_utf8_lossy(&output.stderr);
@@ -148,8 +147,10 @@ impl ClaudeCodePlugin {
.await .await
} }
"review" => { "review" => {
let review_prompt = let review_prompt = format!(
format!("Review the following code or changes. Be concise and actionable:\n\n{}", prompt); "Review the following code or changes. Be concise and actionable:\n\n{}",
prompt
);
call_claude( call_claude(
&self.claude_bin, &self.claude_bin,
&review_prompt, &review_prompt,
@@ -184,7 +185,10 @@ impl ClaudeCodePlugin {
#[tonic::async_trait] #[tonic::async_trait]
impl PluginBridge for ClaudeCodePlugin { impl PluginBridge for ClaudeCodePlugin {
async fn register(&self, _req: Request<PluginInfo>) -> Result<Response<RegisterResponse>, Status> { async fn register(
&self,
_req: Request<PluginInfo>,
) -> Result<Response<RegisterResponse>, Status> {
Err(Status::unimplemented("not a bridge")) Err(Status::unimplemented("not a bridge"))
} }
@@ -195,13 +199,19 @@ impl PluginBridge for ClaudeCodePlugin {
Ok(Response::new(response)) Ok(Response::new(response))
} }
type StreamCallStream = futures_core::stream::Empty<Result<CallResponse, Status>>; type StreamCallStream = tokio_stream::Empty<Result<CallResponse, Status>>;
async fn stream_call(&self, _req: Request<CallRequest>) -> Result<Response<Self::StreamCallStream>, Status> { async fn stream_call(
&self,
_req: Request<CallRequest>,
) -> Result<Response<Self::StreamCallStream>, Status> {
Err(Status::unimplemented("stream_call not supported")) Err(Status::unimplemented("stream_call not supported"))
} }
async fn heartbeat(&self, _req: Request<HeartbeatRequest>) -> Result<Response<HeartbeatResponse>, Status> { async fn heartbeat(
&self,
_req: Request<HeartbeatRequest>,
) -> Result<Response<HeartbeatResponse>, Status> {
Ok(Response::new(HeartbeatResponse { ok: true })) Ok(Response::new(HeartbeatResponse { ok: true }))
} }
} }
@@ -210,7 +220,8 @@ impl PluginBridge for ClaudeCodePlugin {
async fn main() -> Result<()> { async fn main() -> Result<()> {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
let claude_bin = find_claude_bin().context("Claude Code CLI not found. Install: npm install -g @anthropic-ai/claude-code")?; let claude_bin = find_claude_bin()
.context("Claude Code CLI not found. Install: npm install -g @anthropic-ai/claude-code")?;
tracing::info!("Found Claude Code CLI: {}", claude_bin); tracing::info!("Found Claude Code CLI: {}", claude_bin);
let endpoint = agentsd_plugin_sdk::plugin_endpoint_from_env(DEFAULT_PORT); let endpoint = agentsd_plugin_sdk::plugin_endpoint_from_env(DEFAULT_PORT);
@@ -224,21 +235,42 @@ async fn main() -> Result<()> {
"Claude Code", "Claude Code",
env!("CARGO_PKG_VERSION"), env!("CARGO_PKG_VERSION"),
"standard", "standard",
&["process.spawn", "display.text", "fs.read", "fs.write", "network.request"], &[
"process.spawn",
"display.text",
"fs.read",
"fs.write",
"network.request",
],
&["hermes"], &["hermes"],
vec![ vec![
export("chat", "Send prompt to Claude Code, get text response (no tools)"), export(
export("code", "Generate or edit code with Claude Code (Bash/Read/Edit/Write)"), "chat",
"Send prompt to Claude Code, get text response (no tools)",
),
export(
"code",
"Generate or edit code with Claude Code (Bash/Read/Edit/Write)",
),
export("review", "Review code changes (Read/Glob/Grep only)"), export("review", "Review code changes (Read/Glob/Grep only)"),
export("ask", "Ask about the codebase (Read/Glob/Grep, max 3 turns)"), export(
export("run", "Full Claude Code access (all tools, unlimited turns)"), "ask",
"Ask about the codebase (Read/Glob/Grep, max 3 turns)",
),
export(
"run",
"Full Claude Code access (all tools, unlimited turns)",
),
], ],
&endpoint, &endpoint,
), ),
) )
.await?; .await?;
tracing::info!("Registered with agentsd: session={}", register_resp.session_id); tracing::info!(
"Registered with agentsd: session={}",
register_resp.session_id
);
let plugin = ClaudeCodePlugin::new(claude_bin); let plugin = ClaudeCodePlugin::new(claude_bin);
let addr = listen_addr.parse()?; let addr = listen_addr.parse()?;

View File

@@ -26,7 +26,10 @@ async fn main() -> Result<()> {
.await? .await?
.into_inner(); .into_inner();
ensure!(register.ok, "register failed: {}", register.error); ensure!(register.ok, "register failed: {}", register.error);
println!("Register: ok={} session={}", register.ok, register.session_id); println!(
"Register: ok={} session={}",
register.ok, register.session_id
);
let mut display = DisplayClient::new(channel.clone()); let mut display = DisplayClient::new(channel.clone());
let resp = display let resp = display
@@ -84,7 +87,11 @@ async fn main() -> Result<()> {
ensure!(resp.ok, "FS.List failed: {}", resp.error); ensure!(resp.ok, "FS.List failed: {}", resp.error);
println!("FS.List: ok={} entries={}", resp.ok, resp.entries.len()); println!("FS.List: ok={} entries={}", resp.ok, resp.entries.len());
for entry in resp.entries.iter().take(5) { for entry in resp.entries.iter().take(5) {
println!(" {} {}", entry.name, if entry.is_dir { "[dir]" } else { "" }); println!(
" {} {}",
entry.name,
if entry.is_dir { "[dir]" } else { "" }
);
} }
println!("\nAll syscalls verified OK!"); println!("\nAll syscalls verified OK!");

View File

@@ -44,7 +44,9 @@ impl PluginBridge for HermesPlugin {
&self, &self,
_req: Request<PluginInfo>, _req: Request<PluginInfo>,
) -> std::result::Result<Response<RegisterResponse>, Status> { ) -> std::result::Result<Response<RegisterResponse>, Status> {
Err(Status::unimplemented("hermes plugin is not an agentsd bridge")) Err(Status::unimplemented(
"hermes plugin is not an agentsd bridge",
))
} }
async fn call( async fn call(
@@ -62,13 +64,16 @@ impl PluginBridge for HermesPlugin {
Ok(Response::new(response)) Ok(Response::new(response))
} }
type StreamCallStream = tokio_stream::wrappers::ReceiverStream<std::result::Result<CallResponse, Status>>; type StreamCallStream =
tokio_stream::wrappers::ReceiverStream<std::result::Result<CallResponse, Status>>;
async fn stream_call( async fn stream_call(
&self, &self,
_req: Request<CallRequest>, _req: Request<CallRequest>,
) -> std::result::Result<Response<Self::StreamCallStream>, Status> { ) -> std::result::Result<Response<Self::StreamCallStream>, Status> {
Err(Status::unimplemented("stream_call is not implemented for hermes")) Err(Status::unimplemented(
"stream_call is not implemented for hermes",
))
} }
async fn heartbeat( async fn heartbeat(
@@ -110,7 +115,7 @@ async fn main() -> Result<()> {
println!("Hermes PluginBridge server listening on {}", listen_addr); println!("Hermes PluginBridge server listening on {}", listen_addr);
Server::builder() Server::builder()
.add_service(PluginBridgeServer::new(HermesPlugin::default())) .add_service(PluginBridgeServer::new(HermesPlugin))
.serve_with_incoming(incoming) .serve_with_incoming(incoming)
.await?; .await?;
Ok(()) Ok(())

View File

@@ -59,6 +59,7 @@ pub fn plugin_info(
} }
} }
#[allow(clippy::too_many_arguments)]
pub fn plugin_info_with_endpoint( pub fn plugin_info_with_endpoint(
id: &str, id: &str,
name: &str, name: &str,
@@ -95,7 +96,11 @@ pub fn encode_json<T: Serialize>(value: &T) -> Result<Vec<u8>> {
serde_json::to_vec(value).context("encode JSON") serde_json::to_vec(value).context("encode JSON")
} }
pub fn call_response_json<T: Serialize>(value: &T, ok: bool, error: impl Into<String>) -> CallResponse { pub fn call_response_json<T: Serialize>(
value: &T,
ok: bool,
error: impl Into<String>,
) -> CallResponse {
match encode_json(value) { match encode_json(value) {
Ok(result) => CallResponse { Ok(result) => CallResponse {
result, result,