use crate::types::*;
use serde::{Serialize, Deserialize};
#[cfg(target_arch = "wasm32")]
pub(crate) mod raw {
#[link(wasm_import_module = "env")]
extern "C" {
pub fn history_get(ptr: u32, len: u32) -> u64;
pub fn prompt_complete(ptr: u32, len: u32) -> u64;
pub fn env_get(ptr: u32, len: u32) -> u64;
pub fn fs_read(ptr: u32, len: u32) -> u64;
pub fn fs_write(ptr: u32, len: u32) -> u64;
pub fn fs_create(ptr: u32, len: u32) -> u64;
pub fn fs_delete(ptr: u32, len: u32) -> u64;
pub fn fs_mkdir(ptr: u32, len: u32) -> u64;
pub fn fs_ls(ptr: u32, len: u32) -> u64;
pub fn fs_chmod(ptr: u32, len: u32) -> u64;
pub fn fs_read_lines(ptr: u32, len: u32) -> u64;
pub fn fs_grep(ptr: u32, len: u32) -> u64;
pub fn fs_glob(ptr: u32, len: u32) -> u64;
pub fn http_get(ptr: u32, len: u32) -> u64;
pub fn http_post(ptr: u32, len: u32) -> u64;
pub fn http_request(ptr: u32, len: u32) -> u64;
pub fn ws_connect(ptr: u32, len: u32) -> u64;
pub fn ws_send(ptr: u32, len: u32) -> u64;
pub fn ws_close(ptr: u32, len: u32) -> u64;
pub fn tcp_connect(ptr: u32, len: u32) -> u64;
pub fn tcp_connect_tls(ptr: u32, len: u32) -> u64;
pub fn tcp_set_callback(ptr: u32, len: u32) -> u64;
pub fn tcp_write(ptr: u32, len: u32) -> u64;
pub fn tcp_close(ptr: u32, len: u32) -> u64;
pub fn tcp_request(ptr: u32, len: u32) -> u64;
pub fn shell_exec(ptr: u32, len: u32) -> u64;
pub fn log_write(ptr: u32, len: u32) -> u64;
pub fn sys_info(ptr: u32, len: u32) -> u64;
pub fn time_now(ptr: u32, len: u32) -> u64;
pub fn disk_usage(ptr: u32, len: u32) -> u64;
pub fn fs_allowed_dirs(ptr: u32, len: u32) -> u64;
pub fn fs_copy(ptr: u32, len: u32) -> u64;
pub fn archive_pack(ptr: u32, len: u32) -> u64;
pub fn archive_unpack(ptr: u32, len: u32) -> u64;
pub fn archive_list(ptr: u32, len: u32) -> u64;
pub fn file_transfer_send(ptr: u32, len: u32) -> u64;
pub fn file_transfer_recv(ptr: u32, len: u32) -> u64;
pub fn cluster_node_list(ptr: u32, len: u32) -> u64;
}
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) mod raw {
macro_rules! stub {
($($name:ident),* $(,)?) => {
$(pub unsafe extern "C" fn $name(_ptr: u32, _len: u32) -> u64 {
panic!(concat!(stringify!($name), " called outside WASM"))
})*
};
}
stub!(
history_get, prompt_complete, env_get,
fs_read, fs_write, fs_create, fs_delete, fs_mkdir,
fs_ls, fs_chmod, fs_read_lines, fs_grep, fs_glob, fs_copy,
fs_allowed_dirs,
http_get, http_post, http_request,
ws_connect, ws_send, ws_close,
tcp_connect, tcp_connect_tls, tcp_set_callback, tcp_write, tcp_close, tcp_request,
shell_exec,
log_write,
sys_info, time_now, disk_usage,
archive_pack, archive_unpack, archive_list,
file_transfer_send, file_transfer_recv, cluster_node_list,
);
}
pub(crate) fn call_host<S, R>(
host_fn: unsafe extern "C" fn(u32, u32) -> u64,
value: &S,
) -> Result<R, SkillError>
where
S: serde::Serialize,
R: serde::de::DeserializeOwned,
{
let encoded = rmp_serde::to_vec_named(value)?;
let result = unsafe { host_fn(encoded.as_ptr() as u32, encoded.len() as u32) };
let bytes = unpack_result(result);
#[derive(serde::Deserialize)]
struct ErrorCheck {
#[serde(default)]
error: String,
}
if let Ok(check) = rmp_serde::from_slice::<ErrorCheck>(&bytes) {
if !check.error.is_empty() {
return Err(SkillError(check.error));
}
}
Ok(rmp_serde::from_slice(&bytes)?)
}
fn unpack_result(packed: u64) -> Vec<u8> {
let ptr = (packed >> 32) as u32;
let len = (packed & 0xFFFF_FFFF) as u32;
if len == 0 {
return Vec::new();
}
unsafe { std::slice::from_raw_parts(ptr as *const u8, len as usize).to_vec() }
}
pub fn history_get(filter: &str) -> Result<Vec<HistoryMessage>, SkillError> {
#[derive(serde::Serialize)]
struct Req<'a> { filter: &'a str }
call_host(raw::history_get, &Req { filter })
}
pub fn prompt_complete(req: PromptRequest) -> Result<PromptResponse, SkillError> {
let resp: PromptResponse = call_host(raw::prompt_complete, &req)?;
if !resp.error.is_empty() {
return Err(SkillError(resp.error));
}
Ok(resp)
}
pub fn get_env(key: &str) -> String {
#[derive(serde::Serialize)]
struct Req<'a> { key: &'a str }
#[derive(serde::Deserialize)]
struct Resp { value: String }
call_host::<_, Resp>(raw::env_get, &Req { key })
.map(|r| r.value)
.unwrap_or_default()
}
pub fn unmarshal_batch_result(payload: &[u8]) -> Result<BatchResult, SkillError> {
Ok(rmp_serde::from_slice(payload)?)
}
pub fn fs_read(path: &str) -> Result<Vec<u8>, SkillError> {
let resp: FsResponse = call_host(raw::fs_read, &FsRequest { path: path.into(), content: vec![] })?;
check_fs_error(resp.error)?;
Ok(resp.content)
}
pub fn fs_write(path: &str, content: Vec<u8>) -> Result<(), SkillError> {
let resp: FsResponse = call_host(raw::fs_write, &FsRequest { path: path.into(), content })?;
check_fs_error(resp.error)
}
pub fn fs_create(path: &str, content: Vec<u8>) -> Result<(), SkillError> {
let resp: FsResponse = call_host(raw::fs_create, &FsRequest { path: path.into(), content })?;
check_fs_error(resp.error)
}
pub fn fs_delete(path: &str) -> Result<(), SkillError> {
let resp: FsResponse = call_host(raw::fs_delete, &FsRequest { path: path.into(), content: vec![] })?;
check_fs_error(resp.error)
}
pub fn fs_mkdir(path: &str, recursive: bool) -> Result<(), SkillError> {
let resp: FsResponse = call_host(raw::fs_mkdir, &FsMkdirRequest { path: path.into(), recursive })?;
check_fs_error(resp.error)
}
pub fn fs_ls(path: &str, long: bool) -> Result<Vec<DirEntry>, SkillError> {
let resp: FsLsResponse = call_host(raw::fs_ls, &FsLsRequest { path: path.into(), long })?;
check_fs_error(resp.error)?;
Ok(resp.entries)
}
pub fn fs_chmod(path: &str, mode: u32, recursive: bool) -> Result<(), SkillError> {
let resp: FsResponse = call_host(raw::fs_chmod, &FsChmodRequest { path: path.into(), mode, recursive })?;
check_fs_error(resp.error)
}
pub fn fs_read_lines(req: FsReadLinesRequest) -> Result<TextFileContent, SkillError> {
let resp: TextFileContentResponse = call_host(raw::fs_read_lines, &req)?;
if !resp.error.is_empty() {
return Err(SkillError(resp.error));
}
Ok(TextFileContent {
lines: resp.lines,
total_lines: resp.total_lines,
offset: resp.offset,
is_truncated: resp.is_truncated,
})
}
pub fn fs_grep(opts: GrepOptions) -> Result<Vec<GrepFileMatch>, SkillError> {
let resp: GrepResponse = call_host(raw::fs_grep, &opts)?;
if !resp.error.is_empty() {
return Err(SkillError(resp.error));
}
Ok(resp.matches)
}
pub fn fs_glob(opts: GlobOptions) -> Result<Vec<GlobEntry>, SkillError> {
let resp: GlobResponse = call_host(raw::fs_glob, &opts)?;
if !resp.error.is_empty() {
return Err(SkillError(resp.error));
}
Ok(resp.matches)
}
pub fn tcp_request(opts: &TcpRequestOptions) -> Result<TcpRequestResult, SkillError> {
let resp: TcpRequestResult = call_host(raw::tcp_request, opts)?;
if !resp.error.is_empty() {
return Err(SkillError(resp.error));
}
Ok(resp)
}
pub fn shell_exec(req: &ShellExecRequest) -> Result<ShellExecResult, SkillError> {
let resp: ShellExecResult = call_host(raw::shell_exec, req)?;
if !resp.error.is_empty() {
return Err(SkillError(resp.error));
}
Ok(resp)
}
pub fn log_msg(level: &str, message: &str, fields: &[(&str, &str)]) {
use std::collections::HashMap;
let mut f: HashMap<&str, &str> = HashMap::new();
for (k, v) in fields {
f.insert(k, v);
}
#[derive(serde::Serialize)]
struct Req<'a> {
level: &'a str,
message: &'a str,
fields: HashMap<&'a str, &'a str>,
}
#[derive(serde::Deserialize)]
struct Empty {}
let _ = call_host::<_, Empty>(raw::log_write, &Req { level, message, fields: f });
}
pub fn log_debug(message: &str, fields: &[(&str, &str)]) { log_msg("debug", message, fields); }
pub fn log_info(message: &str, fields: &[(&str, &str)]) { log_msg("info", message, fields); }
pub fn log_warn(message: &str, fields: &[(&str, &str)]) { log_msg("warn", message, fields); }
pub fn log_error(message: &str, fields: &[(&str, &str)]) { log_msg("error", message, fields); }
pub fn sys_info() -> Result<SysInfoResult, SkillError> {
#[derive(serde::Serialize)]
struct Empty {}
call_host(raw::sys_info, &Empty {})
}
pub fn time_now() -> Result<TimeNowResult, SkillError> {
#[derive(serde::Serialize)]
struct Empty {}
call_host(raw::time_now, &Empty {})
}
pub fn disk_usage(path: &str) -> Result<DiskUsageResult, SkillError> {
#[derive(serde::Serialize)]
struct Req<'a> { path: &'a str }
let resp: DiskUsageResult = call_host(raw::disk_usage, &Req { path })?;
if !resp.error.is_empty() {
return Err(SkillError(resp.error));
}
Ok(resp)
}
pub fn fs_allowed_dirs() -> Result<Vec<AllowedDir>, SkillError> {
#[derive(serde::Serialize)]
struct Empty {}
#[derive(serde::Deserialize)]
struct Resp {
#[serde(default)]
dirs: Vec<AllowedDir>,
#[serde(default)]
error: String,
}
let resp: Resp = call_host(raw::fs_allowed_dirs, &Empty {})?;
if !resp.error.is_empty() {
return Err(SkillError(resp.error));
}
Ok(resp.dirs)
}
pub fn fs_copy(source: &str, dest: &str) -> Result<(), SkillError> {
#[derive(Serialize)]
struct Req { source: String, dest: String }
let resp: FsResponse = call_host(raw::fs_copy, &Req {
source: source.into(), dest: dest.into(),
})?;
check_fs_error(resp.error)
}
#[derive(Deserialize, Default)]
pub struct ArchivePackResult {
#[serde(default)]
pub files_count: i64,
#[serde(default)]
pub format: String,
#[serde(default)]
pub error: String,
}
pub fn archive_pack(source: &str, output: &str, format: &str, include: &str, exclude: &str) -> Result<ArchivePackResult, SkillError> {
#[derive(Serialize)]
struct Req { source: String, output: String, format: String, include: String, exclude: String }
let resp: ArchivePackResult = call_host(raw::archive_pack, &Req {
source: source.into(), output: output.into(), format: format.into(),
include: include.into(), exclude: exclude.into(),
})?;
if !resp.error.is_empty() { return Err(SkillError(resp.error)); }
Ok(resp)
}
#[derive(Deserialize, Default)]
pub struct ArchiveEntry {
#[serde(default)]
pub name: String,
#[serde(default)]
pub size: i64,
#[serde(default)]
pub is_dir: bool,
}
pub fn archive_list(archive: &str, format: &str, include: &str, exclude: &str) -> Result<Vec<ArchiveEntry>, SkillError> {
#[derive(Serialize)]
struct Req { archive: String, format: String, include: String, exclude: String }
#[derive(Deserialize, Default)]
struct Resp { #[serde(default)] entries: Vec<ArchiveEntry>, #[serde(default)] error: String }
let resp: Resp = call_host(raw::archive_list, &Req {
archive: archive.into(), format: format.into(),
include: include.into(), exclude: exclude.into(),
})?;
if !resp.error.is_empty() { return Err(SkillError(resp.error)); }
Ok(resp.entries)
}
pub fn archive_unpack(archive: &str, dest: &str, format: &str, include: &str, exclude: &str, strip: i32) -> Result<i64, SkillError> {
#[derive(Serialize)]
struct Req { archive: String, dest: String, format: String, include: String, exclude: String, strip: i32 }
#[derive(Deserialize, Default)]
struct Resp { #[serde(default)] files_count: i64, #[serde(default)] error: String }
let resp: Resp = call_host(raw::archive_unpack, &Req {
archive: archive.into(), dest: dest.into(), format: format.into(),
include: include.into(), exclude: exclude.into(), strip,
})?;
if !resp.error.is_empty() { return Err(SkillError(resp.error)); }
Ok(resp.files_count)
}
pub fn file_transfer_send(target_node: &str, local_path: &str, remote_path: &str) -> Result<(), SkillError> {
#[derive(Serialize)]
struct Req { target_node: String, local_path: String, remote_path: String }
#[derive(Deserialize, Default)]
struct Resp { #[serde(default)] error: String }
let resp: Resp = call_host(raw::file_transfer_send, &Req {
target_node: target_node.into(), local_path: local_path.into(), remote_path: remote_path.into(),
})?;
if !resp.error.is_empty() { return Err(SkillError(resp.error)); }
Ok(())
}
pub fn file_transfer_recv(source_node: &str, remote_path: &str, local_path: &str) -> Result<(), SkillError> {
#[derive(Serialize)]
struct Req { source_node: String, remote_path: String, local_path: String }
#[derive(Deserialize, Default)]
struct Resp { #[serde(default)] error: String }
let resp: Resp = call_host(raw::file_transfer_recv, &Req {
source_node: source_node.into(), remote_path: remote_path.into(), local_path: local_path.into(),
})?;
if !resp.error.is_empty() { return Err(SkillError(resp.error)); }
Ok(())
}
#[derive(Deserialize, Default)]
pub struct ClusterNodeInfo {
#[serde(default)]
pub node_id: String,
#[serde(default)]
pub role: String,
#[serde(default)]
pub skills: Vec<String>,
}
pub fn cluster_node_list() -> Result<(String, Vec<ClusterNodeInfo>), SkillError> {
#[derive(Deserialize, Default)]
struct Resp {
#[serde(default)]
current_node: String,
#[serde(default)]
nodes: Vec<ClusterNodeInfo>,
#[serde(default)]
error: String,
}
#[derive(Serialize)]
struct Empty {}
let resp: Resp = call_host(raw::cluster_node_list, &Empty {})?;
if !resp.error.is_empty() { return Err(SkillError(resp.error)); }
Ok((resp.current_node, resp.nodes))
}
fn check_fs_error(err: String) -> Result<(), SkillError> {
if err.is_empty() { Ok(()) } else { Err(SkillError(err)) }
}