From 949ad2a989a6076f747fd6beb7050eac6338b06e Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Wed, 15 Apr 2026 16:01:54 -0400 Subject: [PATCH 01/23] feat(sync-plugin): scaffold plugin sync step with Component Model interface - Add `type: plugin` as a new sync step type in canister.yaml (path/url/sha256/dirs fields) - New `crates/icp-sync-plugin` crate: sandbox path enforcement implemented and tested; runtime stub pending wasmtime Component Model implementation - Wire SyncStep::Plugin through manifest adapter, syncer, deploy and sync commands - Define plugin interface in sync-plugin/sync-plugin.wit (WIT / Component Model) - Add design.md and plan.md in sync-plugin/ - Add POC plugin skeleton in sync-plugin/poc/ - Update JSON schemas Co-Authored-By: Claude Sonnet 4.6 --- Cargo.lock | 14 + Cargo.toml | 3 +- crates/icp-cli/src/commands/deploy.rs | 9 +- crates/icp-cli/src/commands/sync.rs | 9 +- crates/icp-cli/src/operations/sync.rs | 17 +- crates/icp-sync-plugin/Cargo.toml | 21 ++ crates/icp-sync-plugin/src/lib.rs | 4 + crates/icp-sync-plugin/src/runtime.rs | 32 ++ crates/icp-sync-plugin/src/sandbox.rs | 89 +++++ crates/icp/Cargo.toml | 1 + crates/icp/src/canister/sync/mod.rs | 13 + crates/icp/src/canister/sync/plugin.rs | 163 ++++++++ crates/icp/src/manifest/adapter/mod.rs | 3 +- crates/icp/src/manifest/adapter/plugin.rs | 100 +++++ crates/icp/src/manifest/adapter/prebuilt.rs | 3 +- crates/icp/src/manifest/canister.rs | 96 +++++ docs/schemas/canister-yaml-schema.json | 49 ++- docs/schemas/icp-yaml-schema.json | 49 ++- sync-plugin/design.md | 331 ++++++++++++++++ sync-plugin/plan.md | 359 ++++++++++++++++++ sync-plugin/poc/.gitignore | 1 + sync-plugin/poc/Cargo.lock | 398 ++++++++++++++++++++ sync-plugin/poc/Cargo.toml | 13 + sync-plugin/poc/src/lib.rs | 2 + sync-plugin/sync-plugin.wit | 69 ++++ 25 files changed, 1838 insertions(+), 10 deletions(-) create mode 100644 crates/icp-sync-plugin/Cargo.toml create mode 100644 crates/icp-sync-plugin/src/lib.rs create mode 100644 crates/icp-sync-plugin/src/runtime.rs create mode 100644 crates/icp-sync-plugin/src/sandbox.rs create mode 100644 crates/icp/src/canister/sync/plugin.rs create mode 100644 crates/icp/src/manifest/adapter/plugin.rs create mode 100644 sync-plugin/design.md create mode 100644 sync-plugin/plan.md create mode 100644 sync-plugin/poc/.gitignore create mode 100644 sync-plugin/poc/Cargo.lock create mode 100644 sync-plugin/poc/Cargo.toml create mode 100644 sync-plugin/poc/src/lib.rs create mode 100644 sync-plugin/sync-plugin.wit diff --git a/Cargo.lock b/Cargo.lock index 44bb5a9dc..08749c3ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3329,6 +3329,7 @@ dependencies = [ "ic-management-canister-types 0.7.1", "ic-utils", "icp-canister-interfaces", + "icp-sync-plugin", "icrc-ledger-types", "indoc", "itertools 0.14.0", @@ -3454,6 +3455,19 @@ dependencies = [ "wslpath2", ] +[[package]] +name = "icp-sync-plugin" +version = "0.2.3" +dependencies = [ + "camino", + "candid", + "candid_parser", + "hex", + "ic-agent", + "snafu", + "tokio", +] + [[package]] name = "icrc-cbor" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index b2a08a2d4..2ead0f454 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ members = ["crates/*"] default-members = ["crates/icp-cli"] -exclude = ["examples/icp-rust", "examples/icp-rust-recipe"] +exclude = ["examples/icp-rust", "examples/icp-rust-recipe", "sync-plugin/poc"] resolver = "3" [workspace.package] @@ -54,6 +54,7 @@ ic-management-canister-types = { version = "0.7.1" } ic-utils = { version = "0.47.0" } icp = { path = "crates/icp" } icp-canister-interfaces = { path = "crates/icp-canister-interfaces" } +icp-sync-plugin = { path = "crates/icp-sync-plugin" } ic-identity-hsm = "0.47.0" icrc-ledger-types = "0.1.10" indicatif = "0.18.0" diff --git a/crates/icp-cli/src/commands/deploy.rs b/crates/icp-cli/src/commands/deploy.rs index deb36de88..bb799fc2e 100644 --- a/crates/icp-cli/src/commands/deploy.rs +++ b/crates/icp-cli/src/commands/deploy.rs @@ -362,7 +362,14 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow: // method to permit the user identity to upload assets directly before syncing. info!("Syncing canisters:"); - sync_many(ctx.syncer.clone(), agent.clone(), sync_canisters, ctx.debug).await?; + sync_many( + ctx.syncer.clone(), + agent.clone(), + sync_canisters, + environment_selection.name().to_owned(), + ctx.debug, + ) + .await?; } // Print URLs for deployed canisters diff --git a/crates/icp-cli/src/commands/sync.rs b/crates/icp-cli/src/commands/sync.rs index 7e4663285..e662ac2cd 100644 --- a/crates/icp-cli/src/commands/sync.rs +++ b/crates/icp-cli/src/commands/sync.rs @@ -77,7 +77,14 @@ pub(crate) async fn exec(ctx: &Context, args: &SyncArgs) -> Result<(), anyhow::E info!("Syncing canisters:"); - sync_many(ctx.syncer.clone(), agent, sync_canisters, ctx.debug).await?; + sync_many( + ctx.syncer.clone(), + agent, + sync_canisters, + environment_selection.name().to_owned(), + ctx.debug, + ) + .await?; Ok(()) } diff --git a/crates/icp-cli/src/operations/sync.rs b/crates/icp-cli/src/operations/sync.rs index feb407055..f14c81e4f 100644 --- a/crates/icp-cli/src/operations/sync.rs +++ b/crates/icp-cli/src/operations/sync.rs @@ -32,6 +32,7 @@ async fn sync_canister( canister_path: PathBuf, canister_id: Principal, canister_info: &Canister, + environment: &str, pb: &mut MultiStepProgressBar, ) -> Result<(), SynchronizeError> { let step_count = canister_info.sync.steps.len(); @@ -50,6 +51,7 @@ async fn sync_canister( &Params { path: canister_path.clone(), cid: canister_id, + environment: environment.to_owned(), }, agent, Some(tx), @@ -70,6 +72,7 @@ pub(crate) async fn sync_many( syncer: Arc, agent: Agent, canisters: Vec<(Principal, PathBuf, Canister)>, + environment: String, debug: bool, ) -> Result<(), SyncOperationError> { let mut futs = FuturesOrdered::new(); @@ -81,12 +84,20 @@ pub(crate) async fn sync_many( let fut = { let agent = agent.clone(); let syncer = syncer.clone(); + let environment = environment.clone(); async move { // Define the sync logic - let sync_result = - sync_canister(&syncer, &agent, canister_path, cid, &canister_info, &mut pb) - .await; + let sync_result = sync_canister( + &syncer, + &agent, + canister_path, + cid, + &canister_info, + &environment, + &mut pb, + ) + .await; // Execute with progress tracking for final state let result = ProgressManager::execute_with_progress( diff --git a/crates/icp-sync-plugin/Cargo.toml b/crates/icp-sync-plugin/Cargo.toml new file mode 100644 index 000000000..1d2495877 --- /dev/null +++ b/crates/icp-sync-plugin/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "icp-sync-plugin" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +publish.workspace = true + +[dependencies] +camino.workspace = true +candid.workspace = true +candid_parser.workspace = true +hex.workspace = true +ic-agent.workspace = true +snafu.workspace = true +tokio.workspace = true +# wasmtime with component-model feature — added during implementation +# wasmtime = { version = "...", features = ["component-model"] } + +[lints] +workspace = true diff --git a/crates/icp-sync-plugin/src/lib.rs b/crates/icp-sync-plugin/src/lib.rs new file mode 100644 index 000000000..00a40be12 --- /dev/null +++ b/crates/icp-sync-plugin/src/lib.rs @@ -0,0 +1,4 @@ +mod runtime; +mod sandbox; + +pub use runtime::{RunPluginError, run_plugin}; diff --git a/crates/icp-sync-plugin/src/runtime.rs b/crates/icp-sync-plugin/src/runtime.rs new file mode 100644 index 000000000..fcafb76a2 --- /dev/null +++ b/crates/icp-sync-plugin/src/runtime.rs @@ -0,0 +1,32 @@ +// Runtime implementation — to be written using wasmtime::component. +// See sync-plugin/sync-plugin.wit for the interface definition. + +use camino::Utf8PathBuf; +use candid::Principal; +use ic_agent::Agent; +use snafu::prelude::*; +use tokio::sync::mpsc::Sender; + +#[derive(Debug, Snafu)] +pub enum RunPluginError { + #[snafu(display("failed to load wasm component from {path}"))] + LoadComponent { path: Utf8PathBuf }, + + #[snafu(display("failed to call exec() on plugin at {path}"))] + CallExec { path: Utf8PathBuf }, + + #[snafu(display("plugin returned error: {message}"))] + PluginFailed { message: String }, +} + +pub fn run_plugin( + _wasm_path: Utf8PathBuf, + _base_dir: Utf8PathBuf, + _allowed_dirs: Vec, + _target_canister_id: Principal, + _agent: Agent, + _environment: String, + _stdio: Option>, +) -> Result<(), RunPluginError> { + unimplemented!("sync plugin runtime: migration to wasmtime Component Model in progress") +} diff --git a/crates/icp-sync-plugin/src/sandbox.rs b/crates/icp-sync-plugin/src/sandbox.rs new file mode 100644 index 000000000..9742d45dc --- /dev/null +++ b/crates/icp-sync-plugin/src/sandbox.rs @@ -0,0 +1,89 @@ +use camino::{Utf8Path, Utf8PathBuf}; + +/// Returns `true` iff `path` (already canonicalized) starts with at least one +/// of the `allowed_dirs`. +pub fn is_path_allowed(path: &Utf8Path, allowed_dirs: &[Utf8PathBuf]) -> bool { + allowed_dirs.iter().any(|dir| path.starts_with(dir)) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn dirs(paths: &[&str]) -> Vec { + paths.iter().map(|p| Utf8PathBuf::from(*p)).collect() + } + + fn path(s: &str) -> Utf8PathBuf { + Utf8PathBuf::from(s) + } + + #[test] + fn allowed_exact_dir() { + let allowed = dirs(&["/project/canister/assets"]); + assert!(is_path_allowed( + path("/project/canister/assets").as_path(), + &allowed + )); + } + + #[test] + fn allowed_file_inside_dir() { + let allowed = dirs(&["/project/canister/assets"]); + assert!(is_path_allowed( + path("/project/canister/assets/data.txt").as_path(), + &allowed + )); + } + + #[test] + fn allowed_nested_file() { + let allowed = dirs(&["/project/canister/assets"]); + assert!(is_path_allowed( + path("/project/canister/assets/subdir/data.txt").as_path(), + &allowed + )); + } + + #[test] + fn denied_outside_dir() { + let allowed = dirs(&["/project/canister/assets"]); + assert!(!is_path_allowed( + path("/project/canister/other/data.txt").as_path(), + &allowed + )); + } + + #[test] + fn denied_parent_traversal_attempt() { + // A path that looks like it goes outside — canonicalization in the + // host prevents this from reaching is_path_allowed in practice, but + // verify we handle an already-resolved traversal correctly. + let allowed = dirs(&["/project/canister/assets"]); + assert!(!is_path_allowed(path("/etc/passwd").as_path(), &allowed)); + } + + #[test] + fn denied_sibling_prefix_match() { + // "/project/canister/assets-other" must NOT be allowed just because + // "/project/canister/assets" is in the list. + let allowed = dirs(&["/project/canister/assets"]); + assert!(!is_path_allowed( + path("/project/canister/assets-other/file.txt").as_path(), + &allowed + )); + } + + #[test] + fn multiple_allowed_dirs() { + let allowed = dirs(&["/project/canister/assets", "/project/canister/config"]); + assert!(is_path_allowed( + path("/project/canister/config/settings.json").as_path(), + &allowed + )); + assert!(!is_path_allowed( + path("/project/canister/private/secret.key").as_path(), + &allowed + )); + } +} diff --git a/crates/icp/Cargo.toml b/crates/icp/Cargo.toml index 66809c58c..e21411b8b 100644 --- a/crates/icp/Cargo.toml +++ b/crates/icp/Cargo.toml @@ -34,6 +34,7 @@ ic-ledger-types = { workspace = true } ic-management-canister-types = { workspace = true } ic-utils = { workspace = true } icp-canister-interfaces = { workspace = true } +icp-sync-plugin = { workspace = true } icrc-ledger-types = { workspace = true } indoc = { workspace = true } itertools = { workspace = true } diff --git a/crates/icp/src/canister/sync/mod.rs b/crates/icp/src/canister/sync/mod.rs index adc06ec69..d2b8ccb5c 100644 --- a/crates/icp/src/canister/sync/mod.rs +++ b/crates/icp/src/canister/sync/mod.rs @@ -8,11 +8,15 @@ use crate::manifest::canister::SyncStep; use crate::prelude::*; mod assets; +mod plugin; mod script; pub struct Params { pub path: PathBuf, pub cid: Principal, + /// Name of the environment being synced (e.g. "local", "production"). + /// Passed to sync plugin steps via `SyncExecInput`. + pub environment: String, } #[derive(Debug, Snafu)] @@ -22,6 +26,9 @@ pub enum SynchronizeError { #[snafu(transparent)] Assets { source: assets::AssetsError }, + + #[snafu(transparent)] + Plugin { source: plugin::PluginError }, } #[async_trait] @@ -49,6 +56,12 @@ impl Synchronize for Syncer { match step { SyncStep::Assets(adapter) => Ok(assets::sync(adapter, params, agent).await?), SyncStep::Script(adapter) => Ok(script::sync(adapter, params, stdio).await?), + SyncStep::Plugin(adapter) => { + Ok( + plugin::sync(adapter, params, agent, ¶ms.environment.clone(), stdio) + .await?, + ) + } } } } diff --git a/crates/icp/src/canister/sync/plugin.rs b/crates/icp/src/canister/sync/plugin.rs new file mode 100644 index 000000000..a0aacb599 --- /dev/null +++ b/crates/icp/src/canister/sync/plugin.rs @@ -0,0 +1,163 @@ +use camino::Utf8PathBuf; +use ic_agent::Agent; +use icp_sync_plugin::{RunPluginError, run_plugin}; +use reqwest::{Client, Method, Request}; +use sha2::{Digest, Sha256}; +use snafu::prelude::*; +use tokio::sync::mpsc::Sender; +use url::Url; + +use crate::{ + fs::{read, write}, + manifest::adapter::{plugin::Adapter, prebuilt::SourceField}, +}; + +use super::Params; + +#[derive(Debug, Snafu)] +pub enum PluginError { + #[snafu(display("failed to read plugin wasm file"))] + ReadWasm { source: crate::fs::IoError }, + + #[snafu(display("failed to parse plugin url"))] + ParseUrl { source: url::ParseError }, + + #[snafu(display("failed to fetch plugin wasm file"))] + HttpRequest { source: reqwest::Error }, + + #[snafu(display("http request failed: {status}"))] + HttpStatus { status: reqwest::StatusCode }, + + #[snafu(display("failed to read http response for plugin"))] + HttpResponse { source: reqwest::Error }, + + #[snafu(display("failed to write downloaded plugin wasm to temp file"))] + WriteTempWasm { source: crate::fs::IoError }, + + #[snafu(display("plugin wasm checksum mismatch, expected: {expected}, actual: {actual}"))] + ChecksumMismatch { expected: String, actual: String }, + + #[snafu(display("failed to canonicalize allowed dir '{dir}'"))] + CanonicalizeDirs { source: std::io::Error, dir: String }, + + #[snafu(display("failed to run plugin"))] + Run { source: RunPluginError }, + + #[snafu(display("failed to send log message"))] + Log { + source: tokio::sync::mpsc::error::SendError, + }, +} + +pub(super) async fn sync( + adapter: &Adapter, + params: &Params, + agent: &Agent, + environment: &str, + stdio: Option>, +) -> Result<(), PluginError> { + // 1. Acquire the wasm bytes — either from a local path or a remote URL. + let (wasm_bytes, wasm_path) = match &adapter.source { + SourceField::Local(s) => { + let full_path = params.path.join(&s.path); + if let Some(tx) = &stdio { + tx.send(format!("Reading plugin wasm: {full_path}")) + .await + .context(LogSnafu)?; + } + let bytes = read(full_path.as_ref()).context(ReadWasmSnafu)?; + (bytes, full_path) + } + + SourceField::Remote(s) => { + let url = Url::parse(&s.url).context(ParseUrlSnafu)?; + if let Some(tx) = &stdio { + tx.send(format!("Fetching plugin wasm: {url}")) + .await + .context(LogSnafu)?; + } + let client = Client::new(); + let req = Request::new(Method::GET, url); + let resp = client.execute(req).await.context(HttpRequestSnafu)?; + let status = resp.status(); + if !status.is_success() { + return HttpStatusSnafu { status }.fail(); + } + let bytes = resp.bytes().await.context(HttpResponseSnafu)?.to_vec(); + + // Write to a temp file so we can pass a path to `run_plugin`. + let tmp_path = params.path.join(format!( + ".icp-plugin-{}.wasm", + hex::encode(&bytes[..std::cmp::min(8, bytes.len())]) + )); + write(tmp_path.as_ref(), &bytes).context(WriteTempWasmSnafu)?; + (bytes, tmp_path) + } + }; + + // 2. Verify sha256 checksum if provided. + let cksum = hex::encode({ + let mut h = Sha256::new(); + h.update(&wasm_bytes); + h.finalize() + }); + + if let Some(expected) = &adapter.sha256 { + if let Some(tx) = &stdio { + tx.send("Verifying plugin wasm checksum".to_string()) + .await + .context(LogSnafu)?; + } + if &cksum != expected { + return ChecksumMismatchSnafu { + expected: expected.clone(), + actual: cksum, + } + .fail(); + } + } + + // 3. Canonicalize declared dirs relative to the canister directory. + let base_dir = Utf8PathBuf::from(params.path.as_str()); + let allowed_dirs: Vec = adapter + .dirs + .as_deref() + .unwrap_or(&[]) + .iter() + .map(|d| { + let abs = params.path.join(d); + std::fs::canonicalize(abs.as_std_path()) + .context(CanonicalizeDirsSnafu { dir: d.clone() }) + .map(|p| { + Utf8PathBuf::from_path_buf(p) + .unwrap_or_else(|p| Utf8PathBuf::from(p.to_string_lossy().as_ref())) + }) + }) + .collect::, _>>()?; + + // 4. Run the plugin (blocking call — signal Tokio that this thread will block). + let wasm_path_buf = Utf8PathBuf::from(wasm_path.as_str()); + let agent_clone = agent.clone(); + let environment_owned = environment.to_owned(); + let stdio_clone = stdio.clone(); + + tokio::task::block_in_place(|| { + run_plugin( + wasm_path_buf, + base_dir, + allowed_dirs, + params.cid, + agent_clone, + environment_owned, + stdio_clone, + ) + }) + .context(RunSnafu)?; + + // Clean up temp file if we downloaded from a remote URL. + if matches!(&adapter.source, SourceField::Remote(_)) { + let _ = std::fs::remove_file(wasm_path.as_std_path()); + } + + Ok(()) +} diff --git a/crates/icp/src/manifest/adapter/mod.rs b/crates/icp/src/manifest/adapter/mod.rs index 99c66b732..5b29639f1 100644 --- a/crates/icp/src/manifest/adapter/mod.rs +++ b/crates/icp/src/manifest/adapter/mod.rs @@ -1,3 +1,4 @@ pub mod assets; -pub mod prebuilt; +pub mod plugin; pub mod script; +pub mod prebuilt; diff --git a/crates/icp/src/manifest/adapter/plugin.rs b/crates/icp/src/manifest/adapter/plugin.rs new file mode 100644 index 000000000..dd52077b9 --- /dev/null +++ b/crates/icp/src/manifest/adapter/plugin.rs @@ -0,0 +1,100 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use super::prebuilt::SourceField; + +/// Configuration for a sync plugin step. +/// +/// A sync plugin is a WebAssembly module invoked during `icp sync` for a +/// specific canister. It runs inside the Extism sandbox with restricted +/// permissions — it can only call canister methods on the canister being +/// synced and read files from the declared `dirs` allowlist. +/// +/// Example: +/// ```yaml +/// - type: plugin +/// path: ./plugins/populate-data.wasm +/// sha256: e3b0c44298fc1c149afb... # optional but recommended +/// dirs: # optional read-access directories +/// - assets/seed-data/ +/// ``` +#[derive(Clone, Debug, Deserialize, PartialEq, JsonSchema, Serialize)] +pub struct Adapter { + #[serde(flatten)] + pub source: SourceField, + + /// Optional sha256 checksum of the wasm file. + /// Required when `url` is used; optional (but recommended) for `path`. + pub sha256: Option, + + /// Directories (relative to canister directory) the plugin may read from. + pub dirs: Option>, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::manifest::adapter::prebuilt::{LocalSource, RemoteSource}; + + #[test] + fn local_path() { + assert_eq!( + serde_yaml::from_str::( + r#" + path: plugins/my-sync.wasm + "# + ) + .expect("failed to deserialize Adapter from yaml"), + Adapter { + source: SourceField::Local(LocalSource { + path: "plugins/my-sync.wasm".into(), + }), + sha256: None, + dirs: None, + }, + ); + } + + #[test] + fn local_path_with_sha256_and_dirs() { + assert_eq!( + serde_yaml::from_str::( + r#" + path: plugins/my-sync.wasm + sha256: abc123 + dirs: + - assets/seed-data/ + - config/ + "# + ) + .expect("failed to deserialize Adapter from yaml"), + Adapter { + source: SourceField::Local(LocalSource { + path: "plugins/my-sync.wasm".into(), + }), + sha256: Some("abc123".to_string()), + dirs: Some(vec!["assets/seed-data/".to_string(), "config/".to_string(),]), + }, + ); + } + + #[test] + fn remote_url_with_sha256() { + assert_eq!( + serde_yaml::from_str::( + r#" + url: https://example.com/plugins/migrate-v2.wasm + sha256: a665a45920422f9d417e + "# + ) + .expect("failed to deserialize Adapter from yaml"), + Adapter { + source: SourceField::Remote(RemoteSource { + url: "https://example.com/plugins/migrate-v2.wasm".to_string(), + }), + sha256: Some("a665a45920422f9d417e".to_string()), + dirs: None, + }, + ); + } +} diff --git a/crates/icp/src/manifest/adapter/prebuilt.rs b/crates/icp/src/manifest/adapter/prebuilt.rs index 599540a64..9974d75f1 100644 --- a/crates/icp/src/manifest/adapter/prebuilt.rs +++ b/crates/icp/src/manifest/adapter/prebuilt.rs @@ -27,7 +27,8 @@ pub enum SourceField { Remote(RemoteSource), } -/// Configuration for a Pre-built canister build adapter. +/// Configuration for a wasm source — used by adapters that load a `.wasm` file +/// either from a local path or from a remote URL. #[derive(Clone, Debug, Deserialize, PartialEq, JsonSchema, Serialize)] pub struct Adapter { #[serde(flatten)] diff --git a/crates/icp/src/manifest/canister.rs b/crates/icp/src/manifest/canister.rs index 6fafaf721..9ba8dfdfa 100644 --- a/crates/icp/src/manifest/canister.rs +++ b/crates/icp/src/manifest/canister.rs @@ -316,6 +316,11 @@ pub enum SyncStep { /// Represents syncing of an assets canister Assets(adapter::assets::Adapter), + + /// Represents a sync step executed by a WebAssembly plugin running inside + /// the Extism sandbox. The plugin can call canister methods on exactly + /// the canister being synced and read files from the declared `dirs`. + Plugin(adapter::plugin::Adapter), } impl fmt::Display for SyncStep { @@ -326,6 +331,13 @@ impl fmt::Display for SyncStep { match self { SyncStep::Script(v) => format!("script {v}"), SyncStep::Assets(v) => format!("assets {v}"), + SyncStep::Plugin(v) => { + let src = match &v.source { + adapter::prebuilt::SourceField::Local(l) => format!("path: {}", l.path), + adapter::prebuilt::SourceField::Remote(r) => format!("url: {}", r.url), + }; + format!("plugin {src}") + } } ) } @@ -722,6 +734,90 @@ mod tests { }; } + #[test] + fn sync_steps_plugin_local() { + assert_eq!( + validate_canister_yaml(indoc! {r#" + name: my-canister + build: + steps: + - type: script + command: dosomething.sh + sync: + steps: + - type: plugin + path: ./plugins/my-sync.wasm + dirs: + - assets/seed-data/ + "#}), + CanisterManifest { + name: "my-canister".to_string(), + settings: Settings::default(), + init_args: None, + instructions: Instructions::BuildSync { + build: BuildSteps { + steps: vec![BuildStep::Script(script::Adapter { + command: script::CommandField::Command("dosomething.sh".to_string()), + })] + }, + sync: Some(SyncSteps { + steps: vec![SyncStep::Plugin( + crate::manifest::adapter::plugin::Adapter { + source: prebuilt::SourceField::Local(prebuilt::LocalSource { + path: "./plugins/my-sync.wasm".into(), + }), + sha256: None, + dirs: Some(vec!["assets/seed-data/".to_string()]), + } + )] + }), + }, + }, + ); + } + + #[test] + fn sync_steps_plugin_remote() { + assert_eq!( + validate_canister_yaml(indoc! {r#" + name: my-canister + build: + steps: + - type: script + command: dosomething.sh + sync: + steps: + - type: plugin + url: https://example.com/plugins/migrate-v2.wasm + sha256: a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3 + "#}), + CanisterManifest { + name: "my-canister".to_string(), + settings: Settings::default(), + init_args: None, + instructions: Instructions::BuildSync { + build: BuildSteps { + steps: vec![BuildStep::Script(script::Adapter { + command: script::CommandField::Command("dosomething.sh".to_string()), + })] + }, + sync: Some(SyncSteps { + steps: vec![SyncStep::Plugin(crate::manifest::adapter::plugin::Adapter { + source: prebuilt::SourceField::Remote(prebuilt::RemoteSource { + url: "https://example.com/plugins/migrate-v2.wasm".to_string(), + }), + sha256: Some( + "a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3" + .to_string() + ), + dirs: None, + })] + }), + }, + }, + ); + } + #[test] fn sync_steps() { assert_eq!( diff --git a/docs/schemas/canister-yaml-schema.json b/docs/schemas/canister-yaml-schema.json index ba1ac9637..1fd9b220d 100644 --- a/docs/schemas/canister-yaml-schema.json +++ b/docs/schemas/canister-yaml-schema.json @@ -44,7 +44,7 @@ "description": "Remote url to fetch a WASM file from" } ], - "description": "Configuration for a Pre-built canister build adapter.", + "description": "Configuration for a wasm source — used by adapters that load a `.wasm` file\neither from a local path or from a remote URL.", "properties": { "sha256": { "description": "Optional sha256 checksum of the WASM", @@ -89,6 +89,39 @@ ], "type": "object" }, + "Adapter4": { + "anyOf": [ + { + "$ref": "#/$defs/LocalSource", + "description": "Local path on-disk to read a WASM file from" + }, + { + "$ref": "#/$defs/RemoteSource", + "description": "Remote url to fetch a WASM file from" + } + ], + "description": "Configuration for a sync plugin step.\n\nA sync plugin is a WebAssembly module invoked during `icp sync` for a\nspecific canister. It runs inside the Extism sandbox with restricted\npermissions — it can only call canister methods on the canister being\nsynced and read files from the declared `dirs` allowlist.\n\nExample:\n```yaml\n- type: plugin\n path: ./plugins/populate-data.wasm\n sha256: e3b0c44298fc1c149afb... # optional but recommended\n dirs: # optional read-access directories\n - assets/seed-data/\n```", + "properties": { + "dirs": { + "description": "Directories (relative to canister directory) the plugin may read from.", + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "sha256": { + "description": "Optional sha256 checksum of the wasm file.\nRequired when `url` is used; optional (but recommended) for `path`.", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "ArgsFormat": { "description": "Format specifier for canister call/install args content.", "oneOf": [ @@ -452,6 +485,20 @@ "type" ], "type": "object" + }, + { + "$ref": "#/$defs/Adapter4", + "description": "Represents a sync step executed by a WebAssembly plugin running inside\nthe Extism sandbox. The plugin can call canister methods on exactly\nthe canister being synced and read files from the declared `dirs`.", + "properties": { + "type": { + "const": "plugin", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" } ] }, diff --git a/docs/schemas/icp-yaml-schema.json b/docs/schemas/icp-yaml-schema.json index efd5d1bc7..12687cc29 100644 --- a/docs/schemas/icp-yaml-schema.json +++ b/docs/schemas/icp-yaml-schema.json @@ -44,7 +44,7 @@ "description": "Remote url to fetch a WASM file from" } ], - "description": "Configuration for a Pre-built canister build adapter.", + "description": "Configuration for a wasm source — used by adapters that load a `.wasm` file\neither from a local path or from a remote URL.", "properties": { "sha256": { "description": "Optional sha256 checksum of the WASM", @@ -89,6 +89,39 @@ ], "type": "object" }, + "Adapter4": { + "anyOf": [ + { + "$ref": "#/$defs/LocalSource", + "description": "Local path on-disk to read a WASM file from" + }, + { + "$ref": "#/$defs/RemoteSource", + "description": "Remote url to fetch a WASM file from" + } + ], + "description": "Configuration for a sync plugin step.\n\nA sync plugin is a WebAssembly module invoked during `icp sync` for a\nspecific canister. It runs inside the Extism sandbox with restricted\npermissions — it can only call canister methods on the canister being\nsynced and read files from the declared `dirs` allowlist.\n\nExample:\n```yaml\n- type: plugin\n path: ./plugins/populate-data.wasm\n sha256: e3b0c44298fc1c149afb... # optional but recommended\n dirs: # optional read-access directories\n - assets/seed-data/\n```", + "properties": { + "dirs": { + "description": "Directories (relative to canister directory) the plugin may read from.", + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "sha256": { + "description": "Optional sha256 checksum of the wasm file.\nRequired when `url` is used; optional (but recommended) for `path`.", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "ArgsFormat": { "description": "Format specifier for canister call/install args content.", "oneOf": [ @@ -948,6 +981,20 @@ "type" ], "type": "object" + }, + { + "$ref": "#/$defs/Adapter4", + "description": "Represents a sync step executed by a WebAssembly plugin running inside\nthe Extism sandbox. The plugin can call canister methods on exactly\nthe canister being synced and read files from the declared `dirs`.", + "properties": { + "type": { + "const": "plugin", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" } ] }, diff --git a/sync-plugin/design.md b/sync-plugin/design.md new file mode 100644 index 000000000..c4c49ebc1 --- /dev/null +++ b/sync-plugin/design.md @@ -0,0 +1,331 @@ +# Sync Plugin System Design + +## Overview + +This document describes the design for extending `icp sync` with a new step type: +**`plugin`**. A sync plugin is a WebAssembly component whose `exec()` function is +invoked by `icp-cli` during the sync phase for a specific canister. Plugins run +inside the wasmtime sandbox with deliberately restricted permissions. + +--- + +## Motivation + +The existing sync steps (`script` and `assets`) cover common patterns, but +cannot express arbitrary post-deployment logic without shelling out. Shell +scripts lack structure, have unrestricted host access, and cannot be distributed +as self-contained verifiable artifacts. + +Sync plugins fill that gap: + +- Written in any language that targets WebAssembly (Rust, Go, C, etc.) +- Distributed as a single `.wasm` component file (local or remote URL + sha256) +- Sandboxed — cannot make arbitrary syscalls, network connections, or file + system access beyond what the host explicitly allows +- Can call canister methods (update and query) on **exactly one canister** — + the one being synced — via the `canister-call` host function +- Can read files from a declared allowlist of directories via the `read-file` + host function + +--- + +## Canister Manifest Syntax + +A sync plugin step is declared in `canister.yaml` under `sync.steps` with +`type: plugin`: + +```yaml +name: my-canister +build: + steps: + - type: pre-built + path: dist/my_canister.wasm + +sync: + steps: + # Local plugin + - type: plugin + path: ./plugins/populate-data.wasm + sha256: e3b0c44298fc1c149afb... # optional but recommended + dirs: # optional read-access directories + - assets/seed-data/ + - config/ + + # Remote plugin (downloaded + verified before execution) + - type: plugin + url: https://example.com/plugins/migrate-v2.wasm + sha256: a665a45920422f9d417e... # required for remote +``` + +**Fields**: + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `type` | `"plugin"` | yes | Identifies the step type | +| `path` | string | one of `path`/`url` | Local path to the wasm file, relative to canister directory | +| `url` | string | one of `path`/`url` | Remote URL to download the wasm file from | +| `sha256` | string | required for `url`, optional for `path` | SHA-256 hex digest of the wasm file | +| `dirs` | `[string]` | no | Directories (relative to canister dir) the plugin may read from | + +--- + +## Plugin Interface (WIT) + +The interface is defined in [sync-plugin.wit](sync-plugin.wit) — that file is the +source of truth. Notable design choices: + +- **`result` throughout** — all fallible host functions return + `result<..., string>`, and `exec` returns `result, string>`. + This lets the guest use Rust's `?` operator directly on every host call. + +- **No JSON at the boundary** — types are encoded via the Canonical ABI, which + wasmtime handles transparently. Neither the host nor the plugin deals with + serialization. + +- **`canister-call` takes a request record, not a canister ID** — the host + always calls the canister from `sync-exec-input.canister-id`; the plugin + cannot supply a different target. The restriction is structural, not enforced + by a runtime check on a field value. + +--- + +## Host-Side Enforcement + +The host functions registered via `wasmtime::component::bindgen!` enforce all +restrictions through the host state struct — there is no way for the wasm +component to bypass them: + +### `canister-call` + +``` +Captured: target_canister_id: Principal +Enforcement: always calls target_canister_id regardless of plugin request; + plugin cannot call any other principal +``` + +### `read-file` + +``` +Captured: allowed_dirs: Vec (absolute, canonicalized) +Enforcement: canonicalize(requested_path) must have one of allowed_dirs as a prefix + → if not, return Err(...) to the plugin +``` + +### `list-dir` + +``` +Captured: allowed_dirs: Vec (absolute, canonicalized) +Enforcement: same prefix check as read-file +Result: entries one level deep (name + is-dir flag); caller descends by + calling list-dir again with an appended entry name +``` + +Canonicalization prevents `../` traversal attacks for both `read-file` and +`list-dir`. + +### `log` + +No restrictions — prints to the CLI progress stream (or stdout during testing). + +### Network / other I/O + +The wasmtime Component Model sandbox does not expose WASI socket or filesystem +interfaces to the component unless explicitly linked. Since the host only links +the four declared import functions, the plugin cannot open sockets, write files, +or spawn processes. + +--- + +## Crate Structure + +### `crates/icp-sync-plugin` + +Runtime crate — host-side Component Model integration for sync plugins. + +``` +crates/icp-sync-plugin/ + src/ + lib.rs — public API: run_plugin(...), RunPluginError + runtime.rs — wasmtime component setup, host state, bindgen!, exec() call + sandbox.rs — path canonicalization + allowlist enforcement + Cargo.toml — depends on: wasmtime (component-model feature), candid, + candid-parser, ic-agent, camino, snafu, tokio +``` + +Public function signature: + +```rust +pub fn run_plugin( + wasm_path: Utf8PathBuf, + base_dir: Utf8PathBuf, + allowed_dirs: Vec, + target_canister_id: Principal, + agent: Agent, + environment: String, + stdio: Option>, +) -> Result<(), RunPluginError> +``` + +### Host-Side Pattern (`runtime.rs`) + +```rust +wasmtime::component::bindgen!({ + world: "sync-plugin", + path: "../../sync-plugin/sync-plugin.wit", +}); + +struct HostState { /* target_canister_id, agent, allowed_dirs, base_dir, stdio */ } + +impl SyncPluginImports for HostState { + fn canister_call(&mut self, req: CanisterCallRequest) -> Result { ... } + fn read_file(&mut self, path: String) -> Result { ... } + fn list_dir(&mut self, path: String) -> Result, String> { ... } + fn log(&mut self, message: String) { ... } +} + +// In run_plugin: +let engine = Engine::new(Config::new().wasm_component_model(true))?; +let component = Component::from_file(&engine, &wasm_path)?; +let mut store = Store::new(&engine, host_state); +let (plugin, _) = SyncPlugin::instantiate(&mut store, &component, &linker)?; +let result = plugin.call_exec(&mut store, &input)?; +``` + +The `bindgen!` macro generates `SyncPlugin`, `SyncPluginImports`, and all WIT +types as plain Rust structs/enums — no JSON, no manual serialization. + +### `crates/icp/src/manifest/adapter/plugin.rs` + +Describes the `canister.yaml` fields: + +```rust +pub struct Adapter { + #[serde(flatten)] + pub source: super::prebuilt::SourceField, + pub sha256: Option, + pub dirs: Option>, +} +``` + +### `crates/icp/src/canister/sync/plugin.rs` + +Resolves the wasm, verifies sha256, canonicalizes dirs, then calls +`icp_sync_plugin::run_plugin(...)`. + +--- + +## Writing a Sync Plugin (Rust) + +Plugins are built as WebAssembly components targeting `wasm32-wasip2` using +[`cargo component`](https://github.com/bytecodealliance/cargo-component): + +```bash +cargo install cargo-component +cargo component build --release +``` + +The WIT file (`sync-plugin/sync-plugin.wit`) is distributed with the tool and +referenced in the plugin's `Cargo.toml`: + +```toml +[package.metadata.component] +package = "icp:sync-plugin" +``` + +**`src/lib.rs`** — implement the generated `Guest` trait: + +```rust +cargo_component_bindings::generate!(); + +use bindings::Guest; +use bindings::icp::sync_plugin::types::{CanisterCallRequest, CallType, SyncExecInput}; + +struct MyPlugin; + +impl Guest for MyPlugin { + fn exec(input: SyncExecInput) -> Result, String> { + bindings::log(&format!("syncing canister {}", input.canister_id)); + + let entries = bindings::list_dir("seed-data/")?; + + for entry in entries { + if entry.is_dir { continue; } + + let path = format!("seed-data/{}", entry.name); + let data = bindings::read_file(&path)?; + + bindings::canister_call(CanisterCallRequest { + method: "seed".to_string(), + arg: format!("(\"{}\")", data.trim()), + call_type: Some(CallType::Update), + })?; + + bindings::log(&format!("{path}: ok")); + } + + Ok(Some(format!( + "seeded canister {} in environment {}", + input.canister_id, input.environment + ))) + } +} +``` + +`cargo_component_bindings::generate!()` runs at build time — nothing generated +is committed to the repo. The WIT file is the sole source of truth. + +--- + +## Sandbox Summary + +| Capability | Allowed | Enforcement | +|------------|---------|-------------| +| `canister-call` to target canister | Yes | Host always uses captured `target_canister_id` | +| `canister-call` to any other canister | No | Not a parameter; host ignores any such intent | +| `read-file` within declared `dirs` | Yes | Path allowlist checked after canonicalization | +| `read-file` outside declared `dirs` | No | Returns `Err(...)` to plugin | +| `list-dir` within declared `dirs` | Yes | Path allowlist checked after canonicalization | +| `list-dir` outside declared `dirs` | No | Returns `Err(...)` to plugin | +| `log` (print to CLI output) | Yes | Unrestricted | +| Arbitrary filesystem write | No | No WASI filesystem write interface linked | +| Network access (TCP/UDP/etc.) | No | No WASI socket interface linked | +| Spawning processes | No | No WASI process interface linked | +| Calls to other environments | No | Agent scoped to environment at plugin load time | + +--- + +## Decisions + +**1. No generated file checked in** + +`wasmtime::component::bindgen!` (host side) and `cargo_component_bindings::generate!()` +(guest side) both run at build time — nothing generated is committed to the repo. +The WIT file is the sole source of truth. + +**2. `result` for all fallible functions** + +`exec` returns `result, string>` — the ok arm carries optional +output text, the err arm carries the error message. All host functions follow the +same pattern, so the guest can use `?` uniformly. + +**3. `dirs` resolution** + +Relative to the canister directory. Consistent with other adapters. + +**4. Caching downloaded wasm** + +Not implemented in the POC — deferred. + +**5. Plugin timeout** + +Not implemented in the POC. wasmtime supports epoch-based interruption and +fuel-based metering; adding a configurable `timeout_seconds` field to the +adapter is a follow-up. + +--- + +## Follow-up Items + +- **Wasm caching**: cache remote plugin wasm files in `.icp/cache/`. +- **Plugin timeout**: add `timeout_seconds: Option` to + `adapter::plugin::Adapter`; wire through to wasmtime epoch interruption. diff --git a/sync-plugin/plan.md b/sync-plugin/plan.md new file mode 100644 index 000000000..5e81cb3ae --- /dev/null +++ b/sync-plugin/plan.md @@ -0,0 +1,359 @@ +# Sync Plugin Implementation Plan + +Reference: [sync-plugin/design.md](sync-plugin/design.md) + +--- + +## Step 1 — Create the sync plugin manifest adapter + +**New file**: `crates/icp/src/manifest/adapter/plugin.rs` + +```rust +use super::prebuilt::SourceField; +use crate::prelude::*; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// Configuration for a sync plugin step. +#[derive(Clone, Debug, Deserialize, PartialEq, JsonSchema, Serialize)] +pub struct Adapter { + #[serde(flatten)] + pub source: SourceField, // path: or url: + pub sha256: Option, + pub dirs: Option>, // read-access directory allowlist +} +``` + +Add `pub mod plugin;` to `crates/icp/src/manifest/adapter/mod.rs`. + +--- + +## Step 2 — Add `SyncStep::Plugin` to the canister manifest + +**File**: `crates/icp/src/manifest/canister.rs` + +```rust +#[derive(Clone, Debug, Deserialize, PartialEq, JsonSchema, Serialize)] +#[serde(tag = "type", rename_all = "lowercase")] +pub enum SyncStep { + Script(adapter::script::Adapter), + Assets(adapter::assets::Adapter), + Plugin(adapter::plugin::Adapter), // NEW +} +``` + +Update `SyncStep::fmt` to cover the new variant. + +Add a test case for the new YAML syntax in `canister.rs` tests: + +```yaml +sync: + steps: + - type: plugin + path: ./plugins/my-sync.wasm + dirs: + - assets/seed-data/ +``` + +--- + +## Step 3 — Write the WIT interface file + +**File**: `sync-plugin/sync-plugin.wit` (already created) + +The WIT world defines the complete contract between icp-cli and any sync plugin. +It uses: +- `result` for all fallible operations — no `nullable` field workarounds +- `option` for optional values +- Plain `record` and `enum` types that map directly to Rust structs/enums + +The WIT file is the single source of truth for both the host runtime and guest +plugin code — no separate schema file, no generated file checked in. + +Add the WIT file path to the workspace `Cargo.toml` as a note for reviewers, or +document it in the crate README. The `bindgen!` macro on the host side and the +`cargo component` tool on the guest side both resolve the path at build time. + +--- + +## Step 4 — Implement `crates/icp-sync-plugin` with wasmtime + +**Crate**: `crates/icp-sync-plugin/` + +Add to `Cargo.toml`: + +```toml +[dependencies] +camino.workspace = true +candid.workspace = true +candid_parser.workspace = true +hex.workspace = true +ic-agent.workspace = true +snafu.workspace = true +tokio.workspace = true +wasmtime = { workspace = true } +``` + +Add `wasmtime` to the root `Cargo.toml` `[workspace.dependencies]` table with +its version and required features: + +```toml +# root Cargo.toml +[workspace.dependencies] +wasmtime = { version = "X", features = ["component-model"] } +``` + +In `crates/icp-sync-plugin/Cargo.toml` declare it without a version +(`workspace = true` inherits everything from the root). + +### `src/sandbox.rs` + +Already implemented and tested — no changes needed. + +```rust +/// Returns true iff `path` (canonicalized) starts with one of `allowed_dirs`. +pub fn is_path_allowed(path: &Utf8Path, allowed_dirs: &[Utf8PathBuf]) -> bool +``` + +### `src/runtime.rs` + +Replace the stub with the wasmtime Component Model implementation. + +```rust +wasmtime::component::bindgen!({ + world: "sync-plugin", + path: "../../sync-plugin/sync-plugin.wit", +}); + +struct HostState { + target_canister_id: Principal, + agent: Arc, + allowed_dirs: Arc>, + base_dir: Arc, + stdio: Option>, +} + +impl SyncPluginImports for HostState { + fn canister_call(&mut self, req: CanisterCallRequest) -> Result { ... } + fn read_file(&mut self, path: String) -> Result { ... } + fn list_dir(&mut self, path: String) -> Result, String> { ... } + fn log(&mut self, message: String) { ... } +} +``` + +Error variants (one per primary action): + +- `LoadComponent { path }` — wasmtime fails to load or parse the component +- `Instantiate { path }` — linker or store setup failure +- `CallExec { path }` — wasmtime trap or ABI error during the exec() call +- `PluginFailed { message }` — exec() returned `Err(message)` + +`canister_call` in `HostState` blocks the current thread on the async agent call +using `tokio::runtime::Handle::current().block_on(...)` — the host is already +inside a `tokio::task::block_in_place` call in `sync/plugin.rs`. + +### `src/lib.rs` + +Re-exports `run_plugin` and `RunPluginError` — no change to the public API. + +--- + +## Step 5 — Implement `sync/plugin.rs` in the `icp` crate + +**File**: `crates/icp/src/canister/sync/plugin.rs` (already exists as a stub) + +```rust +pub async fn sync( + adapter: &adapter::plugin::Adapter, + params: &Params, + agent: &Agent, + environment: &str, + stdio: Option>, +) -> Result<(), PluginError> +``` + +Responsibilities: +1. Resolve the wasm path: + - `Local`: join with `params.path` (canister directory) + - `Remote`: download to temp file (reuse the download + sha256 utility used + by the prebuilt build adapter) +2. Verify sha256 if present +3. Canonicalize declared `dirs` relative to `params.path` +4. Call `icp_sync_plugin::run_plugin(...)` + +Add `PluginError` variants for each failing action (wasm resolution, download, +sha256 mismatch, run). + +--- + +## Step 6 — Wire `SyncStep::Plugin` into the dispatcher + +**File**: `crates/icp/src/canister/sync/mod.rs` + +```rust +mod plugin; + +// In Syncer::sync(): +SyncStep::Plugin(adapter) => { + Ok(plugin::sync(adapter, params, agent, environment, stdio).await?) +} +``` + +Add `Plugin` variant to `SynchronizeError`. + +The `environment` string must be threaded through from `Params` (add a field) +or passed as a separate parameter — check how `assets::sync` currently receives +it and be consistent. + +--- + +## Step 7 — Build the proof-of-concept plugin + +**Directory**: `sync-plugin/poc/` + +A Rust wasm plugin that: +1. Lists a declared directory and reads each text file found +2. Calls an update method on the canister, passing the file content as a string argument +3. Logs the result of each call + +### Toolchain + +Plugins use plain `cargo build` — no `cargo-component` tool required: + +```bash +rustup target add wasm32-wasip2 +cargo build --target wasm32-wasip2 --release +``` + +The output is a WebAssembly component binary (`.wasm`) that the host loads +directly with `wasmtime::component::Component::from_file`. + +### `Cargo.toml` + +```toml +[package] +name = "icp-sync-plugin-poc" +version = "0.1.0" +edition = "2024" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = { version = "X", features = ["realloc"] } + +[build-dependencies] +# none — build.rs only emits rerun-if-changed directives +``` + +### `build.rs` + +A minimal build script that tells Cargo to re-run bindings generation whenever +the WIT file changes: + +```rust +fn main() { + println!("cargo:rerun-if-changed=../../sync-plugin/sync-plugin.wit"); +} +``` + +### `src/lib.rs` + +Use the `wit_bindgen::generate!` proc macro (no separate `build.rs` code +generation step — the macro expands at compile time from the WIT path): + +```rust +wit_bindgen::generate!({ + world: "sync-plugin", + path: "../../sync-plugin/sync-plugin.wit", +}); + +use exports::icp::sync_plugin::types::{CanisterCallRequest, CallType, GuestExec, SyncExecInput}; + +struct Plugin; + +impl GuestExec for Plugin { + fn exec(input: SyncExecInput) -> Result, String> { + log(&format!("sync plugin: starting for canister {}", input.canister_id)); + + let entries = list_dir("seed-data/")?; + + for entry in entries { + if entry.is_dir { continue; } + let path = format!("seed-data/{}", entry.name); + let data = read_file(&path)?; + canister_call(CanisterCallRequest { + method: "seed".to_string(), + arg: format!("(\"{}\")", data.trim().replace('"', "\\\"")), + call_type: Some(CallType::Update), + })?; + log(&format!("{path}: ok")); + } + + Ok(Some(format!( + "seeded canister {} in environment {}", + input.canister_id, input.environment + ))) + } +} + +export!(Plugin); +``` + +--- + +## Step 8 — Update JSON schema and CLI docs + +```bash +./scripts/generate-config-schemas.sh # regenerate canister-yaml-schema.json +./scripts/generate-cli-docs.sh # regenerate CLI reference docs +``` + +The new `SyncStep::Plugin` variant and `adapter::plugin::Adapter` implement +`JsonSchema` (via `schemars`), so the schema generator picks them up +automatically once wired in. + +--- + +## Step 9 — Add integration tests + +- A `canister.yaml` fixture with `type: plugin` in `crates/icp-cli/tests/` or + `examples/` +- Unit tests in `adapter/plugin.rs` (YAML round-trip, same style as + `adapter/prebuilt.rs`) +- Unit tests in `sync/plugin.rs` for sha256 verification and path allowlist + enforcement (no network needed — use a minimal hand-crafted wasm component or + build the poc plugin in the test) +- Unit tests in `sandbox.rs` for `list_dir` allowlist enforcement: path outside + allowed dirs, `../` traversal attempts, and a valid listing (these already + exist and pass) + +--- + +## Order of Dependencies + +``` +Step 1 (plugin adapter) ──► Step 2 (SyncStep::Plugin) + └─ Step 6 (dispatcher) +Step 3 (WIT file — already done) +Step 4 (icp-sync-plugin runtime) + └─ Step 5 (sync/plugin.rs) ──► Step 6 (dispatcher) +Step 8 (schema + docs) — after Steps 1–2 +Step 9 (tests) — after Steps 1–6 +``` + +Steps 1–2 (manifest layer) and Step 4 (runtime layer) can be developed +independently and in parallel. Step 5 joins both. Step 6 is the final wire-up. + +--- + +## Follow-up Items (post-POC) + +These are out of scope for the current implementation; tracked here for later: + +- **Wasm caching**: cache remote plugin wasm in `.icp/cache/` to avoid + re-downloading on every sync. +- **Plugin timeout**: add `timeout_seconds: Option` to + `adapter::plugin::Adapter` and wire through to wasmtime's epoch interruption + mechanism. diff --git a/sync-plugin/poc/.gitignore b/sync-plugin/poc/.gitignore new file mode 100644 index 000000000..2f7896d1d --- /dev/null +++ b/sync-plugin/poc/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/sync-plugin/poc/Cargo.lock b/sync-plugin/poc/Cargo.lock new file mode 100644 index 000000000..fa678ca70 --- /dev/null +++ b/sync-plugin/poc/Cargo.lock @@ -0,0 +1,398 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64-serde" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba368df5de76a5bea49aaf0cf1b39ccfbbef176924d1ba5db3e4135216cbe3c7" +dependencies = [ + "base64 0.21.7", + "serde", +] + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "extism-convert" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1a8eac059a1730a21aa47f99a0c2075ba0ab88fd0c4e52e35027cf99cdf3e7" +dependencies = [ + "anyhow", + "base64 0.22.1", + "bytemuck", + "extism-convert-macros", + "prost", + "rmp-serde", + "serde", + "serde_json", +] + +[[package]] +name = "extism-convert-macros" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "848f105dd6e1af2ea4bb4a76447658e8587167df3c4e4658c4258e5b14a5b051" +dependencies = [ + "manyhow", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "extism-manifest" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953a22ad322939ae4567ec73a34913a3a43dcbdfa648b8307d38fe56bb3a0acd" +dependencies = [ + "base64 0.22.1", + "serde", + "serde_json", +] + +[[package]] +name = "extism-pdk" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352fcb5a66eb74145a1c4a01f2bd15d59c62c85be73aac8471880c65b26b798f" +dependencies = [ + "anyhow", + "base64 0.22.1", + "extism-convert", + "extism-manifest", + "extism-pdk-derive", + "serde", + "serde_json", +] + +[[package]] +name = "extism-pdk-derive" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d086daea5fd844e3c5ac69ddfe36df4a9a43e7218cf7d1f888182b089b09806c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "icp-sync-plugin-poc" +version = "0.1.0" +dependencies = [ + "base64 0.21.7", + "base64-serde", + "extism-pdk", + "serde", + "serde_json", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "manyhow" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "manyhow-macros" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-utils" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "winnow" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +dependencies = [ + "memchr", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/sync-plugin/poc/Cargo.toml b/sync-plugin/poc/Cargo.toml new file mode 100644 index 000000000..0eae40802 --- /dev/null +++ b/sync-plugin/poc/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "icp-sync-plugin-poc" +version = "0.1.0" +edition = "2024" +publish = false + +[lib] +crate-type = ["cdylib"] + +# Dependencies added during implementation. +# This plugin targets wasm32-wasip2 and is built with `cargo component build`. +# See sync-plugin/sync-plugin.wit for the interface. +[dependencies] diff --git a/sync-plugin/poc/src/lib.rs b/sync-plugin/poc/src/lib.rs new file mode 100644 index 000000000..54907b5b3 --- /dev/null +++ b/sync-plugin/poc/src/lib.rs @@ -0,0 +1,2 @@ +// Sync plugin POC — to be rewritten for the WebAssembly Component Model. +// See sync-plugin/sync-plugin.wit for the interface definition. diff --git a/sync-plugin/sync-plugin.wit b/sync-plugin/sync-plugin.wit new file mode 100644 index 000000000..ed6aaca00 --- /dev/null +++ b/sync-plugin/sync-plugin.wit @@ -0,0 +1,69 @@ +package icp:sync-plugin@0.1.0; + +/// Types shared between the host runtime and sync plugins. +interface types { + /// Whether a canister call is an update or a query. + enum call-type { update, query } + + /// Input passed by the runtime to the plugin's exec() export. + record sync-exec-input { + /// Textual principal of the canister being synced. + canister-id: string, + /// Name of the environment being synced (e.g. "production", "local"). + environment: string, + } + + /// A request to call a method on the target canister. + record canister-call-request { + /// The canister method to call. + method: string, + /// Candid IDL text notation, e.g. `("hello")`. + arg: string, + /// Defaults to update if omitted. + call-type: option, + } + + /// A single entry returned by list-dir. + record dir-entry { + /// File or directory name (not a full path). + name: string, + /// True if the entry is a directory; false if it is a file. + is-dir: bool, + } +} + +/// The complete interface of a sync plugin. +world sync-plugin { + use types.{sync-exec-input, canister-call-request, dir-entry}; + + // ------------------------------------------------------------------------- + // Host functions (imports) — provided by icp-cli, called by the plugin + // ------------------------------------------------------------------------- + + /// Make an update or query call to the canister being synced. + /// The host always calls the canister from sync-exec-input.canister-id; + /// the plugin does not choose the target. + /// Returns Candid IDL text on success or an error message on failure. + import canister-call: func(req: canister-call-request) -> result; + + /// Read a UTF-8 file from the host filesystem. + /// The host enforces that the path falls within a declared `dirs` entry. + /// Returns the file contents or an error message. + import read-file: func(path: string) -> result; + + /// List one level of entries in a host filesystem directory. + /// The host enforces the same `dirs` allowlist as read-file. + /// Returns directory entries or an error message. + import list-dir: func(path: string) -> result, string>; + + /// Print a message to the CLI's progress output. + import log: func(message: string); + + // ------------------------------------------------------------------------- + // Plugin exports — implemented by the plugin, called by the host + // ------------------------------------------------------------------------- + + /// Execute the sync plugin for the canister being synced. + /// Returns optional output text on success or an error message on failure. + export exec: func(input: sync-exec-input) -> result, string>; +} From b8414577911bf189f7bb841e0207da269c297e82 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Wed, 15 Apr 2026 19:29:09 -0400 Subject: [PATCH 02/23] feat(sync-plugin): implement wasmtime Component Model runtime and POC plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the stub in `crates/icp-sync-plugin/src/runtime.rs` with a full wasmtime component model host implementation. The host provides four import functions to the plugin (canister-call, read-file, list-dir, log) and calls the plugin's exported exec() function. Also flesh out the proof-of-concept guest plugin in sync-plugin/poc/ with wit-bindgen bindings and a seed-data uploader that demonstrates the full host↔guest contract end-to-end. Co-Authored-By: Claude Sonnet 4.6 --- Cargo.lock | 841 +++++++++++++++++++++++++- Cargo.toml | 1 + crates/icp-sync-plugin/Cargo.toml | 4 +- crates/icp-sync-plugin/src/runtime.rs | 188 +++++- sync-plugin/poc/Cargo.lock | 422 ++++++------- sync-plugin/poc/Cargo.toml | 4 +- sync-plugin/poc/build.rs | 3 + sync-plugin/poc/src/lib.rs | 43 +- 8 files changed, 1258 insertions(+), 248 deletions(-) create mode 100644 sync-plugin/poc/build.rs diff --git a/Cargo.lock b/Cargo.lock index 08749c3ec..55087f4f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.1" @@ -176,7 +185,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" dependencies = [ - "object", + "object 0.37.3", ] [[package]] @@ -326,7 +335,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix", + "rustix 1.1.4", "slab", "windows-sys 0.61.2", ] @@ -357,7 +366,7 @@ dependencies = [ "cfg-if", "event-listener 5.4.1", "futures-lite", - "rustix", + "rustix 1.1.4", ] [[package]] @@ -394,7 +403,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix", + "rustix 1.1.4", "signal-hook-registry", "slab", "windows-sys 0.61.2", @@ -557,6 +566,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -739,7 +754,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee04c4c84f1f811b017f2fbb7dd8815c976e7ca98593de9c1e2afad0f636bff4" dependencies = [ - "base64", + "base64 0.22.1", "bollard-stubs", "bytes", "futures-core", @@ -852,6 +867,9 @@ name = "bumpalo" version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +dependencies = [ + "allocator-api2", +] [[package]] name = "byte-unit" @@ -1207,6 +1225,15 @@ dependencies = [ "cc", ] +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -1328,6 +1355,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpp_demangle" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" +dependencies = [ + "cfg-if", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1346,6 +1382,132 @@ dependencies = [ "libc", ] +[[package]] +name = "cranelift-assembler-x64" +version = "0.117.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b83fcf2fc1c8954561490d02079b496fd0c757da88129981e15bfe3a548229" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.117.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7496a6e92b5cee48c5d772b0443df58816dee30fed6ba19b2a28e78037ecedf" + +[[package]] +name = "cranelift-bforest" +version = "0.117.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73a9dc0a8d3d49ee772101924968830f1c1937d650c571d3c2dd69dc36a68f41" +dependencies = [ + "cranelift-entity", +] + +[[package]] +name = "cranelift-bitset" +version = "0.117.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "573c641174c40ef31021ae4a5a3ad78974e280633502d0dfc6e362385e0c100f" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-codegen" +version = "0.117.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d7c94d572615156f2db682181cadbd96342892c31e08cc26a757344319a9220" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.15.5", + "log", + "pulley-interpreter", + "regalloc2", + "rustc-hash 2.1.1", + "serde", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.117.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beecd9fcf2c3e06da436d565de61a42676097ea6eb6b4499346ac6264b6bb9ce" +dependencies = [ + "cranelift-assembler-x64", + "cranelift-codegen-shared", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.117.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f4ff8d2e1235f2d6e7fc3c6738be6954ba972cd295f09079ebffeca2f864e22" + +[[package]] +name = "cranelift-control" +version = "0.117.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "001312e9fbc7d9ca9517474d6fe71e29d07e52997fd7efe18f19e8836446ceb2" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.117.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb0fd6d4aae680275fcbceb08683416b744e65c8b607352043d3f0951d72b3b2" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-frontend" +version = "0.117.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fd44e7e5dcea20ca104d45894748205c51365ce4cdb18f4418e3ba955971d1b" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.117.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f900e0a3847d51eed0321f0777947fb852ccfce0da7fb070100357f69a2f37fc" + +[[package]] +name = "cranelift-native" +version = "0.117.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7617f13f392ebb63c5126258aca8b8eca739636ca7e4eeee301d3eff68489a6a" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -1571,6 +1733,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + [[package]] name = "der" version = "0.7.10" @@ -1693,6 +1864,16 @@ dependencies = [ "dirs-sys", ] +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + [[package]] name = "dirs" version = "6.0.0" @@ -1861,6 +2042,18 @@ dependencies = [ "serde", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "ena" version = "0.14.4" @@ -1998,6 +2191,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + [[package]] name = "fancy-regex" version = "0.17.0" @@ -2032,7 +2231,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", - "rustix", + "rustix 1.1.4", "windows-sys 0.59.0", ] @@ -2324,6 +2523,28 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "fxprof-processed-profile" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27d12c0aed7f1e24276a241aadc4cb8ea9f83000f34bc062b7cc2d51e3b0fabd" +dependencies = [ + "bitflags 2.11.0", + "debugid", + "fxhash", + "serde", + "serde_json", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -2376,6 +2597,17 @@ dependencies = [ "wasip3", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +dependencies = [ + "fallible-iterator", + "indexmap", + "stable_deref_trait", +] + [[package]] name = "git2" version = "0.20.4" @@ -2729,6 +2961,7 @@ dependencies = [ "allocator-api2", "equivalent", "foldhash 0.1.5", + "serde", ] [[package]] @@ -2949,7 +3182,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -3459,6 +3692,7 @@ dependencies = [ name = "icp-sync-plugin" version = "0.2.3" dependencies = [ + "anyhow", "camino", "candid", "candid_parser", @@ -3466,6 +3700,7 @@ dependencies = [ "ic-agent", "snafu", "tokio", + "wasmtime", ] [[package]] @@ -3758,6 +3993,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -3773,6 +4017,26 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "ittapi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1" +dependencies = [ + "anyhow", + "ittapi-sys", + "log", +] + +[[package]] +name = "ittapi-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc" +dependencies = [ + "cc", +] + [[package]] name = "jiff" version = "0.2.23" @@ -4129,6 +4393,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -4249,12 +4519,30 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix 1.1.4", +] + [[package]] name = "memmap2" version = "0.9.10" @@ -4634,6 +4922,18 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "crc32fast", + "hashbrown 0.15.5", + "indexmap", + "memchr", +] + [[package]] name = "object" version = "0.37.3" @@ -4863,7 +5163,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ - "base64", + "base64 0.22.1", "serde_core", ] @@ -5095,7 +5395,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -5114,6 +5414,18 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -5261,6 +5573,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "pulley-interpreter" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0ecb9823083f71df8735f21f6c44f2f2b55986d674802831df20f27e26c907" +dependencies = [ + "cranelift-bitset", + "log", + "wasmtime-math", +] + [[package]] name = "pxfm" version = "0.1.28" @@ -5432,6 +5755,26 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -5516,6 +5859,20 @@ dependencies = [ "serde_json", ] +[[package]] +name = "regalloc2" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc06e6b318142614e4a48bc725abbf08ff166694835c43c9dae5a9009704639a" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.15.5", + "log", + "rustc-hash 2.1.1", + "smallvec", +] + [[package]] name = "regex" version = "1.12.3" @@ -5574,7 +5931,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-channel", @@ -5718,6 +6075,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -5739,6 +6102,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.4" @@ -5748,7 +6124,7 @@ dependencies = [ "bitflags 2.11.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -6412,6 +6788,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "smartstring" @@ -6465,6 +6844,12 @@ dependencies = [ "der", ] +[[package]] +name = "sptr" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -6669,6 +7054,12 @@ dependencies = [ "xattr", ] +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + [[package]] name = "tempfile" version = "3.27.0" @@ -6678,7 +7069,7 @@ dependencies = [ "fastrand", "getrandom 0.4.2", "once_cell", - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -6718,7 +7109,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ - "rustix", + "rustix 1.1.4", "windows-sys 0.60.2", ] @@ -6991,6 +7382,7 @@ dependencies = [ "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", + "toml_write", "winnow 0.7.15", ] @@ -7015,6 +7407,12 @@ dependencies = [ "winnow 1.0.0", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "toml_writer" version = "1.0.7+spec-1.1.0" @@ -7123,6 +7521,17 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "trait-variant" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -7427,6 +7836,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.224.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab7a13a23790fe91ea4eb7526a1f3131001d874e3e00c2976c48861f2e82920" +dependencies = [ + "leb128", + "wasmparser 0.224.1", +] + [[package]] name = "wasm-encoder" version = "0.244.0" @@ -7437,6 +7856,16 @@ dependencies = [ "wasmparser 0.244.0", ] +[[package]] +name = "wasm-encoder" +version = "0.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61fb705ce81adde29d2a8e99d87995e39a6e927358c91398f374474746070ef7" +dependencies = [ + "leb128fmt", + "wasmparser 0.246.2", +] + [[package]] name = "wasm-metadata" version = "0.244.0" @@ -7445,7 +7874,7 @@ checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", "indexmap", - "wasm-encoder", + "wasm-encoder 0.244.0", "wasmparser 0.244.0", ] @@ -7462,6 +7891,19 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.224.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f17a5917c2ddd3819e84c661fae0d6ba29d7b9c1f0e96c708c65a9c4188e11" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", + "serde", +] + [[package]] name = "wasmparser" version = "0.244.0" @@ -7487,6 +7929,305 @@ dependencies = [ "serde", ] +[[package]] +name = "wasmparser" +version = "0.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71cde4757396defafd25417cfb36aa3161027d06d865b0c24baaae229aac005d" +dependencies = [ + "bitflags 2.11.0", + "indexmap", + "semver", +] + +[[package]] +name = "wasmprinter" +version = "0.224.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0095b53a3b09cbc2f90f789ea44aa1b17ecc2dad8b267e657c7391f3ded6293d" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.224.1", +] + +[[package]] +name = "wasmtime" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809cc8780708f1deed0a7c3fcab46954f0e8c08a6fe0252772481fbc88fcf946" +dependencies = [ + "addr2line", + "anyhow", + "async-trait", + "bitflags 2.11.0", + "bumpalo", + "cc", + "cfg-if", + "encoding_rs", + "fxprof-processed-profile", + "gimli", + "hashbrown 0.15.5", + "indexmap", + "ittapi", + "libc", + "log", + "mach2", + "memfd", + "object 0.36.7", + "once_cell", + "paste", + "postcard", + "psm", + "pulley-interpreter", + "rayon", + "rustix 0.38.44", + "semver", + "serde", + "serde_derive", + "serde_json", + "smallvec", + "sptr", + "target-lexicon", + "trait-variant", + "wasm-encoder 0.224.1", + "wasmparser 0.224.1", + "wasmtime-asm-macros", + "wasmtime-cache", + "wasmtime-component-macro", + "wasmtime-component-util", + "wasmtime-cranelift", + "wasmtime-environ", + "wasmtime-fiber", + "wasmtime-jit-debug", + "wasmtime-jit-icache-coherence", + "wasmtime-math", + "wasmtime-slab", + "wasmtime-versioned-export-macros", + "wasmtime-winch", + "wat", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmtime-asm-macros" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "236964b6b35af0f08879c9c56dbfbc5adc12e8d624672341a0121df31adaa3fa" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "wasmtime-cache" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5d75ac36ee28647f6d871a93eefc7edcb729c3096590031ba50857fac44fa8" +dependencies = [ + "anyhow", + "base64 0.21.7", + "directories-next", + "log", + "postcard", + "rustix 0.38.44", + "serde", + "serde_derive", + "sha2 0.10.9", + "toml 0.8.23", + "windows-sys 0.59.0", + "zstd", +] + +[[package]] +name = "wasmtime-component-macro" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581ef04bf33904db9a902ffb558e7b2de534d6a4881ee985ea833f187a78fdf" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasmtime-component-util", + "wasmtime-wit-bindgen", + "wit-parser 0.224.1", +] + +[[package]] +name = "wasmtime-component-util" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7108498a8a0afc81c7d2d81b96cdc509cd631d7bbaa271b7db5137026f10e3" + +[[package]] +name = "wasmtime-cranelift" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abcc9179097235c91f299a8ff56b358ee921266b61adff7d14d6e48428954dd2" +dependencies = [ + "anyhow", + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools 0.12.1", + "log", + "object 0.36.7", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 1.0.69", + "wasmparser 0.224.1", + "wasmtime-environ", + "wasmtime-versioned-export-macros", +] + +[[package]] +name = "wasmtime-environ" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e90f6cba665939381839bbf2ddf12d732fca03278867910348ef1281b700954" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "indexmap", + "log", + "object 0.36.7", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasm-encoder 0.224.1", + "wasmparser 0.224.1", + "wasmprinter", + "wasmtime-component-util", +] + +[[package]] +name = "wasmtime-fiber" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5c2ac21f0b39d72d2dac198218a12b3ddeb4ab388a8fa0d2e429855876783c" +dependencies = [ + "anyhow", + "cc", + "cfg-if", + "rustix 0.38.44", + "wasmtime-asm-macros", + "wasmtime-versioned-export-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmtime-jit-debug" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74812989369947f4f5a33f4ae8ff551eb6c8a97ff55e0269a9f5f0fac93cd755" +dependencies = [ + "cc", + "object 0.36.7", + "rustix 0.38.44", + "wasmtime-versioned-export-macros", +] + +[[package]] +name = "wasmtime-jit-icache-coherence" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f180cc0d2745e3a5df5d02231cd3046f49c75512eaa987b8202363b112e125d" +dependencies = [ + "anyhow", + "cfg-if", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmtime-math" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5f04c5dcf5b2f88f81cfb8d390294b2f67109dc4d0197ea7303c60a092df27c" +dependencies = [ + "libm", +] + +[[package]] +name = "wasmtime-slab" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9681707f1ae9a4708ca22058722fca5c135775c495ba9b9624fe3732b94c97" + +[[package]] +name = "wasmtime-versioned-export-macros" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2fe69d04986a12fc759d2e79494100d600adcb3bb79e63dedfc8e6bb2ab03e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "wasmtime-winch" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9c8eae8395d530bb00a388030de9f543528674c382326f601de47524376975" +dependencies = [ + "anyhow", + "cranelift-codegen", + "gimli", + "object 0.36.7", + "target-lexicon", + "wasmparser 0.224.1", + "wasmtime-cranelift", + "wasmtime-environ", + "winch-codegen", +] + +[[package]] +name = "wasmtime-wit-bindgen" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a5531455e2c55994a1540355140369bb7ec0e46d2699731c5ee9f4cf9c3f7d4" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "wit-parser 0.224.1", +] + +[[package]] +name = "wast" +version = "246.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe3fe8e3bf88ad96d031b4181ddbd64634b17cb0d06dfc3de589ef43591a9a62" +dependencies = [ + "bumpalo", + "leb128fmt", + "memchr", + "unicode-width 0.2.2", + "wasm-encoder 0.246.2", +] + +[[package]] +name = "wat" +version = "1.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd7fda1199b94fff395c2d19a153f05dbe7807630316fa9673367666fd2ad8c" +dependencies = [ + "wast", +] + [[package]] name = "web-sys" version = "0.3.91" @@ -7553,6 +8294,24 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "winch-codegen" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dbd4e07bd92c7ddace2f3267bdd31d4197b5ec58c315751325d45c19bfb56df" +dependencies = [ + "anyhow", + "cranelift-codegen", + "gimli", + "regalloc2", + "smallvec", + "target-lexicon", + "thiserror 1.0.69", + "wasmparser 0.224.1", + "wasmtime-cranelift", + "wasmtime-environ", +] + [[package]] name = "windows" version = "0.61.3" @@ -8039,7 +8798,7 @@ checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", "heck", - "wit-parser", + "wit-parser 0.244.0", ] [[package]] @@ -8086,10 +8845,28 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "wasm-encoder", + "wasm-encoder 0.244.0", "wasm-metadata", "wasmparser 0.244.0", - "wit-parser", + "wit-parser 0.244.0", +] + +[[package]] +name = "wit-parser" +version = "0.224.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3477d8d0acb530d76beaa8becbdb1e3face08929db275f39934963eb4f716f8" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.224.1", ] [[package]] @@ -8138,7 +8915,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix", + "rustix 1.1.4", ] [[package]] @@ -8336,6 +9113,34 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zvariant" version = "4.2.0" diff --git a/Cargo.toml b/Cargo.toml index 2ead0f454..a81a7fdb6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -105,6 +105,7 @@ tracing-subscriber = "0.3.20" url = { version = "2.5.4", features = ["serde"] } uuid = { version = "1.16.0", features = ["serde", "v4"] } wasmparser = "0.245.1" +wasmtime = { version = "30", features = ["component-model"] } winreg = "0.56.0" wslpath2 = "0.1" zeroize = "1.8.1" diff --git a/crates/icp-sync-plugin/Cargo.toml b/crates/icp-sync-plugin/Cargo.toml index 1d2495877..6658a51d5 100644 --- a/crates/icp-sync-plugin/Cargo.toml +++ b/crates/icp-sync-plugin/Cargo.toml @@ -7,6 +7,7 @@ repository.workspace = true publish.workspace = true [dependencies] +anyhow.workspace = true camino.workspace = true candid.workspace = true candid_parser.workspace = true @@ -14,8 +15,7 @@ hex.workspace = true ic-agent.workspace = true snafu.workspace = true tokio.workspace = true -# wasmtime with component-model feature — added during implementation -# wasmtime = { version = "...", features = ["component-model"] } +wasmtime.workspace = true [lints] workspace = true diff --git a/crates/icp-sync-plugin/src/runtime.rs b/crates/icp-sync-plugin/src/runtime.rs index fcafb76a2..857a33ebd 100644 --- a/crates/icp-sync-plugin/src/runtime.rs +++ b/crates/icp-sync-plugin/src/runtime.rs @@ -1,5 +1,7 @@ -// Runtime implementation — to be written using wasmtime::component. -// See sync-plugin/sync-plugin.wit for the interface definition. +// Host-side Component Model runtime for sync plugins. +// The WIT world is in sync-plugin/sync-plugin.wit. + +use std::sync::Arc; use camino::Utf8PathBuf; use candid::Principal; @@ -7,26 +9,188 @@ use ic_agent::Agent; use snafu::prelude::*; use tokio::sync::mpsc::Sender; +use crate::sandbox::is_path_allowed; + +wasmtime::component::bindgen!({ + world: "sync-plugin", + path: "../../sync-plugin/sync-plugin.wit", +}); + +use icp::sync_plugin::types::CallType; + +// HostState holds everything the plugin's import functions need. +struct HostState { + target_canister_id: Principal, + agent: Arc, + allowed_dirs: Arc>, + base_dir: Arc, + stdio: Option>, +} + +// `types::Host` is an empty marker trait generated for the `types` interface. +impl icp::sync_plugin::types::Host for HostState {} + +impl SyncPluginImports for HostState { + fn canister_call(&mut self, req: CanisterCallRequest) -> Result { + let arg_bytes = candid_parser::parse_idl_args(&req.arg) + .map_err(|e| format!("failed to parse Candid arg: {e}"))? + .to_bytes() + .map_err(|e| format!("failed to encode Candid arg: {e}"))?; + + let cid = self.target_canister_id; + let method = req.method.clone(); + let agent = Arc::clone(&self.agent); + let call_type = req.call_type.unwrap_or(CallType::Update); + + // We are already inside tokio::task::block_in_place (see sync/plugin.rs), + // so blocking the thread here is safe. + let result = tokio::runtime::Handle::current() + .block_on(async move { + match call_type { + CallType::Update => agent.update(&cid, &method).with_arg(arg_bytes).await, + CallType::Query => { + agent + .query(&cid, &method) + .with_arg(arg_bytes) + .call() + .await + } + } + }) + .map_err(|e| format!("canister call failed: {e}"))?; + + candid::IDLArgs::from_bytes(&result) + .map(|args| args.to_string()) + .map_err(|e| format!("failed to decode canister response: {e}")) + } + + fn read_file(&mut self, path: String) -> Result { + let full_path = self.base_dir.join(&path); + let canon_std = std::fs::canonicalize(full_path.as_std_path()) + .map_err(|e| format!("failed to resolve path '{path}': {e}"))?; + let canon = Utf8PathBuf::from_path_buf(canon_std) + .map_err(|p| format!("path is not valid UTF-8: {}", p.display()))?; + + if !is_path_allowed(&canon, &self.allowed_dirs) { + return Err(format!( + "access denied: '{path}' is outside the declared dirs allowlist" + )); + } + + std::fs::read_to_string(canon.as_std_path()) + .map_err(|e| format!("failed to read file '{path}': {e}")) + } + + fn list_dir(&mut self, path: String) -> Result, String> { + let full_path = self.base_dir.join(&path); + let canon_std = std::fs::canonicalize(full_path.as_std_path()) + .map_err(|e| format!("failed to resolve path '{path}': {e}"))?; + let canon = Utf8PathBuf::from_path_buf(canon_std) + .map_err(|p| format!("path is not valid UTF-8: {}", p.display()))?; + + if !is_path_allowed(&canon, &self.allowed_dirs) { + return Err(format!( + "access denied: '{path}' is outside the declared dirs allowlist" + )); + } + + std::fs::read_dir(canon.as_std_path()) + .map_err(|e| format!("failed to read directory '{path}': {e}"))? + .map(|entry| { + let entry = entry.map_err(|e| format!("failed to read directory entry: {e}"))?; + let name = entry.file_name().to_string_lossy().into_owned(); + let is_dir = entry + .file_type() + .map_err(|e| format!("failed to get file type for '{name}': {e}"))? + .is_dir(); + Ok(DirEntry { name, is_dir }) + }) + .collect() + } + + fn log(&mut self, message: String) { + if let Some(tx) = &self.stdio { + let _ = tx.blocking_send(message); + } + } +} + #[derive(Debug, Snafu)] pub enum RunPluginError { + #[snafu(display("failed to create wasmtime engine for plugin at {path}"))] + CreateEngine { source: anyhow::Error, path: Utf8PathBuf }, + #[snafu(display("failed to load wasm component from {path}"))] - LoadComponent { path: Utf8PathBuf }, + LoadComponent { source: anyhow::Error, path: Utf8PathBuf }, + + #[snafu(display("failed to instantiate wasm component at {path}"))] + Instantiate { source: anyhow::Error, path: Utf8PathBuf }, #[snafu(display("failed to call exec() on plugin at {path}"))] - CallExec { path: Utf8PathBuf }, + CallExec { source: anyhow::Error, path: Utf8PathBuf }, #[snafu(display("plugin returned error: {message}"))] PluginFailed { message: String }, } pub fn run_plugin( - _wasm_path: Utf8PathBuf, - _base_dir: Utf8PathBuf, - _allowed_dirs: Vec, - _target_canister_id: Principal, - _agent: Agent, - _environment: String, - _stdio: Option>, + wasm_path: Utf8PathBuf, + base_dir: Utf8PathBuf, + allowed_dirs: Vec, + target_canister_id: Principal, + agent: Agent, + environment: String, + stdio: Option>, ) -> Result<(), RunPluginError> { - unimplemented!("sync plugin runtime: migration to wasmtime Component Model in progress") + use wasmtime::component::{Component, Linker}; + use wasmtime::{Config, Engine, Store}; + + let mut config = Config::new(); + config.wasm_component_model(true); + let engine = + Engine::new(&config).context(CreateEngineSnafu { path: wasm_path.clone() })?; + + let component = Component::from_file(&engine, wasm_path.as_std_path()) + .context(LoadComponentSnafu { path: wasm_path.clone() })?; + + let canister_id_text = target_canister_id.to_text(); + + let host_state = HostState { + target_canister_id, + agent: Arc::new(agent), + allowed_dirs: Arc::new(allowed_dirs), + base_dir: Arc::new(base_dir), + stdio, + }; + + let mut linker: Linker = Linker::new(&engine); + SyncPlugin::add_to_linker(&mut linker, |s| s) + .context(InstantiateSnafu { path: wasm_path.clone() })?; + + let mut store = Store::new(&engine, host_state); + + let plugin = SyncPlugin::instantiate(&mut store, &component, &linker) + .context(InstantiateSnafu { path: wasm_path.clone() })?; + + let input = SyncExecInput { + canister_id: canister_id_text, + environment, + }; + + let result = plugin + .call_exec(&mut store, &input) + .context(CallExecSnafu { path: wasm_path })?; + + let stdio = store.into_data().stdio; + match result { + Ok(Some(msg)) => { + if let Some(tx) = &stdio { + let _ = tx.blocking_send(msg); + } + } + Ok(None) => {} + Err(message) => return PluginFailedSnafu { message }.fail(), + } + + Ok(()) } diff --git a/sync-plugin/poc/Cargo.lock b/sync-plugin/poc/Cargo.lock index fa678ca70..4d36d4ccb 100644 --- a/sync-plugin/poc/Cargo.lock +++ b/sync-plugin/poc/Cargo.lock @@ -3,56 +3,34 @@ version = 4 [[package]] -name = "anyhow" -version = "1.0.102" +name = "ahash" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64-serde" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba368df5de76a5bea49aaf0cf1b39ccfbbef176924d1ba5db3e4135216cbe3c7" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ - "base64 0.21.7", - "serde", + "cfg-if", + "once_cell", + "version_check", + "zerocopy", ] [[package]] -name = "bytemuck" -version = "1.25.0" +name = "anyhow" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] -name = "bytes" -version = "1.11.1" +name = "bitflags" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] -name = "either" -version = "1.15.0" +name = "cfg-if" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "equivalent" @@ -61,87 +39,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] -name = "extism-convert" -version = "1.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1a8eac059a1730a21aa47f99a0c2075ba0ab88fd0c4e52e35027cf99cdf3e7" -dependencies = [ - "anyhow", - "base64 0.22.1", - "bytemuck", - "extism-convert-macros", - "prost", - "rmp-serde", - "serde", - "serde_json", -] - -[[package]] -name = "extism-convert-macros" -version = "1.21.0" +name = "hashbrown" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "848f105dd6e1af2ea4bb4a76447658e8587167df3c4e4658c4258e5b14a5b051" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "manyhow", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", + "ahash", ] [[package]] -name = "extism-manifest" -version = "1.21.0" +name = "hashbrown" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953a22ad322939ae4567ec73a34913a3a43dcbdfa648b8307d38fe56bb3a0acd" -dependencies = [ - "base64 0.22.1", - "serde", - "serde_json", -] +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] -name = "extism-pdk" -version = "1.4.1" +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "352fcb5a66eb74145a1c4a01f2bd15d59c62c85be73aac8471880c65b26b798f" -dependencies = [ - "anyhow", - "base64 0.22.1", - "extism-convert", - "extism-manifest", - "extism-pdk-derive", - "serde", - "serde_json", -] +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "extism-pdk-derive" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d086daea5fd844e3c5ac69ddfe36df4a9a43e7218cf7d1f888182b089b09806c" +name = "icp-sync-plugin-poc" +version = "0.1.0" dependencies = [ - "proc-macro2", - "quote", - "syn", + "wit-bindgen", ] [[package]] -name = "hashbrown" -version = "0.17.0" +name = "id-arena" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" - -[[package]] -name = "icp-sync-plugin-poc" -version = "0.1.0" -dependencies = [ - "base64 0.21.7", - "base64-serde", - "extism-pdk", - "serde", - "serde_json", -] +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "indexmap" @@ -150,16 +79,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", -] - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", + "hashbrown 0.17.0", + "serde", + "serde_core", ] [[package]] @@ -169,27 +91,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] -name = "manyhow" -version = "0.11.4" +name = "leb128" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" -dependencies = [ - "manyhow-macros", - "proc-macro2", - "quote", - "syn", -] +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] -name = "manyhow-macros" -version = "0.11.4" +name = "log" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" -dependencies = [ - "proc-macro-utils", - "proc-macro2", - "quote", -] +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" @@ -198,32 +109,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "proc-macro-crate" -version = "3.5.0" +name = "once_cell" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit", -] +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] -name = "proc-macro-utils" -version = "0.10.0" +name = "prettyplease" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "quote", - "smallvec", + "syn", ] [[package]] @@ -235,29 +133,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "prost" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-derive" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" -dependencies = [ - "anyhow", - "itertools", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "quote" version = "1.0.45" @@ -268,23 +143,10 @@ dependencies = [ ] [[package]] -name = "rmp" -version = "0.8.15" +name = "semver" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" -dependencies = [ - "num-traits", -] - -[[package]] -name = "rmp-serde" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" -dependencies = [ - "rmp", - "serde", -] +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -293,7 +155,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", - "serde_derive", ] [[package]] @@ -335,6 +196,15 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "spdx" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" +dependencies = [ + "smallvec", +] + [[package]] name = "syn" version = "2.0.117" @@ -347,48 +217,178 @@ dependencies = [ ] [[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasm-encoder" +version = "0.220.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e913f9242315ca39eff82aee0e19ee7a372155717ff0eb082c741e435ce25ed1" dependencies = [ - "serde_core", + "leb128", + "wasmparser", ] [[package]] -name = "toml_edit" -version = "0.25.11+spec-1.1.0" +name = "wasm-metadata" +version = "0.220.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +checksum = "185dfcd27fa5db2e6a23906b54c28199935f71d9a27a1a27b3a88d6fee2afae7" dependencies = [ + "anyhow", "indexmap", - "toml_datetime", - "toml_parser", - "winnow", + "serde", + "serde_derive", + "serde_json", + "spdx", + "wasm-encoder", + "wasmparser", ] [[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" +name = "wasmparser" +version = "0.220.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "8d07b6a3b550fefa1a914b6d54fc175dd11c3392da11eee604e6ffc759805d25" dependencies = [ - "winnow", + "ahash", + "bitflags", + "hashbrown 0.14.5", + "indexmap", + "semver", ] [[package]] -name = "unicode-ident" -version = "1.0.24" +name = "wit-bindgen" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "6a2b3e15cd6068f233926e7d8c7c588b2ec4fb7cc7bf3824115e7c7e2a8485a3" +dependencies = [ + "wit-bindgen-rt", + "wit-bindgen-rust-macro", +] [[package]] -name = "winnow" -version = "1.0.1" +name = "wit-bindgen-core" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +checksum = "b632a5a0fa2409489bd49c9e6d99fcc61bb3d4ce9d1907d44662e75a28c71172" dependencies = [ - "memchr", + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7947d0131c7c9da3f01dfde0ab8bd4c4cf3c5bd49b6dba0ae640f1fa752572ea" +dependencies = [ + "bitflags", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4329de4186ee30e2ef30a0533f9b3c123c019a237a7c82d692807bf1b3ee2697" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177fb7ee1484d113b4792cc480b1ba57664bbc951b42a4beebe573502135b1fc" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.220.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b505603761ed400c90ed30261f44a768317348e49f1864e82ecdc3b2744e5627" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.220.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae2a7999ed18efe59be8de2db9cb2b7f84d88b27818c79353dfc53131840fe1a" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] diff --git a/sync-plugin/poc/Cargo.toml b/sync-plugin/poc/Cargo.toml index 0eae40802..d21453d8a 100644 --- a/sync-plugin/poc/Cargo.toml +++ b/sync-plugin/poc/Cargo.toml @@ -7,7 +7,5 @@ publish = false [lib] crate-type = ["cdylib"] -# Dependencies added during implementation. -# This plugin targets wasm32-wasip2 and is built with `cargo component build`. -# See sync-plugin/sync-plugin.wit for the interface. [dependencies] +wit-bindgen = { version = "0.36", features = ["realloc"] } diff --git a/sync-plugin/poc/build.rs b/sync-plugin/poc/build.rs new file mode 100644 index 000000000..aa2bc02f1 --- /dev/null +++ b/sync-plugin/poc/build.rs @@ -0,0 +1,3 @@ +fn main() { + println!("cargo:rerun-if-changed=../../sync-plugin/sync-plugin.wit"); +} diff --git a/sync-plugin/poc/src/lib.rs b/sync-plugin/poc/src/lib.rs index 54907b5b3..4a8a5c394 100644 --- a/sync-plugin/poc/src/lib.rs +++ b/sync-plugin/poc/src/lib.rs @@ -1,2 +1,41 @@ -// Sync plugin POC — to be rewritten for the WebAssembly Component Model. -// See sync-plugin/sync-plugin.wit for the interface definition. +wit_bindgen::generate!({ + world: "sync-plugin", + path: "../../sync-plugin/sync-plugin.wit", +}); + +struct Plugin; + +impl Guest for Plugin { + fn exec(input: SyncExecInput) -> Result, String> { + log(&format!( + "sync plugin: starting for canister {}", + input.canister_id + )); + + let entries = list_dir("seed-data/")?; + + for entry in entries { + if entry.is_dir { + continue; + } + let path = format!("seed-data/{}", entry.name); + let data = read_file(&path)?; + canister_call(&CanisterCallRequest { + method: "seed".to_string(), + arg: format!( + "(\"{}\")", + data.trim().replace('\\', "\\\\").replace('"', "\\\"") + ), + call_type: Some(icp::sync_plugin::types::CallType::Update), + })?; + log(&format!("{path}: ok")); + } + + Ok(Some(format!( + "seeded canister {} in environment {}", + input.canister_id, input.environment + ))) + } +} + +export!(Plugin); From 84deefa0ea144e329779155385ec247d3523a4b5 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 16 Apr 2026 11:26:41 -0400 Subject: [PATCH 03/23] feat(sync-plugin): add icp-sync-plugin example with Rust CDK canister - Add examples/icp-sync-plugin with a Rust CDK canister that stores (name, content) pairs seeded by the sync plugin from seed-data/ files - Add Candid interface (demo.did) and ic-wasm step to embed it - Link WASI P2 in the wasmtime host so wasm32-wasip2 plugins work - Walk the full error cause chain in sync failure output for better error messages; include wasm path in ReadWasm error - Update POC plugin to pass (filename, content) to the canister's seed() Co-Authored-By: Claude Sonnet 4.6 --- Cargo.lock | 331 +++++++- Cargo.toml | 3 +- crates/icp-cli/src/operations/sync.rs | 8 + crates/icp-sync-plugin/Cargo.toml | 1 + crates/icp-sync-plugin/src/runtime.rs | 22 + crates/icp/src/canister/sync/plugin.rs | 6 +- examples/icp-sync-plugin/.gitignore | 1 + examples/icp-sync-plugin/Cargo.lock | 712 ++++++++++++++++++ examples/icp-sync-plugin/Cargo.toml | 11 + examples/icp-sync-plugin/demo.did | 5 + examples/icp-sync-plugin/icp.yaml | 22 + .../icp-sync-plugin/seed-data/fruit-01.txt | 1 + .../icp-sync-plugin/seed-data/fruit-02.txt | 1 + .../icp-sync-plugin/seed-data/fruit-03.txt | 1 + examples/icp-sync-plugin/src/lib.rs | 23 + sync-plugin/poc/Cargo.lock | 152 ++-- sync-plugin/poc/Cargo.toml | 2 +- sync-plugin/poc/src/lib.rs | 34 +- 18 files changed, 1212 insertions(+), 124 deletions(-) create mode 100644 examples/icp-sync-plugin/.gitignore create mode 100644 examples/icp-sync-plugin/Cargo.lock create mode 100644 examples/icp-sync-plugin/Cargo.toml create mode 100644 examples/icp-sync-plugin/demo.did create mode 100644 examples/icp-sync-plugin/icp.yaml create mode 100644 examples/icp-sync-plugin/seed-data/fruit-01.txt create mode 100644 examples/icp-sync-plugin/seed-data/fruit-02.txt create mode 100644 examples/icp-sync-plugin/seed-data/fruit-03.txt create mode 100644 examples/icp-sync-plugin/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 55087f4f8..c866c471a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,6 +93,21 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.21" @@ -447,7 +462,7 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4888bf91cce63baf1670512d0f12b5d636179a4abbad6504812ac8ab124b3efe" dependencies = [ - "dirs", + "dirs 6.0.0", "git2", "terminal-prompt", ] @@ -1047,6 +1062,84 @@ dependencies = [ "toml 0.8.23", ] +[[package]] +name = "cap-fs-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5528f85b1e134ae811704e41ef80930f56e795923f866813255bc342cc20654" +dependencies = [ + "cap-primitives", + "cap-std", + "io-lifetimes", + "windows-sys 0.59.0", +] + +[[package]] +name = "cap-net-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20a158160765c6a7d0d8c072a53d772e4cb243f38b04bfcf6b4939cfbe7482e7" +dependencies = [ + "cap-primitives", + "cap-std", + "rustix 1.1.4", + "smallvec", +] + +[[package]] +name = "cap-primitives" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cf3aea8a5081171859ef57bc1606b1df6999df4f1110f8eef68b30098d1d3a" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes", + "ipnet", + "maybe-owned", + "rustix 1.1.4", + "rustix-linux-procfs", + "windows-sys 0.59.0", + "winx", +] + +[[package]] +name = "cap-rand" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8144c22e24bbcf26ade86cb6501a0916c46b7e4787abdb0045a467eb1645a1d" +dependencies = [ + "ambient-authority", + "rand 0.8.5", +] + +[[package]] +name = "cap-std" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6dc3090992a735d23219de5c204927163d922f42f575a0189b005c62d37549a" +dependencies = [ + "cap-primitives", + "io-extras", + "io-lifetimes", + "rustix 1.1.4", +] + +[[package]] +name = "cap-time-ext" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def102506ce40c11710a9b16e614af0cde8e76ae51b1f48c04b8d79f4b671a80" +dependencies = [ + "ambient-authority", + "cap-primitives", + "iana-time-zone", + "once_cell", + "rustix 1.1.4", + "winx", +] + [[package]] name = "cargo-generate" version = "0.23.7" @@ -1861,7 +1954,7 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" dependencies = [ - "dirs-sys", + "dirs-sys 0.5.0", ] [[package]] @@ -1874,13 +1967,22 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] + [[package]] name = "dirs" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "dirs-sys", + "dirs-sys 0.5.0", ] [[package]] @@ -1893,6 +1995,17 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users 0.4.6", + "winapi", +] + [[package]] name = "dirs-sys" version = "0.5.0" @@ -2366,6 +2479,17 @@ dependencies = [ "autocfg", ] +[[package]] +name = "fs-set-times" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" +dependencies = [ + "io-lifetimes", + "rustix 1.1.4", + "windows-sys 0.59.0", +] + [[package]] name = "fs_at" version = "0.2.1" @@ -3216,6 +3340,30 @@ dependencies = [ "tower-service", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ic-agent" version = "0.47.0" @@ -3701,6 +3849,7 @@ dependencies = [ "snafu", "tokio", "wasmtime", + "wasmtime-wasi", ] [[package]] @@ -3953,6 +4102,22 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "io-extras" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65" +dependencies = [ + "io-lifetimes", + "windows-sys 0.59.0", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + [[package]] name = "ipnet" version = "2.12.0" @@ -4528,6 +4693,12 @@ dependencies = [ "libc", ] +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + [[package]] name = "memchr" version = "2.8.0" @@ -6128,6 +6299,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustix-linux-procfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +dependencies = [ + "once_cell", + "rustix 1.1.4", +] + [[package]] name = "rustls" version = "0.23.37" @@ -6690,6 +6871,15 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" +[[package]] +name = "shellexpand" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" +dependencies = [ + "dirs 4.0.0", +] + [[package]] name = "shellwords" version = "1.1.0" @@ -7037,6 +7227,22 @@ dependencies = [ "libc", ] +[[package]] +name = "system-interface" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4592f674ce18521c2a81483873a49596655b179f71c5e05d10c1fe66c78745" +dependencies = [ + "bitflags 2.11.0", + "cap-fs-ext", + "cap-std", + "fd-lock", + "io-lifetimes", + "rustix 0.38.44", + "windows-sys 0.59.0", + "winx", +] + [[package]] name = "tap" version = "1.0.1" @@ -8177,6 +8383,50 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "wasmtime-wasi" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ce639c7d398586bc539ae9bba752084c1db7a49ab0f391a3230dcbcc6a64cfd" +dependencies = [ + "anyhow", + "async-trait", + "bitflags 2.11.0", + "bytes", + "cap-fs-ext", + "cap-net-ext", + "cap-rand", + "cap-std", + "cap-time-ext", + "fs-set-times", + "futures", + "io-extras", + "io-lifetimes", + "rustix 0.38.44", + "system-interface", + "thiserror 1.0.69", + "tokio", + "tracing", + "url", + "wasmtime", + "wasmtime-wasi-io", + "wiggle", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmtime-wasi-io" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdcad7178fddaa07786abe8ff5e043acb4bc8c8f737eb117f11e028b48d92792" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "futures", + "wasmtime", +] + [[package]] name = "wasmtime-winch" version = "30.0.2" @@ -8206,6 +8456,15 @@ dependencies = [ "wit-parser 0.224.1", ] +[[package]] +name = "wast" +version = "35.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" +dependencies = [ + "leb128", +] + [[package]] name = "wast" version = "246.0.2" @@ -8225,7 +8484,7 @@ version = "1.246.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bd7fda1199b94fff395c2d19a153f05dbe7807630316fa9673367666fd2ad8c" dependencies = [ - "wast", + "wast 246.0.2", ] [[package]] @@ -8263,6 +8522,48 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" +[[package]] +name = "wiggle" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5a4ea7722c042a659dc70caab0b56d7f45220e8bae1241cf5ebc7ab7efb0dfb" +dependencies = [ + "anyhow", + "async-trait", + "bitflags 2.11.0", + "thiserror 1.0.69", + "tracing", + "wasmtime", + "wiggle-macro", +] + +[[package]] +name = "wiggle-generate" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f786d9d3e006152a360f1145bdc18e56ea22fd5d2356f1ddc2ecfcf7529a77b" +dependencies = [ + "anyhow", + "heck", + "proc-macro2", + "quote", + "shellexpand", + "syn 2.0.117", + "witx", +] + +[[package]] +name = "wiggle-macro" +version = "30.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceac9f94f22ccc0485aeab08187b9f211d1993aaf0ed6eeb8aed43314f6e717c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "wiggle-generate", +] + [[package]] name = "winapi" version = "0.3.9" @@ -8781,6 +9082,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winx" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" +dependencies = [ + "bitflags 2.11.0", + "windows-sys 0.59.0", +] + [[package]] name = "wit-bindgen" version = "0.51.0" @@ -8887,6 +9198,18 @@ dependencies = [ "wasmparser 0.244.0", ] +[[package]] +name = "witx" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" +dependencies = [ + "anyhow", + "log", + "thiserror 1.0.69", + "wast 35.0.2", +] + [[package]] name = "writeable" version = "0.6.2" diff --git a/Cargo.toml b/Cargo.toml index a81a7fdb6..5e880cf0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ members = ["crates/*"] default-members = ["crates/icp-cli"] -exclude = ["examples/icp-rust", "examples/icp-rust-recipe", "sync-plugin/poc"] +exclude = ["examples/icp-rust", "examples/icp-rust-recipe", "examples/icp-sync-plugin", "sync-plugin/poc"] resolver = "3" [workspace.package] @@ -106,6 +106,7 @@ url = { version = "2.5.4", features = ["serde"] } uuid = { version = "1.16.0", features = ["serde", "v4"] } wasmparser = "0.245.1" wasmtime = { version = "30", features = ["component-model"] } +wasmtime-wasi = { version = "30" } winreg = "0.56.0" wslpath2 = "0.1" zeroize = "1.8.1" diff --git a/crates/icp-cli/src/operations/sync.rs b/crates/icp-cli/src/operations/sync.rs index f14c81e4f..5833ecf7d 100644 --- a/crates/icp-cli/src/operations/sync.rs +++ b/crates/icp-cli/src/operations/sync.rs @@ -137,6 +137,14 @@ pub(crate) async fn sync_many( failure.canister_name, failure.canister_id, ); error!("'{}'", failure.error); + { + use std::error::Error; + let mut cause = failure.error.source(); + while let Some(err) = cause { + error!(" caused by: {err}"); + cause = err.source(); + } + } for line in &failure.progress_output { error!("{line}"); } diff --git a/crates/icp-sync-plugin/Cargo.toml b/crates/icp-sync-plugin/Cargo.toml index 6658a51d5..ae9f934ec 100644 --- a/crates/icp-sync-plugin/Cargo.toml +++ b/crates/icp-sync-plugin/Cargo.toml @@ -16,6 +16,7 @@ ic-agent.workspace = true snafu.workspace = true tokio.workspace = true wasmtime.workspace = true +wasmtime-wasi.workspace = true [lints] workspace = true diff --git a/crates/icp-sync-plugin/src/runtime.rs b/crates/icp-sync-plugin/src/runtime.rs index 857a33ebd..4e9ad7b09 100644 --- a/crates/icp-sync-plugin/src/runtime.rs +++ b/crates/icp-sync-plugin/src/runtime.rs @@ -25,6 +25,24 @@ struct HostState { allowed_dirs: Arc>, base_dir: Arc, stdio: Option>, + // WASI context required by wasm32-wasip2 components. We provide a minimal + // context with no ambient authority (no env vars, no stdio, no filesystem). + // Plugins must use the host-provided `log`, `read_file`, and `list_dir` + // imports from the sync-plugin WIT world instead. + wasi_ctx: wasmtime_wasi::WasiCtx, + wasi_table: wasmtime_wasi::ResourceTable, +} + +impl wasmtime_wasi::IoView for HostState { + fn table(&mut self) -> &mut wasmtime_wasi::ResourceTable { + &mut self.wasi_table + } +} + +impl wasmtime_wasi::WasiView for HostState { + fn ctx(&mut self) -> &mut wasmtime_wasi::WasiCtx { + &mut self.wasi_ctx + } } // `types::Host` is an empty marker trait generated for the `types` interface. @@ -161,9 +179,13 @@ pub fn run_plugin( allowed_dirs: Arc::new(allowed_dirs), base_dir: Arc::new(base_dir), stdio, + wasi_ctx: wasmtime_wasi::WasiCtxBuilder::new().build(), + wasi_table: wasmtime_wasi::ResourceTable::new(), }; let mut linker: Linker = Linker::new(&engine); + wasmtime_wasi::add_to_linker_sync(&mut linker) + .context(InstantiateSnafu { path: wasm_path.clone() })?; SyncPlugin::add_to_linker(&mut linker, |s| s) .context(InstantiateSnafu { path: wasm_path.clone() })?; diff --git a/crates/icp/src/canister/sync/plugin.rs b/crates/icp/src/canister/sync/plugin.rs index a0aacb599..557e6cb72 100644 --- a/crates/icp/src/canister/sync/plugin.rs +++ b/crates/icp/src/canister/sync/plugin.rs @@ -16,8 +16,8 @@ use super::Params; #[derive(Debug, Snafu)] pub enum PluginError { - #[snafu(display("failed to read plugin wasm file"))] - ReadWasm { source: crate::fs::IoError }, + #[snafu(display("failed to read plugin wasm at '{path}'"))] + ReadWasm { source: crate::fs::IoError, path: Utf8PathBuf }, #[snafu(display("failed to parse plugin url"))] ParseUrl { source: url::ParseError }, @@ -65,7 +65,7 @@ pub(super) async fn sync( .await .context(LogSnafu)?; } - let bytes = read(full_path.as_ref()).context(ReadWasmSnafu)?; + let bytes = read(full_path.as_ref()).context(ReadWasmSnafu { path: full_path.clone() })?; (bytes, full_path) } diff --git a/examples/icp-sync-plugin/.gitignore b/examples/icp-sync-plugin/.gitignore new file mode 100644 index 000000000..2f7896d1d --- /dev/null +++ b/examples/icp-sync-plugin/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/examples/icp-sync-plugin/Cargo.lock b/examples/icp-sync-plugin/Cargo.lock new file mode 100644 index 000000000..d50847463 --- /dev/null +++ b/examples/icp-sync-plugin/Cargo.lock @@ -0,0 +1,712 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "ar_archive_writer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" +dependencies = [ + "object", +] + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "binread" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16598dfc8e6578e9b597d9910ba2e73618385dc9f4b1d43dd92c349d6be6418f" +dependencies = [ + "binread_derive", + "lazy_static", + "rustversion", +] + +[[package]] +name = "binread_derive" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9672209df1714ee804b1f4d4f68c8eb2a90b1f7a07acf472f88ce198ef1fed" +dependencies = [ + "either", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "candid" +version = "0.10.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba5b4833b63bf7b785fecdd2cc918ed90d988fd974825d5c48e7b407c39ef38" +dependencies = [ + "anyhow", + "binread", + "byteorder", + "candid_derive", + "hex", + "ic_principal", + "leb128", + "num-bigint", + "num-traits", + "paste", + "pretty", + "serde", + "serde_bytes", + "stacker", + "thiserror 1.0.69", +] + +[[package]] +name = "candid_derive" +version = "0.10.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b60664b6324832dfb4863f3b19eb4d58819cd38fba6d3941b101213cea0d9ec" +dependencies = [ + "lazy_static", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "canister" +version = "0.1.0" +dependencies = [ + "candid", + "ic-cdk", +] + +[[package]] +name = "cc" +version = "1.2.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "ic-cdk" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057912339f889013f42b36cc0623585949ed278457efb32aef041bdc48acb111" +dependencies = [ + "candid", + "ic-cdk-executor", + "ic-cdk-macros", + "ic-error-types", + "ic0", + "pin-project-lite", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "ic-cdk-executor" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33716b730ded33690b8a704bff3533fda87d229e58046823647d28816e9bcee7" +dependencies = [ + "ic0", + "slotmap", + "smallvec", +] + +[[package]] +name = "ic-cdk-macros" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b140627c01710ac185fbc984ab1fda1781ffef4abbd952e07383350899b0952b" +dependencies = [ + "candid", + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ic-error-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbeeb3d91aa179d6496d7293becdacedfc413c825cac79fd54ea1906f003ee55" +dependencies = [ + "serde", + "strum", + "strum_macros", +] + +[[package]] +name = "ic0" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1499d08fd5be8f790d477e1865d63bab6a8d748300e141270c4296e6d5fdd6bc" + +[[package]] +name = "ic_principal" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2b6c5941dfd659e77b262342fa58ad49489367ad026255cda8c43682d0c534" +dependencies = [ + "crc32fast", + "data-encoding", + "serde", + "sha2", + "thiserror 1.0.69", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + +[[package]] +name = "libc" +version = "0.2.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pretty" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d22152487193190344590e4f30e219cf3fe140d9e7a3fdb683d82aa2c5f4156" +dependencies = [ + "arrayvec", + "typed-arena", + "unicode-width", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8" +dependencies = [ + "ar_archive_writer", + "cc", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stacker" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.117", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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 2.0.117", +] + +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/examples/icp-sync-plugin/Cargo.toml b/examples/icp-sync-plugin/Cargo.toml new file mode 100644 index 000000000..09b64f280 --- /dev/null +++ b/examples/icp-sync-plugin/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "canister" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +candid = "0.10" +ic-cdk = "0.20" diff --git a/examples/icp-sync-plugin/demo.did b/examples/icp-sync-plugin/demo.did new file mode 100644 index 000000000..7e0eb115b --- /dev/null +++ b/examples/icp-sync-plugin/demo.did @@ -0,0 +1,5 @@ +service : { + seed : (text, text) -> (); + list : () -> (vec record { text; text }) query; + count_items : () -> (nat64) query; +} diff --git a/examples/icp-sync-plugin/icp.yaml b/examples/icp-sync-plugin/icp.yaml new file mode 100644 index 000000000..3bb19f640 --- /dev/null +++ b/examples/icp-sync-plugin/icp.yaml @@ -0,0 +1,22 @@ +canisters: + - name: my-canister + build: + steps: + - type: script + commands: + - cargo build --target wasm32-unknown-unknown --release --locked + - mv target/wasm32-unknown-unknown/release/canister.wasm "$ICP_WASM_OUTPUT_PATH" + + - type: script + commands: + - command -v ic-wasm >/dev/null 2>&1 || { echo >&2 "ic-wasm not found. To install ic-wasm, see https://github.com/dfinity/ic-wasm\n"; exit 1; } + - ic-wasm "$ICP_WASM_OUTPUT_PATH" -o "$ICP_WASM_OUTPUT_PATH" metadata candid:service -f demo.did --keep-name-section + + sync: + steps: + - type: plugin + # Path to the compiled PoC plugin wasm, relative to this directory. + # Build it first: cd ../../sync-plugin/poc && cargo build --target wasm32-wasip2 --release + path: ../../sync-plugin/poc/target/wasm32-wasip2/release/icp_sync_plugin_poc.wasm + dirs: + - seed-data/ diff --git a/examples/icp-sync-plugin/seed-data/fruit-01.txt b/examples/icp-sync-plugin/seed-data/fruit-01.txt new file mode 100644 index 000000000..4c479deff --- /dev/null +++ b/examples/icp-sync-plugin/seed-data/fruit-01.txt @@ -0,0 +1 @@ +apple diff --git a/examples/icp-sync-plugin/seed-data/fruit-02.txt b/examples/icp-sync-plugin/seed-data/fruit-02.txt new file mode 100644 index 000000000..637a09b86 --- /dev/null +++ b/examples/icp-sync-plugin/seed-data/fruit-02.txt @@ -0,0 +1 @@ +banana diff --git a/examples/icp-sync-plugin/seed-data/fruit-03.txt b/examples/icp-sync-plugin/seed-data/fruit-03.txt new file mode 100644 index 000000000..44a910549 --- /dev/null +++ b/examples/icp-sync-plugin/seed-data/fruit-03.txt @@ -0,0 +1 @@ +cherry diff --git a/examples/icp-sync-plugin/src/lib.rs b/examples/icp-sync-plugin/src/lib.rs new file mode 100644 index 000000000..9cfabeb6e --- /dev/null +++ b/examples/icp-sync-plugin/src/lib.rs @@ -0,0 +1,23 @@ +use std::cell::RefCell; + +thread_local! { + static ITEMS: RefCell> = RefCell::default(); +} + +// Add a (name, content) pair to the store (called by the sync plugin for each seed file). +#[ic_cdk::update] +fn seed(name: String, content: String) { + ITEMS.with_borrow_mut(|items| items.push((name, content))); +} + +// Return all stored (name, content) pairs. +#[ic_cdk::query] +fn list() -> Vec<(String, String)> { + ITEMS.with_borrow(|items| items.clone()) +} + +// Return the number of stored items. +#[ic_cdk::query] +fn count_items() -> u64 { + ITEMS.with_borrow(|items| items.len() as u64) +} diff --git a/sync-plugin/poc/Cargo.lock b/sync-plugin/poc/Cargo.lock index 4d36d4ccb..637617b13 100644 --- a/sync-plugin/poc/Cargo.lock +++ b/sync-plugin/poc/Cargo.lock @@ -2,18 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "anyhow" version = "1.0.102" @@ -26,25 +14,25 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "ahash", + "foldhash", ] [[package]] @@ -91,10 +79,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] -name = "leb128" -version = "0.2.5" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "log" @@ -103,16 +91,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] -name = "memchr" -version = "2.8.0" +name = "macro-string" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "59a9dbbfc75d2688ed057456ce8a3ee3f48d12eec09229f560f3643b9f275653" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "once_cell" -version = "1.21.4" +name = "memchr" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "prettyplease" @@ -190,21 +183,6 @@ dependencies = [ "zmij", ] -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "spdx" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" -dependencies = [ - "smallvec", -] - [[package]] name = "syn" version = "2.0.117" @@ -228,86 +206,66 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - [[package]] name = "wasm-encoder" -version = "0.220.1" +version = "0.246.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e913f9242315ca39eff82aee0e19ee7a372155717ff0eb082c741e435ce25ed1" +checksum = "61fb705ce81adde29d2a8e99d87995e39a6e927358c91398f374474746070ef7" dependencies = [ - "leb128", + "leb128fmt", "wasmparser", ] [[package]] name = "wasm-metadata" -version = "0.220.1" +version = "0.246.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185dfcd27fa5db2e6a23906b54c28199935f71d9a27a1a27b3a88d6fee2afae7" +checksum = "e3e4c2aa916c425dcca61a6887d3e135acdee2c6d0ed51fd61c08d41ddaf62b1" dependencies = [ "anyhow", "indexmap", - "serde", - "serde_derive", - "serde_json", - "spdx", "wasm-encoder", "wasmparser", ] [[package]] name = "wasmparser" -version = "0.220.1" +version = "0.246.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d07b6a3b550fefa1a914b6d54fc175dd11c3392da11eee604e6ffc759805d25" +checksum = "71cde4757396defafd25417cfb36aa3161027d06d865b0c24baaae229aac005d" dependencies = [ - "ahash", "bitflags", - "hashbrown 0.14.5", + "hashbrown 0.16.1", "indexmap", "semver", ] [[package]] name = "wit-bindgen" -version = "0.36.0" +version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a2b3e15cd6068f233926e7d8c7c588b2ec4fb7cc7bf3824115e7c7e2a8485a3" +checksum = "7607d30e7e5e8fd5a0695f7cb8b2128829e0bf9dca7a1fe8c4d6ed3ca1058fce" dependencies = [ - "wit-bindgen-rt", + "bitflags", "wit-bindgen-rust-macro", ] [[package]] name = "wit-bindgen-core" -version = "0.36.0" +version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b632a5a0fa2409489bd49c9e6d99fcc61bb3d4ce9d1907d44662e75a28c71172" +checksum = "fda3a4ce47c08d27f575d451a60102bab5251776abd0a7a323d1f038eb6339ab" dependencies = [ "anyhow", "heck", "wit-parser", ] -[[package]] -name = "wit-bindgen-rt" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7947d0131c7c9da3f01dfde0ab8bd4c4cf3c5bd49b6dba0ae640f1fa752572ea" -dependencies = [ - "bitflags", -] - [[package]] name = "wit-bindgen-rust" -version = "0.36.0" +version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4329de4186ee30e2ef30a0533f9b3c123c019a237a7c82d692807bf1b3ee2697" +checksum = "920a1c8c0f89397431db4900a7bf7c511b78e1b7068289fe812dc76e993f1491" dependencies = [ "anyhow", "heck", @@ -321,11 +279,12 @@ dependencies = [ [[package]] name = "wit-bindgen-rust-macro" -version = "0.36.0" +version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "177fb7ee1484d113b4792cc480b1ba57664bbc951b42a4beebe573502135b1fc" +checksum = "857a143d2373abfcd31ad946393efe775ed8c90a2a365ce73c61bf38f36a1000" dependencies = [ "anyhow", + "macro-string", "prettyplease", "proc-macro2", "quote", @@ -336,9 +295,9 @@ dependencies = [ [[package]] name = "wit-component" -version = "0.220.1" +version = "0.246.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b505603761ed400c90ed30261f44a768317348e49f1864e82ecdc3b2744e5627" +checksum = "1936c26cb24b93dc36bf78fb5dc35c55cd37f66ecdc2d2663a717d9fb3ee951e" dependencies = [ "anyhow", "bitflags", @@ -355,11 +314,12 @@ dependencies = [ [[package]] name = "wit-parser" -version = "0.220.1" +version = "0.246.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae2a7999ed18efe59be8de2db9cb2b7f84d88b27818c79353dfc53131840fe1a" +checksum = "fd979042b5ff288607ccf3b314145435453f20fc67173195f91062d2289b204d" dependencies = [ "anyhow", + "hashbrown 0.16.1", "id-arena", "indexmap", "log", @@ -371,26 +331,6 @@ dependencies = [ "wasmparser", ] -[[package]] -name = "zerocopy" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zmij" version = "1.0.21" diff --git a/sync-plugin/poc/Cargo.toml b/sync-plugin/poc/Cargo.toml index d21453d8a..69aea4df0 100644 --- a/sync-plugin/poc/Cargo.toml +++ b/sync-plugin/poc/Cargo.toml @@ -8,4 +8,4 @@ publish = false crate-type = ["cdylib"] [dependencies] -wit-bindgen = { version = "0.36", features = ["realloc"] } +wit-bindgen = { version = "0.56", features = ["realloc"] } diff --git a/sync-plugin/poc/src/lib.rs b/sync-plugin/poc/src/lib.rs index 4a8a5c394..1473d0a21 100644 --- a/sync-plugin/poc/src/lib.rs +++ b/sync-plugin/poc/src/lib.rs @@ -8,34 +8,50 @@ struct Plugin; impl Guest for Plugin { fn exec(input: SyncExecInput) -> Result, String> { log(&format!( - "sync plugin: starting for canister {}", - input.canister_id + "sync plugin: starting for canister {} (environment: {})", + input.canister_id, input.environment )); let entries = list_dir("seed-data/")?; + let mut seeded = 0u32; for entry in entries { if entry.is_dir { continue; } let path = format!("seed-data/{}", entry.name); - let data = read_file(&path)?; + let content = read_file(&path)?; + let name = escape_candid_text(&entry.name); + let content_escaped = escape_candid_text(content.trim()); canister_call(&CanisterCallRequest { method: "seed".to_string(), - arg: format!( - "(\"{}\")", - data.trim().replace('\\', "\\\\").replace('"', "\\\"") - ), + arg: format!("(\"{name}\", \"{content_escaped}\")"), call_type: Some(icp::sync_plugin::types::CallType::Update), })?; log(&format!("{path}: ok")); + seeded += 1; } + // Verify via a query call that the canister received all items. + let count_result = canister_call(&CanisterCallRequest { + method: "count_items".to_string(), + arg: "()".to_string(), + call_type: Some(icp::sync_plugin::types::CallType::Query), + })?; + log(&format!( + "verified: count_items() = {}", + count_result.trim() + )); + Ok(Some(format!( - "seeded canister {} in environment {}", - input.canister_id, input.environment + "seeded {} item(s) into canister {} (environment: {})", + seeded, input.canister_id, input.environment ))) } } +fn escape_candid_text(s: &str) -> String { + s.replace('\\', "\\\\").replace('"', "\\\"") +} + export!(Plugin); From 3cd68f935d03ed875b7f4b1851d431e1e9a3d2f4 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 16 Apr 2026 16:48:28 -0400 Subject: [PATCH 04/23] feat(sync-plugin): switch to WASI preopens and add manifest files field Replace the custom `read-file` / `list-dir` / `stat` WIT imports with WASI preopens of the manifest's `dirs` entries, so plugins traverse them with standard `std::fs`. Add a new `files` manifest field whose contents the host reads and passes inline via `sync-exec-input`. Update the example canister (`set_config`, `register`, `show`) and POC plugin to exercise both paths. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/icp-sync-plugin/build.rs | 3 + crates/icp-sync-plugin/src/lib.rs | 1 - crates/icp-sync-plugin/src/runtime.rs | 151 ++++++++++------------ crates/icp-sync-plugin/src/sandbox.rs | 89 ------------- crates/icp/src/canister/sync/plugin.rs | 49 +++---- crates/icp/src/manifest/adapter/mod.rs | 2 +- crates/icp/src/manifest/adapter/plugin.rs | 32 +++-- crates/icp/src/manifest/canister.rs | 2 + docs/schemas/canister-yaml-schema.json | 14 +- docs/schemas/icp-yaml-schema.json | 14 +- examples/icp-sync-plugin/config.txt | 1 + examples/icp-sync-plugin/demo.did | 6 +- examples/icp-sync-plugin/icp.yaml | 4 +- examples/icp-sync-plugin/src/lib.rs | 26 ++-- sync-plugin/poc/src/lib.rs | 75 +++++++---- sync-plugin/sync-plugin.wit | 36 +++--- 16 files changed, 238 insertions(+), 267 deletions(-) create mode 100644 crates/icp-sync-plugin/build.rs delete mode 100644 crates/icp-sync-plugin/src/sandbox.rs create mode 100644 examples/icp-sync-plugin/config.txt diff --git a/crates/icp-sync-plugin/build.rs b/crates/icp-sync-plugin/build.rs new file mode 100644 index 000000000..aa2bc02f1 --- /dev/null +++ b/crates/icp-sync-plugin/build.rs @@ -0,0 +1,3 @@ +fn main() { + println!("cargo:rerun-if-changed=../../sync-plugin/sync-plugin.wit"); +} diff --git a/crates/icp-sync-plugin/src/lib.rs b/crates/icp-sync-plugin/src/lib.rs index 00a40be12..6c78f715e 100644 --- a/crates/icp-sync-plugin/src/lib.rs +++ b/crates/icp-sync-plugin/src/lib.rs @@ -1,4 +1,3 @@ mod runtime; -mod sandbox; pub use runtime::{RunPluginError, run_plugin}; diff --git a/crates/icp-sync-plugin/src/runtime.rs b/crates/icp-sync-plugin/src/runtime.rs index 4e9ad7b09..5ede6fac8 100644 --- a/crates/icp-sync-plugin/src/runtime.rs +++ b/crates/icp-sync-plugin/src/runtime.rs @@ -8,27 +8,22 @@ use candid::Principal; use ic_agent::Agent; use snafu::prelude::*; use tokio::sync::mpsc::Sender; - -use crate::sandbox::is_path_allowed; +use wasmtime_wasi::{DirPerms, FilePerms}; wasmtime::component::bindgen!({ world: "sync-plugin", path: "../../sync-plugin/sync-plugin.wit", }); -use icp::sync_plugin::types::CallType; +use icp::sync_plugin::types::{CallType, FileInput}; // HostState holds everything the plugin's import functions need. struct HostState { target_canister_id: Principal, agent: Arc, - allowed_dirs: Arc>, - base_dir: Arc, stdio: Option>, - // WASI context required by wasm32-wasip2 components. We provide a minimal - // context with no ambient authority (no env vars, no stdio, no filesystem). - // Plugins must use the host-provided `log`, `read_file`, and `list_dir` - // imports from the sync-plugin WIT world instead. + // WASI context. Preopened directories in this context are the only + // filesystem locations the plugin can access. wasi_ctx: wasmtime_wasi::WasiCtx, wasi_table: wasmtime_wasi::ResourceTable, } @@ -66,13 +61,7 @@ impl SyncPluginImports for HostState { .block_on(async move { match call_type { CallType::Update => agent.update(&cid, &method).with_arg(arg_bytes).await, - CallType::Query => { - agent - .query(&cid, &method) - .with_arg(arg_bytes) - .call() - .await - } + CallType::Query => agent.query(&cid, &method).with_arg(arg_bytes).call().await, } }) .map_err(|e| format!("canister call failed: {e}"))?; @@ -82,50 +71,6 @@ impl SyncPluginImports for HostState { .map_err(|e| format!("failed to decode canister response: {e}")) } - fn read_file(&mut self, path: String) -> Result { - let full_path = self.base_dir.join(&path); - let canon_std = std::fs::canonicalize(full_path.as_std_path()) - .map_err(|e| format!("failed to resolve path '{path}': {e}"))?; - let canon = Utf8PathBuf::from_path_buf(canon_std) - .map_err(|p| format!("path is not valid UTF-8: {}", p.display()))?; - - if !is_path_allowed(&canon, &self.allowed_dirs) { - return Err(format!( - "access denied: '{path}' is outside the declared dirs allowlist" - )); - } - - std::fs::read_to_string(canon.as_std_path()) - .map_err(|e| format!("failed to read file '{path}': {e}")) - } - - fn list_dir(&mut self, path: String) -> Result, String> { - let full_path = self.base_dir.join(&path); - let canon_std = std::fs::canonicalize(full_path.as_std_path()) - .map_err(|e| format!("failed to resolve path '{path}': {e}"))?; - let canon = Utf8PathBuf::from_path_buf(canon_std) - .map_err(|p| format!("path is not valid UTF-8: {}", p.display()))?; - - if !is_path_allowed(&canon, &self.allowed_dirs) { - return Err(format!( - "access denied: '{path}' is outside the declared dirs allowlist" - )); - } - - std::fs::read_dir(canon.as_std_path()) - .map_err(|e| format!("failed to read directory '{path}': {e}"))? - .map(|entry| { - let entry = entry.map_err(|e| format!("failed to read directory entry: {e}"))?; - let name = entry.file_name().to_string_lossy().into_owned(); - let is_dir = entry - .file_type() - .map_err(|e| format!("failed to get file type for '{name}': {e}"))? - .is_dir(); - Ok(DirEntry { name, is_dir }) - }) - .collect() - } - fn log(&mut self, message: String) { if let Some(tx) = &self.stdio { let _ = tx.blocking_send(message); @@ -136,16 +81,34 @@ impl SyncPluginImports for HostState { #[derive(Debug, Snafu)] pub enum RunPluginError { #[snafu(display("failed to create wasmtime engine for plugin at {path}"))] - CreateEngine { source: anyhow::Error, path: Utf8PathBuf }, + CreateEngine { + source: anyhow::Error, + path: Utf8PathBuf, + }, #[snafu(display("failed to load wasm component from {path}"))] - LoadComponent { source: anyhow::Error, path: Utf8PathBuf }, + LoadComponent { + source: anyhow::Error, + path: Utf8PathBuf, + }, + + #[snafu(display("failed to preopen directory '{dir}' for the plugin"))] + PreopenDir { + source: anyhow::Error, + dir: Utf8PathBuf, + }, #[snafu(display("failed to instantiate wasm component at {path}"))] - Instantiate { source: anyhow::Error, path: Utf8PathBuf }, + Instantiate { + source: anyhow::Error, + path: Utf8PathBuf, + }, #[snafu(display("failed to call exec() on plugin at {path}"))] - CallExec { source: anyhow::Error, path: Utf8PathBuf }, + CallExec { + source: anyhow::Error, + path: Utf8PathBuf, + }, #[snafu(display("plugin returned error: {message}"))] PluginFailed { message: String }, @@ -154,7 +117,8 @@ pub enum RunPluginError { pub fn run_plugin( wasm_path: Utf8PathBuf, base_dir: Utf8PathBuf, - allowed_dirs: Vec, + dirs: Vec, + files: Vec<(String, String)>, target_canister_id: Principal, agent: Agent, environment: String, @@ -165,38 +129,61 @@ pub fn run_plugin( let mut config = Config::new(); config.wasm_component_model(true); - let engine = - Engine::new(&config).context(CreateEngineSnafu { path: wasm_path.clone() })?; - - let component = Component::from_file(&engine, wasm_path.as_std_path()) - .context(LoadComponentSnafu { path: wasm_path.clone() })?; - - let canister_id_text = target_canister_id.to_text(); + let engine = Engine::new(&config).context(CreateEngineSnafu { + path: wasm_path.clone(), + })?; + + let component = + Component::from_file(&engine, wasm_path.as_std_path()).context(LoadComponentSnafu { + path: wasm_path.clone(), + })?; + + // Preopen each declared directory read-only. The guest sees it at the + // same relative path it used in the manifest. + let mut wasi_builder = wasmtime_wasi::WasiCtxBuilder::new(); + for dir in &dirs { + let host_path = base_dir.join(dir); + wasi_builder + .preopened_dir( + host_path.as_std_path(), + dir, + DirPerms::READ, + FilePerms::READ, + ) + .context(PreopenDirSnafu { dir: host_path })?; + } let host_state = HostState { target_canister_id, agent: Arc::new(agent), - allowed_dirs: Arc::new(allowed_dirs), - base_dir: Arc::new(base_dir), stdio, - wasi_ctx: wasmtime_wasi::WasiCtxBuilder::new().build(), + wasi_ctx: wasi_builder.build(), wasi_table: wasmtime_wasi::ResourceTable::new(), }; let mut linker: Linker = Linker::new(&engine); - wasmtime_wasi::add_to_linker_sync(&mut linker) - .context(InstantiateSnafu { path: wasm_path.clone() })?; - SyncPlugin::add_to_linker(&mut linker, |s| s) - .context(InstantiateSnafu { path: wasm_path.clone() })?; + wasmtime_wasi::add_to_linker_sync(&mut linker).context(InstantiateSnafu { + path: wasm_path.clone(), + })?; + SyncPlugin::add_to_linker(&mut linker, |s| s).context(InstantiateSnafu { + path: wasm_path.clone(), + })?; let mut store = Store::new(&engine, host_state); - let plugin = SyncPlugin::instantiate(&mut store, &component, &linker) - .context(InstantiateSnafu { path: wasm_path.clone() })?; + let plugin = + SyncPlugin::instantiate(&mut store, &component, &linker).context(InstantiateSnafu { + path: wasm_path.clone(), + })?; let input = SyncExecInput { - canister_id: canister_id_text, + canister_id: target_canister_id.to_text(), environment, + dirs, + files: files + .into_iter() + .map(|(name, content)| FileInput { name, content }) + .collect(), }; let result = plugin diff --git a/crates/icp-sync-plugin/src/sandbox.rs b/crates/icp-sync-plugin/src/sandbox.rs deleted file mode 100644 index 9742d45dc..000000000 --- a/crates/icp-sync-plugin/src/sandbox.rs +++ /dev/null @@ -1,89 +0,0 @@ -use camino::{Utf8Path, Utf8PathBuf}; - -/// Returns `true` iff `path` (already canonicalized) starts with at least one -/// of the `allowed_dirs`. -pub fn is_path_allowed(path: &Utf8Path, allowed_dirs: &[Utf8PathBuf]) -> bool { - allowed_dirs.iter().any(|dir| path.starts_with(dir)) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn dirs(paths: &[&str]) -> Vec { - paths.iter().map(|p| Utf8PathBuf::from(*p)).collect() - } - - fn path(s: &str) -> Utf8PathBuf { - Utf8PathBuf::from(s) - } - - #[test] - fn allowed_exact_dir() { - let allowed = dirs(&["/project/canister/assets"]); - assert!(is_path_allowed( - path("/project/canister/assets").as_path(), - &allowed - )); - } - - #[test] - fn allowed_file_inside_dir() { - let allowed = dirs(&["/project/canister/assets"]); - assert!(is_path_allowed( - path("/project/canister/assets/data.txt").as_path(), - &allowed - )); - } - - #[test] - fn allowed_nested_file() { - let allowed = dirs(&["/project/canister/assets"]); - assert!(is_path_allowed( - path("/project/canister/assets/subdir/data.txt").as_path(), - &allowed - )); - } - - #[test] - fn denied_outside_dir() { - let allowed = dirs(&["/project/canister/assets"]); - assert!(!is_path_allowed( - path("/project/canister/other/data.txt").as_path(), - &allowed - )); - } - - #[test] - fn denied_parent_traversal_attempt() { - // A path that looks like it goes outside — canonicalization in the - // host prevents this from reaching is_path_allowed in practice, but - // verify we handle an already-resolved traversal correctly. - let allowed = dirs(&["/project/canister/assets"]); - assert!(!is_path_allowed(path("/etc/passwd").as_path(), &allowed)); - } - - #[test] - fn denied_sibling_prefix_match() { - // "/project/canister/assets-other" must NOT be allowed just because - // "/project/canister/assets" is in the list. - let allowed = dirs(&["/project/canister/assets"]); - assert!(!is_path_allowed( - path("/project/canister/assets-other/file.txt").as_path(), - &allowed - )); - } - - #[test] - fn multiple_allowed_dirs() { - let allowed = dirs(&["/project/canister/assets", "/project/canister/config"]); - assert!(is_path_allowed( - path("/project/canister/config/settings.json").as_path(), - &allowed - )); - assert!(!is_path_allowed( - path("/project/canister/private/secret.key").as_path(), - &allowed - )); - } -} diff --git a/crates/icp/src/canister/sync/plugin.rs b/crates/icp/src/canister/sync/plugin.rs index 557e6cb72..368606db7 100644 --- a/crates/icp/src/canister/sync/plugin.rs +++ b/crates/icp/src/canister/sync/plugin.rs @@ -8,7 +8,7 @@ use tokio::sync::mpsc::Sender; use url::Url; use crate::{ - fs::{read, write}, + fs::{read, read_to_string, write}, manifest::adapter::{plugin::Adapter, prebuilt::SourceField}, }; @@ -17,7 +17,16 @@ use super::Params; #[derive(Debug, Snafu)] pub enum PluginError { #[snafu(display("failed to read plugin wasm at '{path}'"))] - ReadWasm { source: crate::fs::IoError, path: Utf8PathBuf }, + ReadWasm { + source: crate::fs::IoError, + path: Utf8PathBuf, + }, + + #[snafu(display("failed to read plugin input file at '{path}'"))] + ReadFile { + source: crate::fs::IoError, + path: Utf8PathBuf, + }, #[snafu(display("failed to parse plugin url"))] ParseUrl { source: url::ParseError }, @@ -37,9 +46,6 @@ pub enum PluginError { #[snafu(display("plugin wasm checksum mismatch, expected: {expected}, actual: {actual}"))] ChecksumMismatch { expected: String, actual: String }, - #[snafu(display("failed to canonicalize allowed dir '{dir}'"))] - CanonicalizeDirs { source: std::io::Error, dir: String }, - #[snafu(display("failed to run plugin"))] Run { source: RunPluginError }, @@ -65,7 +71,9 @@ pub(super) async fn sync( .await .context(LogSnafu)?; } - let bytes = read(full_path.as_ref()).context(ReadWasmSnafu { path: full_path.clone() })?; + let bytes = read(full_path.as_ref()).context(ReadWasmSnafu { + path: full_path.clone(), + })?; (bytes, full_path) } @@ -117,23 +125,17 @@ pub(super) async fn sync( } } - // 3. Canonicalize declared dirs relative to the canister directory. + // 3. Collect inputs: `dirs` stays as manifest strings (runtime preopens them), + // `files` are read on the host and passed inline. let base_dir = Utf8PathBuf::from(params.path.as_str()); - let allowed_dirs: Vec = adapter - .dirs - .as_deref() - .unwrap_or(&[]) - .iter() - .map(|d| { - let abs = params.path.join(d); - std::fs::canonicalize(abs.as_std_path()) - .context(CanonicalizeDirsSnafu { dir: d.clone() }) - .map(|p| { - Utf8PathBuf::from_path_buf(p) - .unwrap_or_else(|p| Utf8PathBuf::from(p.to_string_lossy().as_ref())) - }) - }) - .collect::, _>>()?; + let dirs: Vec = adapter.dirs.clone().unwrap_or_default(); + + let mut files: Vec<(String, String)> = Vec::new(); + for name in adapter.files.as_deref().unwrap_or(&[]) { + let abs = params.path.join(name); + let content = read_to_string(abs.as_ref()).context(ReadFileSnafu { path: abs })?; + files.push((name.clone(), content)); + } // 4. Run the plugin (blocking call — signal Tokio that this thread will block). let wasm_path_buf = Utf8PathBuf::from(wasm_path.as_str()); @@ -145,7 +147,8 @@ pub(super) async fn sync( run_plugin( wasm_path_buf, base_dir, - allowed_dirs, + dirs, + files, params.cid, agent_clone, environment_owned, diff --git a/crates/icp/src/manifest/adapter/mod.rs b/crates/icp/src/manifest/adapter/mod.rs index 5b29639f1..d631d1430 100644 --- a/crates/icp/src/manifest/adapter/mod.rs +++ b/crates/icp/src/manifest/adapter/mod.rs @@ -1,4 +1,4 @@ pub mod assets; pub mod plugin; -pub mod script; pub mod prebuilt; +pub mod script; diff --git a/crates/icp/src/manifest/adapter/plugin.rs b/crates/icp/src/manifest/adapter/plugin.rs index dd52077b9..b0b87f291 100644 --- a/crates/icp/src/manifest/adapter/plugin.rs +++ b/crates/icp/src/manifest/adapter/plugin.rs @@ -6,17 +6,20 @@ use super::prebuilt::SourceField; /// Configuration for a sync plugin step. /// /// A sync plugin is a WebAssembly module invoked during `icp sync` for a -/// specific canister. It runs inside the Extism sandbox with restricted -/// permissions — it can only call canister methods on the canister being -/// synced and read files from the declared `dirs` allowlist. +/// specific canister. It runs inside a WASI sandbox whose filesystem access +/// is limited to the directories listed in `dirs` (preopened read-only) plus +/// the contents of any files listed in `files` (read by the host and passed +/// inline to the plugin). /// /// Example: /// ```yaml /// - type: plugin /// path: ./plugins/populate-data.wasm /// sha256: e3b0c44298fc1c149afb... # optional but recommended -/// dirs: # optional read-access directories -/// - assets/seed-data/ +/// dirs: # directories preopened read-only +/// - assets/seed-data +/// files: # files read by the host and passed inline +/// - config.txt /// ``` #[derive(Clone, Debug, Deserialize, PartialEq, JsonSchema, Serialize)] pub struct Adapter { @@ -28,7 +31,13 @@ pub struct Adapter { pub sha256: Option, /// Directories (relative to canister directory) the plugin may read from. + /// Each entry must be a directory; it is preopened via WASI so the plugin + /// can traverse it using standard filesystem APIs. pub dirs: Option>, + + /// Files (relative to canister directory) the host reads and passes to + /// the plugin as part of `sync-exec-input.files`. + pub files: Option>, } #[cfg(test)] @@ -51,20 +60,23 @@ mod tests { }), sha256: None, dirs: None, + files: None, }, ); } #[test] - fn local_path_with_sha256_and_dirs() { + fn local_path_with_sha256_dirs_and_files() { assert_eq!( serde_yaml::from_str::( r#" path: plugins/my-sync.wasm sha256: abc123 dirs: - - assets/seed-data/ - - config/ + - assets/seed-data + - config + files: + - config.txt "# ) .expect("failed to deserialize Adapter from yaml"), @@ -73,7 +85,8 @@ mod tests { path: "plugins/my-sync.wasm".into(), }), sha256: Some("abc123".to_string()), - dirs: Some(vec!["assets/seed-data/".to_string(), "config/".to_string(),]), + dirs: Some(vec!["assets/seed-data".to_string(), "config".to_string()]), + files: Some(vec!["config.txt".to_string()]), }, ); } @@ -94,6 +107,7 @@ mod tests { }), sha256: Some("a665a45920422f9d417e".to_string()), dirs: None, + files: None, }, ); } diff --git a/crates/icp/src/manifest/canister.rs b/crates/icp/src/manifest/canister.rs index 9ba8dfdfa..48f786e13 100644 --- a/crates/icp/src/manifest/canister.rs +++ b/crates/icp/src/manifest/canister.rs @@ -768,6 +768,7 @@ mod tests { }), sha256: None, dirs: Some(vec!["assets/seed-data/".to_string()]), + files: None, } )] }), @@ -811,6 +812,7 @@ mod tests { .to_string() ), dirs: None, + files: None, })] }), }, diff --git a/docs/schemas/canister-yaml-schema.json b/docs/schemas/canister-yaml-schema.json index 1fd9b220d..8b15fc5a5 100644 --- a/docs/schemas/canister-yaml-schema.json +++ b/docs/schemas/canister-yaml-schema.json @@ -100,10 +100,20 @@ "description": "Remote url to fetch a WASM file from" } ], - "description": "Configuration for a sync plugin step.\n\nA sync plugin is a WebAssembly module invoked during `icp sync` for a\nspecific canister. It runs inside the Extism sandbox with restricted\npermissions — it can only call canister methods on the canister being\nsynced and read files from the declared `dirs` allowlist.\n\nExample:\n```yaml\n- type: plugin\n path: ./plugins/populate-data.wasm\n sha256: e3b0c44298fc1c149afb... # optional but recommended\n dirs: # optional read-access directories\n - assets/seed-data/\n```", + "description": "Configuration for a sync plugin step.\n\nA sync plugin is a WebAssembly module invoked during `icp sync` for a\nspecific canister. It runs inside a WASI sandbox whose filesystem access\nis limited to the directories listed in `dirs` (preopened read-only) plus\nthe contents of any files listed in `files` (read by the host and passed\ninline to the plugin).\n\nExample:\n```yaml\n- type: plugin\n path: ./plugins/populate-data.wasm\n sha256: e3b0c44298fc1c149afb... # optional but recommended\n dirs: # directories preopened read-only\n - assets/seed-data\n files: # files read by the host and passed inline\n - config.txt\n```", "properties": { "dirs": { - "description": "Directories (relative to canister directory) the plugin may read from.", + "description": "Directories (relative to canister directory) the plugin may read from.\nEach entry must be a directory; it is preopened via WASI so the plugin\ncan traverse it using standard filesystem APIs.", + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "files": { + "description": "Files (relative to canister directory) the host reads and passes to\nthe plugin as part of `sync-exec-input.files`.", "items": { "type": "string" }, diff --git a/docs/schemas/icp-yaml-schema.json b/docs/schemas/icp-yaml-schema.json index 12687cc29..afcbcd76d 100644 --- a/docs/schemas/icp-yaml-schema.json +++ b/docs/schemas/icp-yaml-schema.json @@ -100,10 +100,20 @@ "description": "Remote url to fetch a WASM file from" } ], - "description": "Configuration for a sync plugin step.\n\nA sync plugin is a WebAssembly module invoked during `icp sync` for a\nspecific canister. It runs inside the Extism sandbox with restricted\npermissions — it can only call canister methods on the canister being\nsynced and read files from the declared `dirs` allowlist.\n\nExample:\n```yaml\n- type: plugin\n path: ./plugins/populate-data.wasm\n sha256: e3b0c44298fc1c149afb... # optional but recommended\n dirs: # optional read-access directories\n - assets/seed-data/\n```", + "description": "Configuration for a sync plugin step.\n\nA sync plugin is a WebAssembly module invoked during `icp sync` for a\nspecific canister. It runs inside a WASI sandbox whose filesystem access\nis limited to the directories listed in `dirs` (preopened read-only) plus\nthe contents of any files listed in `files` (read by the host and passed\ninline to the plugin).\n\nExample:\n```yaml\n- type: plugin\n path: ./plugins/populate-data.wasm\n sha256: e3b0c44298fc1c149afb... # optional but recommended\n dirs: # directories preopened read-only\n - assets/seed-data\n files: # files read by the host and passed inline\n - config.txt\n```", "properties": { "dirs": { - "description": "Directories (relative to canister directory) the plugin may read from.", + "description": "Directories (relative to canister directory) the plugin may read from.\nEach entry must be a directory; it is preopened via WASI so the plugin\ncan traverse it using standard filesystem APIs.", + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "files": { + "description": "Files (relative to canister directory) the host reads and passes to\nthe plugin as part of `sync-exec-input.files`.", "items": { "type": "string" }, diff --git a/examples/icp-sync-plugin/config.txt b/examples/icp-sync-plugin/config.txt new file mode 100644 index 000000000..38f8e886e --- /dev/null +++ b/examples/icp-sync-plugin/config.txt @@ -0,0 +1 @@ +dev diff --git a/examples/icp-sync-plugin/demo.did b/examples/icp-sync-plugin/demo.did index 7e0eb115b..1f1a239ca 100644 --- a/examples/icp-sync-plugin/demo.did +++ b/examples/icp-sync-plugin/demo.did @@ -1,5 +1,5 @@ service : { - seed : (text, text) -> (); - list : () -> (vec record { text; text }) query; - count_items : () -> (nat64) query; + set_config : (text) -> (); + register : (text, text) -> (); + show : () -> (text, vec record { text; text }) query; } diff --git a/examples/icp-sync-plugin/icp.yaml b/examples/icp-sync-plugin/icp.yaml index 3bb19f640..5cfa3acfe 100644 --- a/examples/icp-sync-plugin/icp.yaml +++ b/examples/icp-sync-plugin/icp.yaml @@ -19,4 +19,6 @@ canisters: # Build it first: cd ../../sync-plugin/poc && cargo build --target wasm32-wasip2 --release path: ../../sync-plugin/poc/target/wasm32-wasip2/release/icp_sync_plugin_poc.wasm dirs: - - seed-data/ + - seed-data + files: + - config.txt diff --git a/examples/icp-sync-plugin/src/lib.rs b/examples/icp-sync-plugin/src/lib.rs index 9cfabeb6e..f7145ceb0 100644 --- a/examples/icp-sync-plugin/src/lib.rs +++ b/examples/icp-sync-plugin/src/lib.rs @@ -1,23 +1,27 @@ use std::cell::RefCell; thread_local! { - static ITEMS: RefCell> = RefCell::default(); + static CONFIG: RefCell = RefCell::default(); + static FRUITS: RefCell> = RefCell::default(); } -// Add a (name, content) pair to the store (called by the sync plugin for each seed file). +// Upload the config value (called once by the sync plugin). #[ic_cdk::update] -fn seed(name: String, content: String) { - ITEMS.with_borrow_mut(|items| items.push((name, content))); +fn set_config(value: String) { + CONFIG.with_borrow_mut(|c| *c = value); } -// Return all stored (name, content) pairs. -#[ic_cdk::query] -fn list() -> Vec<(String, String)> { - ITEMS.with_borrow(|items| items.clone()) +// Register a (name, content) fruit pair (called by the sync plugin for each file). +#[ic_cdk::update] +fn register(name: String, content: String) { + FRUITS.with_borrow_mut(|f| f.push((name, content))); } -// Return the number of stored items. +// Return the stored config and every registered fruit. #[ic_cdk::query] -fn count_items() -> u64 { - ITEMS.with_borrow(|items| items.len() as u64) +fn show() -> (String, Vec<(String, String)>) { + ( + CONFIG.with_borrow(|c| c.clone()), + FRUITS.with_borrow(|f| f.clone()), + ) } diff --git a/sync-plugin/poc/src/lib.rs b/sync-plugin/poc/src/lib.rs index 1473d0a21..9ec14881b 100644 --- a/sync-plugin/poc/src/lib.rs +++ b/sync-plugin/poc/src/lib.rs @@ -3,6 +3,9 @@ wit_bindgen::generate!({ path: "../../sync-plugin/sync-plugin.wit", }); +use std::fs; +use std::path::Path; + struct Plugin; impl Guest for Plugin { @@ -12,44 +15,68 @@ impl Guest for Plugin { input.canister_id, input.environment )); - let entries = list_dir("seed-data/")?; - let mut seeded = 0u32; - - for entry in entries { - if entry.is_dir { - continue; - } - let path = format!("seed-data/{}", entry.name); - let content = read_file(&path)?; - let name = escape_candid_text(&entry.name); - let content_escaped = escape_candid_text(content.trim()); + // 1. Upload the config value — the first file the manifest declared. + if let Some(config) = input.files.first() { canister_call(&CanisterCallRequest { - method: "seed".to_string(), - arg: format!("(\"{name}\", \"{content_escaped}\")"), + method: "set_config".to_string(), + arg: format!("(\"{}\")", escape_candid_text(config.content.trim())), call_type: Some(icp::sync_plugin::types::CallType::Update), })?; - log(&format!("{path}: ok")); - seeded += 1; + log(&format!("set_config from {}: ok", config.name)); + } + + // 2. Register every file found by traversing the preopened dirs. + let mut registered = 0u32; + for dir in &input.dirs { + registered += register_dir(Path::new(dir))?; } - // Verify via a query call that the canister received all items. - let count_result = canister_call(&CanisterCallRequest { - method: "count_items".to_string(), + // 3. Verify via a query call and display the canister state. + let shown = canister_call(&CanisterCallRequest { + method: "show".to_string(), arg: "()".to_string(), call_type: Some(icp::sync_plugin::types::CallType::Query), })?; - log(&format!( - "verified: count_items() = {}", - count_result.trim() - )); + log(&format!("show() = {}", shown.trim())); Ok(Some(format!( - "seeded {} item(s) into canister {} (environment: {})", - seeded, input.canister_id, input.environment + "registered {} item(s) in canister {} (environment: {})", + registered, input.canister_id, input.environment ))) } } +fn register_dir(dir: &Path) -> Result { + let entries = fs::read_dir(dir).map_err(|e| format!("read_dir {}: {e}", dir.display()))?; + let mut count = 0u32; + for entry in entries { + let entry = entry.map_err(|e| format!("dir entry in {}: {e}", dir.display()))?; + let path = entry.path(); + let file_type = entry + .file_type() + .map_err(|e| format!("file_type {}: {e}", path.display()))?; + if file_type.is_dir() { + count += register_dir(&path)?; + } else if file_type.is_file() { + let content = fs::read_to_string(&path) + .map_err(|e| format!("read_to_string {}: {e}", path.display()))?; + let path_str = path.to_string_lossy(); + canister_call(&CanisterCallRequest { + method: "register".to_string(), + arg: format!( + "(\"{}\", \"{}\")", + escape_candid_text(&path_str), + escape_candid_text(content.trim()) + ), + call_type: Some(icp::sync_plugin::types::CallType::Update), + })?; + log(&format!("{path_str}: ok")); + count += 1; + } + } + Ok(count) +} + fn escape_candid_text(s: &str) -> String { s.replace('\\', "\\\\").replace('"', "\\\"") } diff --git a/sync-plugin/sync-plugin.wit b/sync-plugin/sync-plugin.wit index ed6aaca00..83297d82d 100644 --- a/sync-plugin/sync-plugin.wit +++ b/sync-plugin/sync-plugin.wit @@ -5,12 +5,28 @@ interface types { /// Whether a canister call is an update or a query. enum call-type { update, query } + /// A file the host read on behalf of the plugin. + record file-input { + /// Path of the file as declared in the manifest (relative to + /// the canister directory). + name: string, + /// UTF-8 contents of the file. + content: string, + } + /// Input passed by the runtime to the plugin's exec() export. record sync-exec-input { /// Textual principal of the canister being synced. canister-id: string, /// Name of the environment being synced (e.g. "production", "local"). environment: string, + /// Directories declared in the manifest step's `dirs` setting. + /// The host preopens each entry via WASI; the plugin can traverse + /// them with standard `wasi:filesystem` (e.g. Rust's `std::fs`). + dirs: list, + /// Files declared in the manifest step's `files` setting, read by + /// the host and passed inline. The plugin decides how to use them. + files: list, } /// A request to call a method on the target canister. @@ -22,19 +38,11 @@ interface types { /// Defaults to update if omitted. call-type: option, } - - /// A single entry returned by list-dir. - record dir-entry { - /// File or directory name (not a full path). - name: string, - /// True if the entry is a directory; false if it is a file. - is-dir: bool, - } } /// The complete interface of a sync plugin. world sync-plugin { - use types.{sync-exec-input, canister-call-request, dir-entry}; + use types.{sync-exec-input, canister-call-request, file-input}; // ------------------------------------------------------------------------- // Host functions (imports) — provided by icp-cli, called by the plugin @@ -46,16 +54,6 @@ world sync-plugin { /// Returns Candid IDL text on success or an error message on failure. import canister-call: func(req: canister-call-request) -> result; - /// Read a UTF-8 file from the host filesystem. - /// The host enforces that the path falls within a declared `dirs` entry. - /// Returns the file contents or an error message. - import read-file: func(path: string) -> result; - - /// List one level of entries in a host filesystem directory. - /// The host enforces the same `dirs` allowlist as read-file. - /// Returns directory entries or an error message. - import list-dir: func(path: string) -> result, string>; - /// Print a message to the CLI's progress output. import log: func(message: string); From f69b85bdc12dc56cbfcfb185a326e016b3051e1e Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 16 Apr 2026 17:01:48 -0400 Subject: [PATCH 05/23] feat(sync-plugin): move Candid encoding into the plugin canister-call now exchanges raw Candid-encoded bytes in both directions. The host forwards arg bytes to ic-agent and returns the response bytes unchanged; plugins are responsible for encoding arguments and decoding responses. The POC plugin is updated accordingly and trimmed to just set_config + register. Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.lock | 1 - crates/icp-sync-plugin/Cargo.toml | 1 - crates/icp-sync-plugin/src/runtime.rs | 15 +- sync-plugin/poc/Cargo.lock | 464 +++++++++++++++++++++++++- sync-plugin/poc/Cargo.toml | 1 + sync-plugin/poc/src/lib.rs | 29 +- sync-plugin/sync-plugin.wit | 10 +- 7 files changed, 480 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c866c471a..bd8e111c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3843,7 +3843,6 @@ dependencies = [ "anyhow", "camino", "candid", - "candid_parser", "hex", "ic-agent", "snafu", diff --git a/crates/icp-sync-plugin/Cargo.toml b/crates/icp-sync-plugin/Cargo.toml index ae9f934ec..f5435cebc 100644 --- a/crates/icp-sync-plugin/Cargo.toml +++ b/crates/icp-sync-plugin/Cargo.toml @@ -10,7 +10,6 @@ publish.workspace = true anyhow.workspace = true camino.workspace = true candid.workspace = true -candid_parser.workspace = true hex.workspace = true ic-agent.workspace = true snafu.workspace = true diff --git a/crates/icp-sync-plugin/src/runtime.rs b/crates/icp-sync-plugin/src/runtime.rs index 5ede6fac8..b11733ebd 100644 --- a/crates/icp-sync-plugin/src/runtime.rs +++ b/crates/icp-sync-plugin/src/runtime.rs @@ -44,11 +44,8 @@ impl wasmtime_wasi::WasiView for HostState { impl icp::sync_plugin::types::Host for HostState {} impl SyncPluginImports for HostState { - fn canister_call(&mut self, req: CanisterCallRequest) -> Result { - let arg_bytes = candid_parser::parse_idl_args(&req.arg) - .map_err(|e| format!("failed to parse Candid arg: {e}"))? - .to_bytes() - .map_err(|e| format!("failed to encode Candid arg: {e}"))?; + fn canister_call(&mut self, req: CanisterCallRequest) -> Result, String> { + let arg_bytes = req.arg; let cid = self.target_canister_id; let method = req.method.clone(); @@ -57,18 +54,14 @@ impl SyncPluginImports for HostState { // We are already inside tokio::task::block_in_place (see sync/plugin.rs), // so blocking the thread here is safe. - let result = tokio::runtime::Handle::current() + tokio::runtime::Handle::current() .block_on(async move { match call_type { CallType::Update => agent.update(&cid, &method).with_arg(arg_bytes).await, CallType::Query => agent.query(&cid, &method).with_arg(arg_bytes).call().await, } }) - .map_err(|e| format!("canister call failed: {e}"))?; - - candid::IDLArgs::from_bytes(&result) - .map(|args| args.to_string()) - .map_err(|e| format!("failed to decode canister response: {e}")) + .map_err(|e| format!("canister call failed: {e}")) } fn log(&mut self, message: String) { diff --git a/sync-plugin/poc/Cargo.lock b/sync-plugin/poc/Cargo.lock index 637617b13..fe1ea820c 100644 --- a/sync-plugin/poc/Cargo.lock +++ b/sync-plugin/poc/Cargo.lock @@ -8,24 +8,200 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "ar_archive_writer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" +dependencies = [ + "object", +] + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "binread" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16598dfc8e6578e9b597d9910ba2e73618385dc9f4b1d43dd92c349d6be6418f" +dependencies = [ + "binread_derive", + "lazy_static", + "rustversion", +] + +[[package]] +name = "binread_derive" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9672209df1714ee804b1f4d4f68c8eb2a90b1f7a07acf472f88ce198ef1fed" +dependencies = [ + "either", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bitflags" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "candid" +version = "0.10.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba5b4833b63bf7b785fecdd2cc918ed90d988fd974825d5c48e7b407c39ef38" +dependencies = [ + "anyhow", + "binread", + "byteorder", + "candid_derive", + "hex", + "ic_principal", + "leb128", + "num-bigint", + "num-traits", + "paste", + "pretty", + "serde", + "serde_bytes", + "stacker", + "thiserror", +] + +[[package]] +name = "candid_derive" +version = "0.10.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b60664b6324832dfb4863f3b19eb4d58819cd38fba6d3941b101213cea0d9ec" +dependencies = [ + "lazy_static", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "cc" +version = "1.2.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "foldhash" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -47,10 +223,30 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "ic_principal" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2b6c5941dfd659e77b262342fa58ad49489367ad026255cda8c43682d0c534" +dependencies = [ + "crc32fast", + "data-encoding", + "serde", + "sha2", + "thiserror", +] + [[package]] name = "icp-sync-plugin-poc" version = "0.1.0" dependencies = [ + "candid", "wit-bindgen", ] @@ -78,12 +274,30 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + [[package]] name = "leb128fmt" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "libc" +version = "0.2.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" + [[package]] name = "log" version = "0.4.29" @@ -98,7 +312,7 @@ checksum = "59a9dbbfc75d2688ed057456ce8a3ee3f48d12eec09229f560f3643b9f275653" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -107,6 +321,61 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pretty" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d22152487193190344590e4f30e219cf3fe140d9e7a3fdb683d82aa2c5f4156" +dependencies = [ + "arrayvec", + "typed-arena", + "unicode-width", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -114,7 +383,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.117", ] [[package]] @@ -126,6 +395,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8" +dependencies = [ + "ar_archive_writer", + "cc", +] + [[package]] name = "quote" version = "1.0.45" @@ -135,6 +414,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "semver" version = "1.0.28" @@ -148,6 +433,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", ] [[package]] @@ -167,7 +463,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -183,6 +479,47 @@ dependencies = [ "zmij", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "stacker" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.117" @@ -194,18 +531,62 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasm-encoder" version = "0.246.2" @@ -240,6 +621,79 @@ dependencies = [ "semver", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "wit-bindgen" version = "0.56.0" @@ -271,7 +725,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -288,7 +742,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] diff --git a/sync-plugin/poc/Cargo.toml b/sync-plugin/poc/Cargo.toml index 69aea4df0..236f0fc35 100644 --- a/sync-plugin/poc/Cargo.toml +++ b/sync-plugin/poc/Cargo.toml @@ -8,4 +8,5 @@ publish = false crate-type = ["cdylib"] [dependencies] +candid = "0.10.19" wit-bindgen = { version = "0.56", features = ["realloc"] } diff --git a/sync-plugin/poc/src/lib.rs b/sync-plugin/poc/src/lib.rs index 9ec14881b..6328ca0bf 100644 --- a/sync-plugin/poc/src/lib.rs +++ b/sync-plugin/poc/src/lib.rs @@ -6,6 +6,8 @@ wit_bindgen::generate!({ use std::fs; use std::path::Path; +use candid::Encode; + struct Plugin; impl Guest for Plugin { @@ -17,9 +19,11 @@ impl Guest for Plugin { // 1. Upload the config value — the first file the manifest declared. if let Some(config) = input.files.first() { + let arg = Encode!(&config.content.trim()) + .map_err(|e| format!("encode set_config arg: {e}"))?; canister_call(&CanisterCallRequest { method: "set_config".to_string(), - arg: format!("(\"{}\")", escape_candid_text(config.content.trim())), + arg, call_type: Some(icp::sync_plugin::types::CallType::Update), })?; log(&format!("set_config from {}: ok", config.name)); @@ -31,14 +35,6 @@ impl Guest for Plugin { registered += register_dir(Path::new(dir))?; } - // 3. Verify via a query call and display the canister state. - let shown = canister_call(&CanisterCallRequest { - method: "show".to_string(), - arg: "()".to_string(), - call_type: Some(icp::sync_plugin::types::CallType::Query), - })?; - log(&format!("show() = {}", shown.trim())); - Ok(Some(format!( "registered {} item(s) in canister {} (environment: {})", registered, input.canister_id, input.environment @@ -60,14 +56,13 @@ fn register_dir(dir: &Path) -> Result { } else if file_type.is_file() { let content = fs::read_to_string(&path) .map_err(|e| format!("read_to_string {}: {e}", path.display()))?; - let path_str = path.to_string_lossy(); + let path_str = path.to_string_lossy().into_owned(); + let content_trimmed = content.trim(); + let arg = Encode!(&path_str, &content_trimmed) + .map_err(|e| format!("encode register arg: {e}"))?; canister_call(&CanisterCallRequest { method: "register".to_string(), - arg: format!( - "(\"{}\", \"{}\")", - escape_candid_text(&path_str), - escape_candid_text(content.trim()) - ), + arg, call_type: Some(icp::sync_plugin::types::CallType::Update), })?; log(&format!("{path_str}: ok")); @@ -77,8 +72,4 @@ fn register_dir(dir: &Path) -> Result { Ok(count) } -fn escape_candid_text(s: &str) -> String { - s.replace('\\', "\\\\").replace('"', "\\\"") -} - export!(Plugin); diff --git a/sync-plugin/sync-plugin.wit b/sync-plugin/sync-plugin.wit index 83297d82d..3161aa0c5 100644 --- a/sync-plugin/sync-plugin.wit +++ b/sync-plugin/sync-plugin.wit @@ -33,8 +33,9 @@ interface types { record canister-call-request { /// The canister method to call. method: string, - /// Candid IDL text notation, e.g. `("hello")`. - arg: string, + /// Candid-encoded argument bytes. The plugin is responsible for + /// encoding; the host forwards these bytes unchanged. + arg: list, /// Defaults to update if omitted. call-type: option, } @@ -51,8 +52,9 @@ world sync-plugin { /// Make an update or query call to the canister being synced. /// The host always calls the canister from sync-exec-input.canister-id; /// the plugin does not choose the target. - /// Returns Candid IDL text on success or an error message on failure. - import canister-call: func(req: canister-call-request) -> result; + /// Returns the raw Candid-encoded response bytes on success or an error + /// message on failure. The plugin is responsible for decoding. + import canister-call: func(req: canister-call-request) -> result, string>; /// Print a message to the CLI's progress output. import log: func(message: string); From 6dd648fb1cf8b4c787336176c9652c8cea85c2e7 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 16 Apr 2026 17:10:51 -0400 Subject: [PATCH 06/23] chore(deps): upgrade wasmtime from 30 to 41 Highest wasmtime version compatible with Rust 1.90.0 (42+ requires 1.91.0). Adapts to API breakage: WasiView::ctx now returns WasiCtxView, add_to_linker_sync moved to the p2 module, and component bindgen requires a HasData marker. Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.lock | 611 +++++++++++++------------- Cargo.toml | 4 +- crates/icp-sync-plugin/src/runtime.rs | 25 +- 3 files changed, 323 insertions(+), 317 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bd8e111c9..aba43374a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "addr2line" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ "gimli", ] @@ -200,7 +200,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" dependencies = [ - "object 0.37.3", + "object", ] [[package]] @@ -462,7 +462,7 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4888bf91cce63baf1670512d0f12b5d636179a4abbad6504812ac8ab124b3efe" dependencies = [ - "dirs 6.0.0", + "dirs", "git2", "terminal-prompt", ] @@ -581,12 +581,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -702,6 +696,15 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + [[package]] name = "bitvec" version = "1.0.1" @@ -769,7 +772,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee04c4c84f1f811b017f2fbb7dd8815c976e7ca98593de9c1e2afad0f636bff4" dependencies = [ - "base64 0.22.1", + "base64", "bollard-stubs", "bytes", "futures-core", @@ -1477,33 +1480,36 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.117.2" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b83fcf2fc1c8954561490d02079b496fd0c757da88129981e15bfe3a548229" +checksum = "50a04121a197fde2fe896f8e7cac9812fc41ed6ee9c63e1906090f9f497845f6" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.117.2" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7496a6e92b5cee48c5d772b0443df58816dee30fed6ba19b2a28e78037ecedf" +checksum = "a09e699a94f477303820fb2167024f091543d6240783a2d3b01a3f21c42bc744" +dependencies = [ + "cranelift-srcgen", +] [[package]] name = "cranelift-bforest" -version = "0.117.2" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73a9dc0a8d3d49ee772101924968830f1c1937d650c571d3c2dd69dc36a68f41" +checksum = "f07732c662a9755529e332d86f8c5842171f6e98ba4d5976a178043dad838654" dependencies = [ "cranelift-entity", ] [[package]] name = "cranelift-bitset" -version = "0.117.2" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "573c641174c40ef31021ae4a5a3ad78974e280633502d0dfc6e362385e0c100f" +checksum = "18391da761cf362a06def7a7cf11474d79e55801dd34c2e9ba105b33dc0aef88" dependencies = [ "serde", "serde_derive", @@ -1511,9 +1517,9 @@ dependencies = [ [[package]] name = "cranelift-codegen" -version = "0.117.2" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d7c94d572615156f2db682181cadbd96342892c31e08cc26a757344319a9220" +checksum = "0b3a09b3042c69810d255aef59ddc3b3e4c0644d1d90ecfd6e3837798cc88a3c" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -1533,39 +1539,42 @@ dependencies = [ "serde", "smallvec", "target-lexicon", + "wasmtime-internal-math", ] [[package]] name = "cranelift-codegen-meta" -version = "0.117.2" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beecd9fcf2c3e06da436d565de61a42676097ea6eb6b4499346ac6264b6bb9ce" +checksum = "75817926ec812241889208d1b190cadb7fedded4592a4bb01b8524babb9e4849" dependencies = [ - "cranelift-assembler-x64", + "cranelift-assembler-x64-meta", "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", "pulley-interpreter", ] [[package]] name = "cranelift-codegen-shared" -version = "0.117.2" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4ff8d2e1235f2d6e7fc3c6738be6954ba972cd295f09079ebffeca2f864e22" +checksum = "859158f87a59476476eda3884d883c32e08a143cf3d315095533b362a3250a63" [[package]] name = "cranelift-control" -version = "0.117.2" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "001312e9fbc7d9ca9517474d6fe71e29d07e52997fd7efe18f19e8836446ceb2" +checksum = "03b65a9aec442d715cbf54d14548b8f395476c09cef7abe03e104a378291ab88" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.117.2" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb0fd6d4aae680275fcbceb08683416b744e65c8b607352043d3f0951d72b3b2" +checksum = "8334c99a7e86060c24028732efd23bac84585770dcb752329c69f135d64f2fc1" dependencies = [ "cranelift-bitset", "serde", @@ -1574,9 +1583,9 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.117.2" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fd44e7e5dcea20ca104d45894748205c51365ce4cdb18f4418e3ba955971d1b" +checksum = "43ac6c095aa5b3e845d7ca3461e67e2b65249eb5401477a5ff9100369b745111" dependencies = [ "cranelift-codegen", "log", @@ -1586,21 +1595,27 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.117.2" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f900e0a3847d51eed0321f0777947fb852ccfce0da7fb070100357f69a2f37fc" +checksum = "69d3d992870ed4f0f2e82e2175275cb3a123a46e9660c6558c46417b822c91fa" [[package]] name = "cranelift-native" -version = "0.117.2" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7617f13f392ebb63c5126258aca8b8eca739636ca7e4eeee301d3eff68489a6a" +checksum = "ee32e36beaf80f309edb535274cfe0349e1c5cf5799ba2d9f42e828285c6b52e" dependencies = [ "cranelift-codegen", "libc", "target-lexicon", ] +[[package]] +name = "cranelift-srcgen" +version = "0.128.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "903adeaf4938e60209a97b53a2e4326cd2d356aab9764a1934630204bae381c9" + [[package]] name = "crc32fast" version = "1.5.0" @@ -1954,7 +1969,7 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" dependencies = [ - "dirs-sys 0.5.0", + "dirs-sys", ] [[package]] @@ -1967,22 +1982,13 @@ dependencies = [ "dirs-sys-next", ] -[[package]] -name = "dirs" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" -dependencies = [ - "dirs-sys 0.3.7", -] - [[package]] name = "dirs" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "dirs-sys 0.5.0", + "dirs-sys", ] [[package]] @@ -1995,17 +2001,6 @@ dependencies = [ "dirs-sys-next", ] -[[package]] -name = "dirs-sys" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" -dependencies = [ - "libc", - "redox_users 0.4.6", - "winapi", -] - [[package]] name = "dirs-sys" version = "0.5.0" @@ -2647,25 +2642,17 @@ dependencies = [ "slab", ] -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - [[package]] name = "fxprof-processed-profile" -version = "0.6.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27d12c0aed7f1e24276a241aadc4cb8ea9f83000f34bc062b7cc2d51e3b0fabd" +checksum = "25234f20a3ec0a962a61770cfe39ecf03cb529a6e474ad8cff025ed497eda557" dependencies = [ "bitflags 2.11.0", "debugid", - "fxhash", + "rustc-hash 2.1.1", "serde", + "serde_derive", "serde_json", ] @@ -2723,9 +2710,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" dependencies = [ "fallible-iterator", "indexmap", @@ -3306,7 +3293,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-channel", "futures-util", @@ -4016,6 +4003,20 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "im-rc" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1955a75fa080c677d3972822ec4bad316169ab1cfc6c257a942c2265dbe5fe" +dependencies = [ + "bitmaps", + "rand_core 0.6.4", + "rand_xoshiro", + "sized-chunks", + "typenum", + "version_check", +] + [[package]] name = "image" version = "0.25.10" @@ -4157,15 +4158,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.14.0" @@ -5094,9 +5086,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.7" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "crc32fast", "hashbrown 0.15.5", @@ -5104,15 +5096,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.21.4" @@ -5333,7 +5316,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ - "base64 0.22.1", + "base64", "serde_core", ] @@ -5745,13 +5728,25 @@ dependencies = [ [[package]] name = "pulley-interpreter" -version = "30.0.2" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0ecb9823083f71df8735f21f6c44f2f2b55986d674802831df20f27e26c907" +checksum = "e9812652c1feb63cf39f8780cecac154a32b22b3665806c733cd4072547233a4" dependencies = [ "cranelift-bitset", "log", - "wasmtime-math", + "pulley-macros", + "wasmtime-internal-math", +] + +[[package]] +name = "pulley-macros" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56000349b6896e3d44286eb9c330891237f40b27fd43c1ccc84547d0b463cb40" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -5919,6 +5914,15 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "rangemap" version = "1.7.1" @@ -6031,9 +6035,9 @@ dependencies = [ [[package]] name = "regalloc2" -version = "0.11.2" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc06e6b318142614e4a48bc725abbf08ff166694835c43c9dae5a9009704639a" +checksum = "08effbc1fa53aaebff69521a5c05640523fab037b34a4a2c109506bc938246fa" dependencies = [ "allocator-api2", "bumpalo", @@ -6101,7 +6105,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "encoding_rs", "futures-channel", @@ -6870,15 +6874,6 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" -[[package]] -name = "shellexpand" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" -dependencies = [ - "dirs 4.0.0", -] - [[package]] name = "shellwords" version = "1.1.0" @@ -6945,6 +6940,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + [[package]] name = "slab" version = "0.4.12" @@ -7033,12 +7038,6 @@ dependencies = [ "der", ] -[[package]] -name = "sptr" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" - [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -7587,7 +7586,6 @@ dependencies = [ "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", - "toml_write", "winnow 0.7.15", ] @@ -7612,12 +7610,6 @@ dependencies = [ "winnow 1.0.0", ] -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - [[package]] name = "toml_writer" version = "1.0.7+spec-1.1.0" @@ -7726,17 +7718,6 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "trait-variant" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "try-lock" version = "0.2.5" @@ -8041,14 +8022,35 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-compose" +version = "0.243.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af801b6f36459023eaec63fdbaedad2fd5a4ab7dc74ecc110a8b5d375c5775e4" +dependencies = [ + "anyhow", + "heck", + "im-rc", + "indexmap", + "log", + "petgraph", + "serde", + "serde_derive", + "serde_yaml", + "smallvec", + "wasm-encoder 0.243.0", + "wasmparser 0.243.0", + "wat", +] + [[package]] name = "wasm-encoder" -version = "0.224.1" +version = "0.243.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ab7a13a23790fe91ea4eb7526a1f3131001d874e3e00c2976c48861f2e82920" +checksum = "c55db9c896d70bd9fa535ce83cd4e1f2ec3726b0edd2142079f594fc3be1cb35" dependencies = [ - "leb128", - "wasmparser 0.224.1", + "leb128fmt", + "wasmparser 0.243.0", ] [[package]] @@ -8098,9 +8100,9 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.224.1" +version = "0.243.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04f17a5917c2ddd3819e84c661fae0d6ba29d7b9c1f0e96c708c65a9c4188e11" +checksum = "f6d8db401b0528ec316dfbe579e6ab4152d61739cfe076706d2009127970159d" dependencies = [ "bitflags 2.11.0", "hashbrown 0.15.5", @@ -8147,20 +8149,20 @@ dependencies = [ [[package]] name = "wasmprinter" -version = "0.224.1" +version = "0.243.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0095b53a3b09cbc2f90f789ea44aa1b17ecc2dad8b267e657c7391f3ded6293d" +checksum = "eb2b6035559e146114c29a909a3232928ee488d6507a1504d8934e8607b36d7b" dependencies = [ "anyhow", "termcolor", - "wasmparser 0.224.1", + "wasmparser 0.243.0", ] [[package]] name = "wasmtime" -version = "30.0.2" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809cc8780708f1deed0a7c3fcab46954f0e8c08a6fe0252772481fbc88fcf946" +checksum = "e2a83182bf04af87571b4c642300479501684f26bab5597f68f68cded5b098fd" dependencies = [ "addr2line", "anyhow", @@ -8170,6 +8172,7 @@ dependencies = [ "cc", "cfg-if", "encoding_rs", + "futures", "fxprof-processed-profile", "gimli", "hashbrown 0.15.5", @@ -8179,98 +8182,113 @@ dependencies = [ "log", "mach2", "memfd", - "object 0.36.7", + "object", "once_cell", - "paste", "postcard", - "psm", "pulley-interpreter", "rayon", - "rustix 0.38.44", + "rustix 1.1.4", "semver", "serde", "serde_derive", "serde_json", "smallvec", - "sptr", "target-lexicon", - "trait-variant", - "wasm-encoder 0.224.1", - "wasmparser 0.224.1", - "wasmtime-asm-macros", - "wasmtime-cache", - "wasmtime-component-macro", - "wasmtime-component-util", - "wasmtime-cranelift", + "tempfile", + "wasm-compose", + "wasm-encoder 0.243.0", + "wasmparser 0.243.0", "wasmtime-environ", - "wasmtime-fiber", - "wasmtime-jit-debug", - "wasmtime-jit-icache-coherence", - "wasmtime-math", - "wasmtime-slab", - "wasmtime-versioned-export-macros", - "wasmtime-winch", + "wasmtime-internal-cache", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-math", + "wasmtime-internal-slab", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "wasmtime-internal-winch", "wat", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] -name = "wasmtime-asm-macros" -version = "30.0.2" +name = "wasmtime-environ" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "236964b6b35af0f08879c9c56dbfbc5adc12e8d624672341a0121df31adaa3fa" +checksum = "cb201c41aa23a3642365cfb2e4a183573d85127a3c9d528f56b9997c984541ab" dependencies = [ - "cfg-if", + "anyhow", + "cpp_demangle", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "indexmap", + "log", + "object", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasm-encoder 0.243.0", + "wasmparser 0.243.0", + "wasmprinter", + "wasmtime-internal-component-util", ] [[package]] -name = "wasmtime-cache" -version = "30.0.2" +name = "wasmtime-internal-cache" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5d75ac36ee28647f6d871a93eefc7edcb729c3096590031ba50857fac44fa8" +checksum = "fb5b3069d1a67ba5969d0eb1ccd7e141367d4e713f4649aa90356c98e8f19bea" dependencies = [ - "anyhow", - "base64 0.21.7", + "base64", "directories-next", "log", "postcard", - "rustix 0.38.44", + "rustix 1.1.4", "serde", "serde_derive", "sha2 0.10.9", - "toml 0.8.23", - "windows-sys 0.59.0", + "toml 0.9.12+spec-1.1.0", + "wasmtime-environ", + "windows-sys 0.61.2", "zstd", ] [[package]] -name = "wasmtime-component-macro" -version = "30.0.2" +name = "wasmtime-internal-component-macro" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2581ef04bf33904db9a902ffb558e7b2de534d6a4881ee985ea833f187a78fdf" +checksum = "0c924400db7b6ca996fef1b23beb0f41d5c809836b1ec60fc25b4057e2d25d9b" dependencies = [ "anyhow", "proc-macro2", "quote", "syn 2.0.117", - "wasmtime-component-util", - "wasmtime-wit-bindgen", - "wit-parser 0.224.1", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser 0.243.0", ] [[package]] -name = "wasmtime-component-util" -version = "30.0.2" +name = "wasmtime-internal-component-util" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a7108498a8a0afc81c7d2d81b96cdc509cd631d7bbaa271b7db5137026f10e3" +checksum = "7d3f65daf4bf3d74ca2fbbe20af0589c42e2b398a073486451425d94fd4afef4" [[package]] -name = "wasmtime-cranelift" -version = "30.0.2" +name = "wasmtime-internal-cranelift" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abcc9179097235c91f299a8ff56b358ee921266b61adff7d14d6e48428954dd2" +checksum = "633e889cdae76829738db0114ab3b02fce51ea4a1cd9675a67a65fce92e8b418" dependencies = [ - "anyhow", "cfg-if", "cranelift-codegen", "cranelift-control", @@ -8278,115 +8296,133 @@ dependencies = [ "cranelift-frontend", "cranelift-native", "gimli", - "itertools 0.12.1", + "itertools 0.14.0", "log", - "object 0.36.7", + "object", "pulley-interpreter", "smallvec", "target-lexicon", - "thiserror 1.0.69", - "wasmparser 0.224.1", + "thiserror 2.0.18", + "wasmparser 0.243.0", "wasmtime-environ", - "wasmtime-versioned-export-macros", + "wasmtime-internal-math", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", ] [[package]] -name = "wasmtime-environ" -version = "30.0.2" +name = "wasmtime-internal-fiber" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e90f6cba665939381839bbf2ddf12d732fca03278867910348ef1281b700954" +checksum = "deb126adc5d0c72695cfb77260b357f1b81705a0f8fa30b3944e7c2219c17341" dependencies = [ - "anyhow", - "cpp_demangle", - "cranelift-bitset", - "cranelift-entity", - "gimli", - "indexmap", - "log", - "object 0.36.7", - "postcard", - "rustc-demangle", - "semver", - "serde", - "serde_derive", - "smallvec", - "target-lexicon", - "wasm-encoder 0.224.1", - "wasmparser 0.224.1", - "wasmprinter", - "wasmtime-component-util", -] - -[[package]] -name = "wasmtime-fiber" -version = "30.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5c2ac21f0b39d72d2dac198218a12b3ddeb4ab388a8fa0d2e429855876783c" -dependencies = [ - "anyhow", "cc", "cfg-if", - "rustix 0.38.44", - "wasmtime-asm-macros", - "wasmtime-versioned-export-macros", - "windows-sys 0.59.0", + "libc", + "rustix 1.1.4", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", ] [[package]] -name = "wasmtime-jit-debug" -version = "30.0.2" +name = "wasmtime-internal-jit-debug" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74812989369947f4f5a33f4ae8ff551eb6c8a97ff55e0269a9f5f0fac93cd755" +checksum = "8e66ff7f90a8002187691ff6237ffd09f954a0ebb9de8b2ff7f5c62632134120" dependencies = [ "cc", - "object 0.36.7", - "rustix 0.38.44", - "wasmtime-versioned-export-macros", + "object", + "rustix 1.1.4", + "wasmtime-internal-versioned-export-macros", ] [[package]] -name = "wasmtime-jit-icache-coherence" -version = "30.0.2" +name = "wasmtime-internal-jit-icache-coherence" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f180cc0d2745e3a5df5d02231cd3046f49c75512eaa987b8202363b112e125d" +checksum = "4b96df23179ae16d54fb3a420f84ffe4383ec9dd06fad3e5bc782f85f66e8e08" dependencies = [ "anyhow", "cfg-if", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] -name = "wasmtime-math" -version = "30.0.2" +name = "wasmtime-internal-math" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5f04c5dcf5b2f88f81cfb8d390294b2f67109dc4d0197ea7303c60a092df27c" +checksum = "86d1380926682b44c383e9a67f47e7a95e60c6d3fa8c072294dab2c7de6168a0" dependencies = [ "libm", ] [[package]] -name = "wasmtime-slab" -version = "30.0.2" +name = "wasmtime-internal-slab" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9681707f1ae9a4708ca22058722fca5c135775c495ba9b9624fe3732b94c97" +checksum = "9b63cbea1c0192c7feb7c0dfb35f47166988a3742f29f46b585ef57246c65764" [[package]] -name = "wasmtime-versioned-export-macros" -version = "30.0.2" +name = "wasmtime-internal-unwinder" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd2fe69d04986a12fc759d2e79494100d600adcb3bb79e63dedfc8e6bb2ab03e" +checksum = "f25c392c7e5fb891a7416e3c34cfbd148849271e8c58744fda875dde4bec4d6a" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "log", + "object", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f8b9796a3f0451a7b702508b303d654de640271ac80287176de222f187a237" dependencies = [ "proc-macro2", "quote", "syn 2.0.117", ] +[[package]] +name = "wasmtime-internal-winch" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0063e61f1d0b2c20e9cfc58361a6513d074a23c80b417aac3033724f51648a0" +dependencies = [ + "cranelift-codegen", + "gimli", + "log", + "object", + "target-lexicon", + "wasmparser 0.243.0", + "wasmtime-environ", + "wasmtime-internal-cranelift", + "winch-codegen", +] + +[[package]] +name = "wasmtime-internal-wit-bindgen" +version = "41.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "587699ca7cae16b4a234ffcc834f37e75675933d533809919b52975f5609e2ef" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "heck", + "indexmap", + "wit-parser 0.243.0", +] + [[package]] name = "wasmtime-wasi" -version = "30.0.2" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ce639c7d398586bc539ae9bba752084c1db7a49ab0f391a3230dcbcc6a64cfd" +checksum = "fc2eb9dc95baed3cd86fdfebf9f9f333337eb308bf8bd973e0c7b06d9418c35f" dependencies = [ "anyhow", "async-trait", @@ -8401,23 +8437,23 @@ dependencies = [ "futures", "io-extras", "io-lifetimes", - "rustix 0.38.44", + "rustix 1.1.4", "system-interface", - "thiserror 1.0.69", + "thiserror 2.0.18", "tokio", "tracing", "url", "wasmtime", "wasmtime-wasi-io", "wiggle", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "wasmtime-wasi-io" -version = "30.0.2" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdcad7178fddaa07786abe8ff5e043acb4bc8c8f737eb117f11e028b48d92792" +checksum = "a0b8402f1e04385071fdd96aca97cba995d7376b572e42ce5841d5b6aaf6fa30" dependencies = [ "anyhow", "async-trait", @@ -8426,35 +8462,6 @@ dependencies = [ "wasmtime", ] -[[package]] -name = "wasmtime-winch" -version = "30.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9c8eae8395d530bb00a388030de9f543528674c382326f601de47524376975" -dependencies = [ - "anyhow", - "cranelift-codegen", - "gimli", - "object 0.36.7", - "target-lexicon", - "wasmparser 0.224.1", - "wasmtime-cranelift", - "wasmtime-environ", - "winch-codegen", -] - -[[package]] -name = "wasmtime-wit-bindgen" -version = "30.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a5531455e2c55994a1540355140369bb7ec0e46d2699731c5ee9f4cf9c3f7d4" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "wit-parser 0.224.1", -] - [[package]] name = "wast" version = "35.0.2" @@ -8523,14 +8530,13 @@ checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" [[package]] name = "wiggle" -version = "30.0.2" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5a4ea7722c042a659dc70caab0b56d7f45220e8bae1241cf5ebc7ab7efb0dfb" +checksum = "a69a60bcbe1475c5dc9ec89210ade54823d44f742e283cba64f98f89697c4cec" dependencies = [ "anyhow", - "async-trait", "bitflags 2.11.0", - "thiserror 1.0.69", + "thiserror 2.0.18", "tracing", "wasmtime", "wiggle-macro", @@ -8538,24 +8544,23 @@ dependencies = [ [[package]] name = "wiggle-generate" -version = "30.0.2" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f786d9d3e006152a360f1145bdc18e56ea22fd5d2356f1ddc2ecfcf7529a77b" +checksum = "21f3dc0fd4dcfc7736434bb216179a2147835309abc09bf226736a40d484548f" dependencies = [ "anyhow", "heck", "proc-macro2", "quote", - "shellexpand", "syn 2.0.117", "witx", ] [[package]] name = "wiggle-macro" -version = "30.0.2" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceac9f94f22ccc0485aeab08187b9f211d1993aaf0ed6eeb8aed43314f6e717c" +checksum = "fea2aea744eded58ae092bf57110c27517dab7d5a300513ff13897325c5c5021" dependencies = [ "proc-macro2", "quote", @@ -8596,20 +8601,22 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winch-codegen" -version = "30.0.2" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dbd4e07bd92c7ddace2f3267bdd31d4197b5ec58c315751325d45c19bfb56df" +checksum = "c55de3ac5b8bd71e5f6c87a9e511dd3ceb194bdb58183c6a7bf21cd8c0e46fbc" dependencies = [ "anyhow", + "cranelift-assembler-x64", "cranelift-codegen", "gimli", "regalloc2", "smallvec", "target-lexicon", - "thiserror 1.0.69", - "wasmparser 0.224.1", - "wasmtime-cranelift", + "thiserror 2.0.18", + "wasmparser 0.243.0", "wasmtime-environ", + "wasmtime-internal-cranelift", + "wasmtime-internal-math", ] [[package]] @@ -9163,9 +9170,9 @@ dependencies = [ [[package]] name = "wit-parser" -version = "0.224.1" +version = "0.243.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3477d8d0acb530d76beaa8becbdb1e3face08929db275f39934963eb4f716f8" +checksum = "df983a8608e513d8997f435bb74207bf0933d0e49ca97aa9d8a6157164b9b7fc" dependencies = [ "anyhow", "id-arena", @@ -9176,7 +9183,7 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", - "wasmparser 0.224.1", + "wasmparser 0.243.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5e880cf0c..61f8b2103 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -105,8 +105,8 @@ tracing-subscriber = "0.3.20" url = { version = "2.5.4", features = ["serde"] } uuid = { version = "1.16.0", features = ["serde", "v4"] } wasmparser = "0.245.1" -wasmtime = { version = "30", features = ["component-model"] } -wasmtime-wasi = { version = "30" } +wasmtime = { version = "41", features = ["component-model"] } +wasmtime-wasi = { version = "41" } winreg = "0.56.0" wslpath2 = "0.1" zeroize = "1.8.1" diff --git a/crates/icp-sync-plugin/src/runtime.rs b/crates/icp-sync-plugin/src/runtime.rs index b11733ebd..b69d7745a 100644 --- a/crates/icp-sync-plugin/src/runtime.rs +++ b/crates/icp-sync-plugin/src/runtime.rs @@ -15,7 +15,7 @@ wasmtime::component::bindgen!({ path: "../../sync-plugin/sync-plugin.wit", }); -use icp::sync_plugin::types::{CallType, FileInput}; +use icp::sync_plugin::types::CallType; // HostState holds everything the plugin's import functions need. struct HostState { @@ -28,15 +28,12 @@ struct HostState { wasi_table: wasmtime_wasi::ResourceTable, } -impl wasmtime_wasi::IoView for HostState { - fn table(&mut self) -> &mut wasmtime_wasi::ResourceTable { - &mut self.wasi_table - } -} - impl wasmtime_wasi::WasiView for HostState { - fn ctx(&mut self) -> &mut wasmtime_wasi::WasiCtx { - &mut self.wasi_ctx + fn ctx(&mut self) -> wasmtime_wasi::WasiCtxView<'_> { + wasmtime_wasi::WasiCtxView { + ctx: &mut self.wasi_ctx, + table: &mut self.wasi_table, + } } } @@ -155,12 +152,14 @@ pub fn run_plugin( }; let mut linker: Linker = Linker::new(&engine); - wasmtime_wasi::add_to_linker_sync(&mut linker).context(InstantiateSnafu { - path: wasm_path.clone(), - })?; - SyncPlugin::add_to_linker(&mut linker, |s| s).context(InstantiateSnafu { + wasmtime_wasi::p2::add_to_linker_sync(&mut linker).context(InstantiateSnafu { path: wasm_path.clone(), })?; + SyncPlugin::add_to_linker::<_, wasmtime::component::HasSelf<_>>(&mut linker, |s| s).context( + InstantiateSnafu { + path: wasm_path.clone(), + }, + )?; let mut store = Store::new(&engine, host_state); From 55193a1fdc0f64b6abcf600fb02cd544f4efbece Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 16 Apr 2026 20:35:37 -0400 Subject: [PATCH 07/23] chore: ignore all target/ dirs from root gitignore Consolidate target/ ignore rule into root .gitignore so nested Rust workspaces don't each need their own gitignore. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 2 +- examples/icp-sync-plugin/.gitignore | 1 - sync-plugin/poc/.gitignore | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 examples/icp-sync-plugin/.gitignore delete mode 100644 sync-plugin/poc/.gitignore diff --git a/.gitignore b/.gitignore index 172c621c5..f8077b5a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -/target +target/ .DS_Store .cursor diff --git a/examples/icp-sync-plugin/.gitignore b/examples/icp-sync-plugin/.gitignore deleted file mode 100644 index 2f7896d1d..000000000 --- a/examples/icp-sync-plugin/.gitignore +++ /dev/null @@ -1 +0,0 @@ -target/ diff --git a/sync-plugin/poc/.gitignore b/sync-plugin/poc/.gitignore deleted file mode 100644 index 2f7896d1d..000000000 --- a/sync-plugin/poc/.gitignore +++ /dev/null @@ -1 +0,0 @@ -target/ From 399d6bd6b972f7d8f2de96cf62ffec26a61fdcc1 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 20 Apr 2026 10:53:01 -0400 Subject: [PATCH 08/23] refactor(sync-plugin): replace custom stdio streams with MemoryOutputPipe Drops LineBuf/PluginStdio/PluginOutputStream and the host-side log() import in favour of MemoryOutputPipe: plugin stdout/stderr are captured after exec() returns and forwarded to the progress channel. Co-Authored-By: Claude Sonnet 4.6 --- crates/icp-sync-plugin/src/runtime.rs | 27 ++++++++++++++++++--------- sync-plugin/poc/src/lib.rs | 8 ++++---- sync-plugin/sync-plugin.wit | 5 +++-- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/crates/icp-sync-plugin/src/runtime.rs b/crates/icp-sync-plugin/src/runtime.rs index b69d7745a..a88ef2b79 100644 --- a/crates/icp-sync-plugin/src/runtime.rs +++ b/crates/icp-sync-plugin/src/runtime.rs @@ -8,6 +8,7 @@ use candid::Principal; use ic_agent::Agent; use snafu::prelude::*; use tokio::sync::mpsc::Sender; +use wasmtime_wasi::p2::pipe::MemoryOutputPipe; use wasmtime_wasi::{DirPerms, FilePerms}; wasmtime::component::bindgen!({ @@ -21,7 +22,6 @@ use icp::sync_plugin::types::CallType; struct HostState { target_canister_id: Principal, agent: Arc, - stdio: Option>, // WASI context. Preopened directories in this context are the only // filesystem locations the plugin can access. wasi_ctx: wasmtime_wasi::WasiCtx, @@ -60,12 +60,6 @@ impl SyncPluginImports for HostState { }) .map_err(|e| format!("canister call failed: {e}")) } - - fn log(&mut self, message: String) { - if let Some(tx) = &self.stdio { - let _ = tx.blocking_send(message); - } - } } #[derive(Debug, Snafu)] @@ -143,10 +137,17 @@ pub fn run_plugin( .context(PreopenDirSnafu { dir: host_path })?; } + let stdout_pipe = MemoryOutputPipe::new(usize::MAX); + let stderr_pipe = MemoryOutputPipe::new(usize::MAX); + if stdio.is_some() { + wasi_builder + .stdout(stdout_pipe.clone()) + .stderr(stderr_pipe.clone()); + } + let host_state = HostState { target_canister_id, agent: Arc::new(agent), - stdio, wasi_ctx: wasi_builder.build(), wasi_table: wasmtime_wasi::ResourceTable::new(), }; @@ -182,7 +183,15 @@ pub fn run_plugin( .call_exec(&mut store, &input) .context(CallExecSnafu { path: wasm_path })?; - let stdio = store.into_data().stdio; + if let Some(tx) = &stdio { + for bytes in [stdout_pipe.contents(), stderr_pipe.contents()] { + if !bytes.is_empty() { + let s = String::from_utf8_lossy(&bytes).into_owned(); + let _ = tx.blocking_send(s); + } + } + } + match result { Ok(Some(msg)) => { if let Some(tx) = &stdio { diff --git a/sync-plugin/poc/src/lib.rs b/sync-plugin/poc/src/lib.rs index 6328ca0bf..8213f297e 100644 --- a/sync-plugin/poc/src/lib.rs +++ b/sync-plugin/poc/src/lib.rs @@ -12,10 +12,10 @@ struct Plugin; impl Guest for Plugin { fn exec(input: SyncExecInput) -> Result, String> { - log(&format!( + println!( "sync plugin: starting for canister {} (environment: {})", input.canister_id, input.environment - )); + ); // 1. Upload the config value — the first file the manifest declared. if let Some(config) = input.files.first() { @@ -26,7 +26,7 @@ impl Guest for Plugin { arg, call_type: Some(icp::sync_plugin::types::CallType::Update), })?; - log(&format!("set_config from {}: ok", config.name)); + println!("set_config from {}: ok", config.name); } // 2. Register every file found by traversing the preopened dirs. @@ -65,7 +65,7 @@ fn register_dir(dir: &Path) -> Result { arg, call_type: Some(icp::sync_plugin::types::CallType::Update), })?; - log(&format!("{path_str}: ok")); + println!("{path_str}: ok"); count += 1; } } diff --git a/sync-plugin/sync-plugin.wit b/sync-plugin/sync-plugin.wit index 3161aa0c5..5f5bb3b9a 100644 --- a/sync-plugin/sync-plugin.wit +++ b/sync-plugin/sync-plugin.wit @@ -56,8 +56,9 @@ world sync-plugin { /// message on failure. The plugin is responsible for decoding. import canister-call: func(req: canister-call-request) -> result, string>; - /// Print a message to the CLI's progress output. - import log: func(message: string); + // The plugin's stdout and stderr are captured by the host and forwarded + // to the CLI's progress output, so plugins can simply print (e.g. + // Rust's `println!` / `eprintln!`) instead of calling a host import. // ------------------------------------------------------------------------- // Plugin exports — implemented by the plugin, called by the host From 529a51b4b9fd15b26a96774264b9a94956d6d567 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 20 Apr 2026 10:55:23 -0400 Subject: [PATCH 09/23] refactor(sync-plugin): move sync-plugin.wit into icp-sync-plugin crate Keeps the WIT file alongside its host-side implementation rather than in a separate top-level directory. Update all path references in build.rs and bindgen! / wit_bindgen::generate! invocations accordingly. Co-Authored-By: Claude Sonnet 4.6 --- crates/icp-sync-plugin/build.rs | 2 +- crates/icp-sync-plugin/src/runtime.rs | 4 +--- {sync-plugin => crates/icp-sync-plugin}/sync-plugin.wit | 0 sync-plugin/poc/build.rs | 2 +- sync-plugin/poc/src/lib.rs | 2 +- 5 files changed, 4 insertions(+), 6 deletions(-) rename {sync-plugin => crates/icp-sync-plugin}/sync-plugin.wit (100%) diff --git a/crates/icp-sync-plugin/build.rs b/crates/icp-sync-plugin/build.rs index aa2bc02f1..44680d997 100644 --- a/crates/icp-sync-plugin/build.rs +++ b/crates/icp-sync-plugin/build.rs @@ -1,3 +1,3 @@ fn main() { - println!("cargo:rerun-if-changed=../../sync-plugin/sync-plugin.wit"); + println!("cargo:rerun-if-changed=sync-plugin.wit"); } diff --git a/crates/icp-sync-plugin/src/runtime.rs b/crates/icp-sync-plugin/src/runtime.rs index a88ef2b79..87976677b 100644 --- a/crates/icp-sync-plugin/src/runtime.rs +++ b/crates/icp-sync-plugin/src/runtime.rs @@ -1,6 +1,4 @@ // Host-side Component Model runtime for sync plugins. -// The WIT world is in sync-plugin/sync-plugin.wit. - use std::sync::Arc; use camino::Utf8PathBuf; @@ -13,7 +11,7 @@ use wasmtime_wasi::{DirPerms, FilePerms}; wasmtime::component::bindgen!({ world: "sync-plugin", - path: "../../sync-plugin/sync-plugin.wit", + path: "sync-plugin.wit", }); use icp::sync_plugin::types::CallType; diff --git a/sync-plugin/sync-plugin.wit b/crates/icp-sync-plugin/sync-plugin.wit similarity index 100% rename from sync-plugin/sync-plugin.wit rename to crates/icp-sync-plugin/sync-plugin.wit diff --git a/sync-plugin/poc/build.rs b/sync-plugin/poc/build.rs index aa2bc02f1..c59632705 100644 --- a/sync-plugin/poc/build.rs +++ b/sync-plugin/poc/build.rs @@ -1,3 +1,3 @@ fn main() { - println!("cargo:rerun-if-changed=../../sync-plugin/sync-plugin.wit"); + println!("cargo:rerun-if-changed=../../crates/icp-sync-plugin/sync-plugin.wit"); } diff --git a/sync-plugin/poc/src/lib.rs b/sync-plugin/poc/src/lib.rs index 8213f297e..d7b6ea415 100644 --- a/sync-plugin/poc/src/lib.rs +++ b/sync-plugin/poc/src/lib.rs @@ -1,6 +1,6 @@ wit_bindgen::generate!({ world: "sync-plugin", - path: "../../sync-plugin/sync-plugin.wit", + path: "../../crates/icp-sync-plugin/sync-plugin.wit", }); use std::fs; From 42a8c5f8c42bae945c2ecb1e9481382cfe26fb12 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 20 Apr 2026 10:57:04 -0400 Subject: [PATCH 10/23] docs(sync-plugin): update SANDBOX.md for buffered stdio and moved WIT Fix the sync-plugin.wit link to its new location in the crate. Update the stdio section: output is now buffered until exec() returns (stdout then stderr), removing stale line-buffering / 64 KiB details. Co-Authored-By: Claude Sonnet 4.6 --- crates/icp-sync-plugin/SANDBOX.md | 92 +++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 crates/icp-sync-plugin/SANDBOX.md diff --git a/crates/icp-sync-plugin/SANDBOX.md b/crates/icp-sync-plugin/SANDBOX.md new file mode 100644 index 000000000..563fd08f1 --- /dev/null +++ b/crates/icp-sync-plugin/SANDBOX.md @@ -0,0 +1,92 @@ +# Sync Plugin Sandbox + +Sync plugins are untrusted WebAssembly components. `icp-cli` runs them inside +a [wasmtime](https://wasmtime.dev/) Component Model sandbox with a deliberately +narrow capability surface. This document describes exactly what a plugin can +and cannot do at runtime. + +## Host interface + +The plugin's only guaranteed way to interact with the outside world is through +the imports declared in [`sync-plugin.wit`](sync-plugin.wit): + +- `canister-call` — update or query call against the target canister only. + The plugin does **not** choose the target; the host fixes it to the + canister being synced. + +That's it. The plugin cannot call other canisters, switch identities, or +reach the management canister. + +## Filesystem + +- The host preopens each directory listed in the manifest's `dirs:` field + **read-only** (`DirPerms::READ`, `FilePerms::READ`). +- The plugin sees each preopen at the same relative path it used in the + manifest (e.g. `dirs: ["assets"]` is visible as `assets/` inside the guest). +- Files listed in `files:` are read by the host and passed inline in + `sync-exec-input.files`; the plugin never opens them itself. +- Any path not covered by a preopen is invisible. Writes, creates, deletes, + renames, and symlinks that escape a preopen are rejected by wasmtime. + +If your plugin needs to emit files (generated code, caches), do it through +the canister or request the feature — writable preopens are not currently +supported. + +## WASI capabilities + +The host links the standard `wasi:cli/imports` world. In practice only a +subset is usable because the default `WasiCtx` denies the rest: + +**Available:** + +- `wasi:filesystem` — constrained to the read-only preopens described above. +- `wasi:io`, `wasi:clocks` (wall + monotonic), `wasi:random` — timestamps, + RNG, stream I/O. Safe to rely on (Rust's `HashMap`, `chrono`, `log`, etc. + work normally). +- `wasi:cli/exit` — `process::exit` and panics abort the guest instance + cleanly; the host reports the error and continues. +- `wasi:cli/environment` — returns **empty** env and args. Do not depend on + environment variables; use `sync-exec-input.environment` instead. +- `wasi:cli/terminal-*` — reports "not a terminal". Libraries that + auto-detect color will simply disable it. + +**Linked but effectively blocked:** + +- `wasi:sockets` (TCP, UDP, DNS) — all addresses are denied by default, so + `connect`, `bind`, and name lookups fail. Treat network as unavailable. + Plugins that need external data should fetch it via the canister. + +**Stdio:** + +- `stdin` is closed. +- `stdout` and `stderr` are captured by the host. After `exec()` returns, + stdout is forwarded to the CLI's progress output first, then stderr. + Invalid UTF-8 is replaced with U+FFFD. +- Use your language's normal print facilities (e.g. Rust's `println!` / + `eprintln!`, or any `log` / `tracing` backend that writes to stderr). + There is no separate host `log` import. + +## What this means for plugin authors + +You can: + +- Read any file under a declared `dirs:` entry. +- Use standard language features that rely on clocks, RNG, or filesystem + reads. +- Panic or exit — the host will surface the error. + +You cannot: + +- Open network connections or resolve DNS. +- Write to disk, spawn subprocesses, or read environment variables. +- Call canisters other than the one being synced. +- Escape a preopen via `..` or symlinks. + +## What this means for users + +A sync plugin is confined to reading the directories and files its manifest +step declares, plus talking to the single canister that step targets. It +cannot exfiltrate data over the network, touch files outside the declared +paths, or interact with other canisters on your behalf. Review the `dirs:` +and `files:` lists in your manifest — those define the plugin's entire view +of your project. From 51e96beac358bc67bd384f651c608e9aa16140c3 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 20 Apr 2026 11:36:52 -0400 Subject: [PATCH 11/23] refactor(sync-plugin): move PoC plugin into examples/icp-sync-plugin workspace Combined the sync plugin PoC (sync-plugin/poc/) and the example canister into a single Cargo workspace under examples/icp-sync-plugin/. The canister lives in canister/ and the plugin in plugin/. Updated icp.yaml and WIT paths accordingly; removed sync-plugin/poc from the root workspace excludes. Co-Authored-By: Claude Sonnet 4.6 --- Cargo.toml | 2 +- examples/icp-sync-plugin/Cargo.lock | 262 +++++- examples/icp-sync-plugin/Cargo.toml | 14 +- examples/icp-sync-plugin/canister/Cargo.toml | 11 + .../icp-sync-plugin/{ => canister}/src/lib.rs | 0 examples/icp-sync-plugin/icp.yaml | 6 +- .../icp-sync-plugin/plugin}/Cargo.toml | 0 examples/icp-sync-plugin/plugin/build.rs | 3 + .../icp-sync-plugin/plugin}/src/lib.rs | 2 +- sync-plugin/poc/Cargo.lock | 792 ------------------ sync-plugin/poc/build.rs | 3 - 11 files changed, 280 insertions(+), 815 deletions(-) create mode 100644 examples/icp-sync-plugin/canister/Cargo.toml rename examples/icp-sync-plugin/{ => canister}/src/lib.rs (100%) rename {sync-plugin/poc => examples/icp-sync-plugin/plugin}/Cargo.toml (100%) create mode 100644 examples/icp-sync-plugin/plugin/build.rs rename {sync-plugin/poc => examples/icp-sync-plugin/plugin}/src/lib.rs (97%) delete mode 100644 sync-plugin/poc/Cargo.lock delete mode 100644 sync-plugin/poc/build.rs diff --git a/Cargo.toml b/Cargo.toml index 61f8b2103..5bd18be97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ members = ["crates/*"] default-members = ["crates/icp-cli"] -exclude = ["examples/icp-rust", "examples/icp-rust-recipe", "examples/icp-sync-plugin", "sync-plugin/poc"] +exclude = ["examples/icp-rust", "examples/icp-rust-recipe", "examples/icp-sync-plugin"] resolver = "3" [workspace.package] diff --git a/examples/icp-sync-plugin/Cargo.lock b/examples/icp-sync-plugin/Cargo.lock index d50847463..db4c92bb1 100644 --- a/examples/icp-sync-plugin/Cargo.lock +++ b/examples/icp-sync-plugin/Cargo.lock @@ -52,6 +52,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + [[package]] name = "block-buffer" version = "0.10.4" @@ -210,12 +216,24 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "generic-array" version = "0.14.7" @@ -226,6 +244,21 @@ dependencies = [ "version_check", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + [[package]] name = "heck" version = "0.5.0" @@ -308,12 +341,44 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "icp-sync-plugin-poc" +version = "0.1.0" +dependencies = [ + "candid", + "wit-bindgen", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "lazy_static" version = "1.5.0" @@ -322,9 +387,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "leb128" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" +checksum = "6cc46bac87ef8093eed6f272babb833b6443374399985ac8ed28471ee0918545" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" @@ -332,6 +403,23 @@ version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "macro-string" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a9dbbfc75d2688ed057456ce8a3ee3f48d12eec09229f560f3643b9f275653" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "memchr" version = "2.8.0" @@ -399,6 +487,16 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -433,6 +531,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.228" @@ -473,6 +577,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "sha2" version = "0.10.9" @@ -616,9 +733,9 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicode-ident" @@ -632,12 +749,52 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wasm-encoder" +version = "0.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61fb705ce81adde29d2a8e99d87995e39a6e927358c91398f374474746070ef7" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e4c2aa916c425dcca61a6887d3e135acdee2c6d0ed51fd61c08d41ddaf62b1" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71cde4757396defafd25417cfb36aa3161027d06d865b0c24baaae229aac005d" +dependencies = [ + "bitflags", + "hashbrown 0.16.1", + "indexmap", + "semver", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -710,3 +867,100 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7607d30e7e5e8fd5a0695f7cb8b2128829e0bf9dca7a1fe8c4d6ed3ca1058fce" +dependencies = [ + "bitflags", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda3a4ce47c08d27f575d451a60102bab5251776abd0a7a323d1f038eb6339ab" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "920a1c8c0f89397431db4900a7bf7c511b78e1b7068289fe812dc76e993f1491" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "857a143d2373abfcd31ad946393efe775ed8c90a2a365ce73c61bf38f36a1000" +dependencies = [ + "anyhow", + "macro-string", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1936c26cb24b93dc36bf78fb5dc35c55cd37f66ecdc2d2663a717d9fb3ee951e" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd979042b5ff288607ccf3b314145435453f20fc67173195f91062d2289b204d" +dependencies = [ + "anyhow", + "hashbrown 0.16.1", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/icp-sync-plugin/Cargo.toml b/examples/icp-sync-plugin/Cargo.toml index 09b64f280..77fbd035e 100644 --- a/examples/icp-sync-plugin/Cargo.toml +++ b/examples/icp-sync-plugin/Cargo.toml @@ -1,11 +1,3 @@ -[package] -name = "canister" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -candid = "0.10" -ic-cdk = "0.20" +[workspace] +members = ["canister", "plugin"] +resolver = "2" diff --git a/examples/icp-sync-plugin/canister/Cargo.toml b/examples/icp-sync-plugin/canister/Cargo.toml new file mode 100644 index 000000000..09b64f280 --- /dev/null +++ b/examples/icp-sync-plugin/canister/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "canister" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +candid = "0.10" +ic-cdk = "0.20" diff --git a/examples/icp-sync-plugin/src/lib.rs b/examples/icp-sync-plugin/canister/src/lib.rs similarity index 100% rename from examples/icp-sync-plugin/src/lib.rs rename to examples/icp-sync-plugin/canister/src/lib.rs diff --git a/examples/icp-sync-plugin/icp.yaml b/examples/icp-sync-plugin/icp.yaml index 5cfa3acfe..eacd3ee5e 100644 --- a/examples/icp-sync-plugin/icp.yaml +++ b/examples/icp-sync-plugin/icp.yaml @@ -4,7 +4,7 @@ canisters: steps: - type: script commands: - - cargo build --target wasm32-unknown-unknown --release --locked + - cargo build --target wasm32-unknown-unknown --release --locked -p canister - mv target/wasm32-unknown-unknown/release/canister.wasm "$ICP_WASM_OUTPUT_PATH" - type: script @@ -16,8 +16,8 @@ canisters: steps: - type: plugin # Path to the compiled PoC plugin wasm, relative to this directory. - # Build it first: cd ../../sync-plugin/poc && cargo build --target wasm32-wasip2 --release - path: ../../sync-plugin/poc/target/wasm32-wasip2/release/icp_sync_plugin_poc.wasm + # Build it first: cd plugin && cargo build --target wasm32-wasip2 --release + path: plugin/target/wasm32-wasip2/release/icp_sync_plugin_poc.wasm dirs: - seed-data files: diff --git a/sync-plugin/poc/Cargo.toml b/examples/icp-sync-plugin/plugin/Cargo.toml similarity index 100% rename from sync-plugin/poc/Cargo.toml rename to examples/icp-sync-plugin/plugin/Cargo.toml diff --git a/examples/icp-sync-plugin/plugin/build.rs b/examples/icp-sync-plugin/plugin/build.rs new file mode 100644 index 000000000..70f9202ea --- /dev/null +++ b/examples/icp-sync-plugin/plugin/build.rs @@ -0,0 +1,3 @@ +fn main() { + println!("cargo:rerun-if-changed=../../../crates/icp-sync-plugin/sync-plugin.wit"); +} diff --git a/sync-plugin/poc/src/lib.rs b/examples/icp-sync-plugin/plugin/src/lib.rs similarity index 97% rename from sync-plugin/poc/src/lib.rs rename to examples/icp-sync-plugin/plugin/src/lib.rs index d7b6ea415..e675d7118 100644 --- a/sync-plugin/poc/src/lib.rs +++ b/examples/icp-sync-plugin/plugin/src/lib.rs @@ -1,6 +1,6 @@ wit_bindgen::generate!({ world: "sync-plugin", - path: "../../crates/icp-sync-plugin/sync-plugin.wit", + path: "../../../crates/icp-sync-plugin/sync-plugin.wit", }); use std::fs; diff --git a/sync-plugin/poc/Cargo.lock b/sync-plugin/poc/Cargo.lock deleted file mode 100644 index fe1ea820c..000000000 --- a/sync-plugin/poc/Cargo.lock +++ /dev/null @@ -1,792 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "ar_archive_writer" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" -dependencies = [ - "object", -] - -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "binread" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16598dfc8e6578e9b597d9910ba2e73618385dc9f4b1d43dd92c349d6be6418f" -dependencies = [ - "binread_derive", - "lazy_static", - "rustversion", -] - -[[package]] -name = "binread_derive" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9672209df1714ee804b1f4d4f68c8eb2a90b1f7a07acf472f88ce198ef1fed" -dependencies = [ - "either", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "bitflags" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "candid" -version = "0.10.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ba5b4833b63bf7b785fecdd2cc918ed90d988fd974825d5c48e7b407c39ef38" -dependencies = [ - "anyhow", - "binread", - "byteorder", - "candid_derive", - "hex", - "ic_principal", - "leb128", - "num-bigint", - "num-traits", - "paste", - "pretty", - "serde", - "serde_bytes", - "stacker", - "thiserror", -] - -[[package]] -name = "candid_derive" -version = "0.10.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b60664b6324832dfb4863f3b19eb4d58819cd38fba6d3941b101213cea0d9ec" -dependencies = [ - "lazy_static", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "cc" -version = "1.2.60" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "data-encoding" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "ic_principal" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2b6c5941dfd659e77b262342fa58ad49489367ad026255cda8c43682d0c534" -dependencies = [ - "crc32fast", - "data-encoding", - "serde", - "sha2", - "thiserror", -] - -[[package]] -name = "icp-sync-plugin-poc" -version = "0.1.0" -dependencies = [ - "candid", - "wit-bindgen", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.0", - "serde", - "serde_core", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "leb128" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.185" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "macro-string" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a9dbbfc75d2688ed057456ce8a3ee3f48d12eec09229f560f3643b9f275653" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", - "serde", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pretty" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d22152487193190344590e4f30e219cf3fe140d9e7a3fdb683d82aa2c5f4156" -dependencies = [ - "arrayvec", - "typed-arena", - "unicode-width", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "psm" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8" -dependencies = [ - "ar_archive_writer", - "cc", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_bytes" -version = "0.11.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "stacker" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013" -dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "windows-sys", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "typed-arena" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasm-encoder" -version = "0.246.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61fb705ce81adde29d2a8e99d87995e39a6e927358c91398f374474746070ef7" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.246.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e4c2aa916c425dcca61a6887d3e135acdee2c6d0ed51fd61c08d41ddaf62b1" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.246.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71cde4757396defafd25417cfb36aa3161027d06d865b0c24baaae229aac005d" -dependencies = [ - "bitflags", - "hashbrown 0.16.1", - "indexmap", - "semver", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "wit-bindgen" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7607d30e7e5e8fd5a0695f7cb8b2128829e0bf9dca7a1fe8c4d6ed3ca1058fce" -dependencies = [ - "bitflags", - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda3a4ce47c08d27f575d451a60102bab5251776abd0a7a323d1f038eb6339ab" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "920a1c8c0f89397431db4900a7bf7c511b78e1b7068289fe812dc76e993f1491" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "857a143d2373abfcd31ad946393efe775ed8c90a2a365ce73c61bf38f36a1000" -dependencies = [ - "anyhow", - "macro-string", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.246.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1936c26cb24b93dc36bf78fb5dc35c55cd37f66ecdc2d2663a717d9fb3ee951e" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.246.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd979042b5ff288607ccf3b314145435453f20fc67173195f91062d2289b204d" -dependencies = [ - "anyhow", - "hashbrown 0.16.1", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/sync-plugin/poc/build.rs b/sync-plugin/poc/build.rs deleted file mode 100644 index c59632705..000000000 --- a/sync-plugin/poc/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("cargo:rerun-if-changed=../../crates/icp-sync-plugin/sync-plugin.wit"); -} From 5159b3a043d596d003719e47bf2c14aaa6a7c9fd Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 20 Apr 2026 11:48:19 -0400 Subject: [PATCH 12/23] docs(sync-plugin): replace outdated docs with DESIGN.md and TODO.md Removes the stale top-level sync-plugin/ directory (design.md, plan.md) and SANDBOX.md, replacing them with a DESIGN.md that reflects the current wasmtime/WASI-based implementation and a TODO.md with remaining work items. Co-Authored-By: Claude Sonnet 4.6 --- crates/icp-sync-plugin/DESIGN.md | 302 +++++++++++++++++++++++++ crates/icp-sync-plugin/SANDBOX.md | 92 -------- crates/icp-sync-plugin/TODO.md | 22 ++ sync-plugin/design.md | 331 --------------------------- sync-plugin/plan.md | 359 ------------------------------ 5 files changed, 324 insertions(+), 782 deletions(-) create mode 100644 crates/icp-sync-plugin/DESIGN.md delete mode 100644 crates/icp-sync-plugin/SANDBOX.md create mode 100644 crates/icp-sync-plugin/TODO.md delete mode 100644 sync-plugin/design.md delete mode 100644 sync-plugin/plan.md diff --git a/crates/icp-sync-plugin/DESIGN.md b/crates/icp-sync-plugin/DESIGN.md new file mode 100644 index 000000000..13f1cae4e --- /dev/null +++ b/crates/icp-sync-plugin/DESIGN.md @@ -0,0 +1,302 @@ +# Sync Plugin System Design + +## Overview + +Sync plugins extend `icp sync` with an arbitrary post-deployment step type. +A plugin is a WebAssembly component whose `exec()` export is invoked by +`icp-cli` during sync for a specific canister. The host runs it inside a +[wasmtime](https://wasmtime.dev/) WASI sandbox with a deliberately narrow +capability surface. + +--- + +## Motivation + +The existing sync steps (`script` and `assets`) cover common patterns but +cannot express arbitrary post-deployment logic without shelling out. Shell +scripts lack structure, have unrestricted host access, and cannot be +distributed as self-contained verifiable artifacts. + +Sync plugins fill that gap: + +- Written in any language that compiles to `wasm32-wasip2` +- Distributed as a single `.wasm` component (local path or remote URL + sha256) +- Sandboxed — cannot make arbitrary syscalls, network connections, or + unrestricted filesystem access +- Can call canister methods (update and query) on **exactly one canister** — + the one being synced +- Can read files from declared directories via the WASI filesystem interface + +--- + +## Canister Manifest Syntax + +A sync plugin step is declared in `canister.yaml` under `sync.steps` with +`type: plugin`: + +```yaml +name: my-canister +build: + steps: + - type: pre-built + path: dist/my_canister.wasm + +sync: + steps: + # Local plugin + - type: plugin + path: ./plugins/populate-data.wasm + sha256: e3b0c44298fc1c149afb... # optional but recommended + dirs: # directories preopened read-only + - assets/seed-data + - config + files: # files read by the host and passed inline + - config.txt + + # Remote plugin (downloaded + verified before execution) + - type: plugin + url: https://example.com/plugins/migrate-v2.wasm + sha256: a665a45920422f9d417e... # required for remote +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `type` | `"plugin"` | yes | Identifies the step type | +| `path` | string | one of `path`/`url` | Local path to the wasm, relative to canister directory | +| `url` | string | one of `path`/`url` | Remote URL to download the wasm from | +| `sha256` | string | required for `url`, optional for `path` | SHA-256 hex digest of the wasm file | +| `dirs` | `[string]` | no | Directories (relative to canister dir) the plugin may read; each is preopened via WASI | +| `files` | `[string]` | no | Files (relative to canister dir) read by the host and passed inline in `sync-exec-input.files` | + +--- + +## Plugin Interface (WIT) + +The interface is defined in [`sync-plugin.wit`](sync-plugin.wit) — that file +is the source of truth. The world has one host-provided import and one +plugin-provided export: + +```wit +world sync-plugin { + // Host import: call the canister being synced. + import canister-call: func(req: canister-call-request) -> result, string>; + + // Plugin export: run the sync step. + export exec: func(input: sync-exec-input) -> result, string>; +} +``` + +Notable choices: + +- **`result` throughout** — all fallible functions return + `result<..., string>`, so plugins can use `?` uniformly. +- **Raw Candid bytes at the boundary** — `canister-call-request.arg` is + `list`. The plugin encodes the argument (e.g. with `candid::Encode!`) + and the host forwards bytes unchanged. The response is also raw bytes for + the plugin to decode. +- **`canister-call` takes no canister ID** — the host always calls the + canister from `sync-exec-input.canister-id`. The plugin cannot supply a + different target; the restriction is structural. +- **Filesystem access via WASI, not a host import** — plugins use standard + language APIs (`std::fs` in Rust). The host preopens the declared `dirs` + read-only; no explicit `read-file` or `list-dir` import is needed. +- **Logging via stdio, not a host import** — stdout and stderr are captured + by the host (via `MemoryOutputPipe`) and forwarded to the CLI's progress + output after `exec()` returns. Plugins use normal print facilities. +- **No generated files checked in** — `wasmtime::component::bindgen!` (host) + and `wit_bindgen::generate!` (guest) both run at build time from the WIT + file. The WIT is the sole source of truth. + +--- + +## Sandbox + +### Filesystem + +- The host preopens each directory listed in `dirs:` **read-only** + (`DirPerms::READ`, `FilePerms::READ`) via `WasiCtxBuilder::preopened_dir`. +- The plugin sees each preopen at the same relative path it used in the + manifest (e.g. `dirs: ["assets"]` is visible as `assets/` inside the guest). +- Files listed in `files:` are read by the host before plugin execution and + passed inline in `sync-exec-input.files`. The plugin accesses them from the + input struct, not from the filesystem. +- Any path not covered by a preopen is invisible. Writes, creates, deletes, + renames, and symlinks that escape a preopen are rejected by wasmtime. + +### WASI capabilities + +The host links `wasi:cli/imports` via `wasmtime_wasi::p2::add_to_linker_sync`. +The effective capability surface is: + +| Capability | Available | Notes | +|------------|-----------|-------| +| `wasi:filesystem` | read-only preopens | constrained to declared `dirs` | +| `wasi:io`, `wasi:clocks`, `wasi:random` | yes | Rust's `HashMap`, `chrono`, etc. work normally | +| `wasi:cli/exit` | yes | `process::exit` / panics abort the guest cleanly | +| `wasi:cli/environment` | empty | returns empty env and args; use `sync-exec-input.environment` | +| `wasi:cli/terminal-*` | not a terminal | color auto-detection libraries simply disable color | +| `wasi:sockets` | blocked | all addresses denied; treat network as unavailable | +| Arbitrary filesystem write | blocked | no writable preopens | +| Spawning subprocesses | blocked | no WASI process interface linked | +| Calls to other canisters | blocked | host ignores any canister ID; always calls the synced canister | + +**Stdio:** +- `stdin` is closed. +- `stdout` and `stderr` are captured with `MemoryOutputPipe`. After `exec()` + returns, stdout is forwarded to the CLI progress output first, then stderr. + Invalid UTF-8 is replaced with U+FFFD. + +### What this means for plugin authors + +You can: +- Read any file under a declared `dirs:` entry using standard filesystem APIs. +- Access inline file content from `sync-exec-input.files`. +- Use clocks, RNG, and standard language features. +- Panic or exit — the host surfaces the error and continues. + +You cannot: +- Open network connections or resolve DNS. +- Write to disk, spawn subprocesses, or read environment variables. +- Call canisters other than the one being synced. +- Escape a preopen via `..` or symlinks. + +--- + +## Crate Structure + +### `crates/icp-sync-plugin` + +Host-side Component Model runtime for sync plugins. + +``` +crates/icp-sync-plugin/ + src/ + lib.rs — public API: run_plugin(), RunPluginError + runtime.rs — wasmtime component setup, HostState, bindgen!, exec() call + sync-plugin.wit — WIT interface (source of truth) + Cargo.toml — wasmtime, wasmtime-wasi, ic-agent, candid, camino, snafu, tokio +``` + +Public function: + +```rust +pub fn run_plugin( + wasm_path: Utf8PathBuf, + base_dir: Utf8PathBuf, + dirs: Vec, + files: Vec<(String, String)>, + target_canister_id: Principal, + agent: Agent, + environment: String, + stdio: Option>, +) -> Result<(), RunPluginError> +``` + +`dirs` and `files` come directly from the manifest adapter. The runtime +preopens each `dir` from `base_dir.join(dir)` and passes `files` inline in +`SyncExecInput`. + +### `HostState` and bindgen + +```rust +wasmtime::component::bindgen!({ + world: "sync-plugin", + path: "sync-plugin.wit", +}); + +struct HostState { + target_canister_id: Principal, + agent: Arc, + wasi_ctx: wasmtime_wasi::WasiCtx, + wasi_table: wasmtime_wasi::ResourceTable, +} + +impl SyncPluginImports for HostState { + fn canister_call(&mut self, req: CanisterCallRequest) -> Result, String> { ... } +} +``` + +`HostState` implements `WasiView` so wasmtime_wasi can access the WASI context. +`canister_call` uses `tokio::runtime::Handle::current().block_on(...)` because +the caller already wraps the synchronous `run_plugin` in +`tokio::task::block_in_place`. + +### `crates/icp/src/manifest/adapter/plugin.rs` + +Deserializes the `canister.yaml` fields into: + +```rust +pub struct Adapter { + pub source: SourceField, // path: or url: + pub sha256: Option, + pub dirs: Option>, + pub files: Option>, +} +``` + +### `crates/icp/src/canister/sync/plugin.rs` + +Resolves the wasm (local read or remote HTTP fetch), verifies sha256, reads +inline files, then calls `icp_sync_plugin::run_plugin(...)`. + +--- + +## Writing a Sync Plugin (Rust) + +Plugins target `wasm32-wasip2` and use `wit_bindgen::generate!` to produce +bindings from the WIT file at build time: + +```toml +# Cargo.toml +[lib] +crate-type = ["cdylib"] + +[dependencies] +candid = "0.10" +wit-bindgen = { version = "0.56", features = ["realloc"] } +``` + +```rust +// src/lib.rs +wit_bindgen::generate!({ + world: "sync-plugin", + path: "../../../crates/icp-sync-plugin/sync-plugin.wit", +}); + +use candid::Encode; +struct Plugin; + +impl Guest for Plugin { + fn exec(input: SyncExecInput) -> Result, String> { + // Access inline files from the manifest's `files:` list. + if let Some(f) = input.files.first() { + let arg = Encode!(&f.content.trim()) + .map_err(|e| format!("encode error: {e}"))?; + canister_call(&CanisterCallRequest { + method: "set_config".to_string(), + arg, + call_type: Some(icp::sync_plugin::types::CallType::Update), + })?; + } + + // Access declared directories via standard std::fs. + for dir in &input.dirs { + // std::fs::read_dir(dir), etc. + } + + Ok(Some(format!("done for canister {}", input.canister_id))) + } +} + +export!(Plugin); +``` + +Build: + +```bash +rustup target add wasm32-wasip2 +cargo build --target wasm32-wasip2 --release +``` + +The output `.wasm` file is loaded directly by the host — no additional +tooling is required. See `examples/icp-sync-plugin/` for a working example. diff --git a/crates/icp-sync-plugin/SANDBOX.md b/crates/icp-sync-plugin/SANDBOX.md deleted file mode 100644 index 563fd08f1..000000000 --- a/crates/icp-sync-plugin/SANDBOX.md +++ /dev/null @@ -1,92 +0,0 @@ -# Sync Plugin Sandbox - -Sync plugins are untrusted WebAssembly components. `icp-cli` runs them inside -a [wasmtime](https://wasmtime.dev/) Component Model sandbox with a deliberately -narrow capability surface. This document describes exactly what a plugin can -and cannot do at runtime. - -## Host interface - -The plugin's only guaranteed way to interact with the outside world is through -the imports declared in [`sync-plugin.wit`](sync-plugin.wit): - -- `canister-call` — update or query call against the target canister only. - The plugin does **not** choose the target; the host fixes it to the - canister being synced. - -That's it. The plugin cannot call other canisters, switch identities, or -reach the management canister. - -## Filesystem - -- The host preopens each directory listed in the manifest's `dirs:` field - **read-only** (`DirPerms::READ`, `FilePerms::READ`). -- The plugin sees each preopen at the same relative path it used in the - manifest (e.g. `dirs: ["assets"]` is visible as `assets/` inside the guest). -- Files listed in `files:` are read by the host and passed inline in - `sync-exec-input.files`; the plugin never opens them itself. -- Any path not covered by a preopen is invisible. Writes, creates, deletes, - renames, and symlinks that escape a preopen are rejected by wasmtime. - -If your plugin needs to emit files (generated code, caches), do it through -the canister or request the feature — writable preopens are not currently -supported. - -## WASI capabilities - -The host links the standard `wasi:cli/imports` world. In practice only a -subset is usable because the default `WasiCtx` denies the rest: - -**Available:** - -- `wasi:filesystem` — constrained to the read-only preopens described above. -- `wasi:io`, `wasi:clocks` (wall + monotonic), `wasi:random` — timestamps, - RNG, stream I/O. Safe to rely on (Rust's `HashMap`, `chrono`, `log`, etc. - work normally). -- `wasi:cli/exit` — `process::exit` and panics abort the guest instance - cleanly; the host reports the error and continues. -- `wasi:cli/environment` — returns **empty** env and args. Do not depend on - environment variables; use `sync-exec-input.environment` instead. -- `wasi:cli/terminal-*` — reports "not a terminal". Libraries that - auto-detect color will simply disable it. - -**Linked but effectively blocked:** - -- `wasi:sockets` (TCP, UDP, DNS) — all addresses are denied by default, so - `connect`, `bind`, and name lookups fail. Treat network as unavailable. - Plugins that need external data should fetch it via the canister. - -**Stdio:** - -- `stdin` is closed. -- `stdout` and `stderr` are captured by the host. After `exec()` returns, - stdout is forwarded to the CLI's progress output first, then stderr. - Invalid UTF-8 is replaced with U+FFFD. -- Use your language's normal print facilities (e.g. Rust's `println!` / - `eprintln!`, or any `log` / `tracing` backend that writes to stderr). - There is no separate host `log` import. - -## What this means for plugin authors - -You can: - -- Read any file under a declared `dirs:` entry. -- Use standard language features that rely on clocks, RNG, or filesystem - reads. -- Panic or exit — the host will surface the error. - -You cannot: - -- Open network connections or resolve DNS. -- Write to disk, spawn subprocesses, or read environment variables. -- Call canisters other than the one being synced. -- Escape a preopen via `..` or symlinks. - -## What this means for users - -A sync plugin is confined to reading the directories and files its manifest -step declares, plus talking to the single canister that step targets. It -cannot exfiltrate data over the network, touch files outside the declared -paths, or interact with other canisters on your behalf. Review the `dirs:` -and `files:` lists in your manifest — those define the plugin's entire view -of your project. diff --git a/crates/icp-sync-plugin/TODO.md b/crates/icp-sync-plugin/TODO.md new file mode 100644 index 000000000..52f73bab7 --- /dev/null +++ b/crates/icp-sync-plugin/TODO.md @@ -0,0 +1,22 @@ +# Sync Plugin TODO + +## Wasm caching + +Cache remote plugin wasm files in `.icp/cache/` so they are not re-downloaded +on every sync. Key the cache entry on the sha256 checksum. When a remote step +has a sha256 and the cached file matches, skip the HTTP fetch entirely. + +## Plugin timeout + +Add `timeout_seconds: Option` to `manifest::adapter::plugin::Adapter` +and wire it through `sync/plugin.rs` → `run_plugin`. Use wasmtime's +epoch-based interruption (`Engine::increment_epoch` on a background thread, +`Store::set_epoch_deadline`) to interrupt a plugin that runs too long. + +## Integration tests + +Add end-to-end tests that compile the `examples/icp-sync-plugin/plugin` +against a mock canister and verify the full `run_plugin` path (wasm load, +WASI preopen, canister-call, stdio capture). Unit-level tests for sha256 +mismatch and the remote-download path in `sync/plugin.rs` would also improve +coverage. diff --git a/sync-plugin/design.md b/sync-plugin/design.md deleted file mode 100644 index c4c49ebc1..000000000 --- a/sync-plugin/design.md +++ /dev/null @@ -1,331 +0,0 @@ -# Sync Plugin System Design - -## Overview - -This document describes the design for extending `icp sync` with a new step type: -**`plugin`**. A sync plugin is a WebAssembly component whose `exec()` function is -invoked by `icp-cli` during the sync phase for a specific canister. Plugins run -inside the wasmtime sandbox with deliberately restricted permissions. - ---- - -## Motivation - -The existing sync steps (`script` and `assets`) cover common patterns, but -cannot express arbitrary post-deployment logic without shelling out. Shell -scripts lack structure, have unrestricted host access, and cannot be distributed -as self-contained verifiable artifacts. - -Sync plugins fill that gap: - -- Written in any language that targets WebAssembly (Rust, Go, C, etc.) -- Distributed as a single `.wasm` component file (local or remote URL + sha256) -- Sandboxed — cannot make arbitrary syscalls, network connections, or file - system access beyond what the host explicitly allows -- Can call canister methods (update and query) on **exactly one canister** — - the one being synced — via the `canister-call` host function -- Can read files from a declared allowlist of directories via the `read-file` - host function - ---- - -## Canister Manifest Syntax - -A sync plugin step is declared in `canister.yaml` under `sync.steps` with -`type: plugin`: - -```yaml -name: my-canister -build: - steps: - - type: pre-built - path: dist/my_canister.wasm - -sync: - steps: - # Local plugin - - type: plugin - path: ./plugins/populate-data.wasm - sha256: e3b0c44298fc1c149afb... # optional but recommended - dirs: # optional read-access directories - - assets/seed-data/ - - config/ - - # Remote plugin (downloaded + verified before execution) - - type: plugin - url: https://example.com/plugins/migrate-v2.wasm - sha256: a665a45920422f9d417e... # required for remote -``` - -**Fields**: - -| Field | Type | Required | Description | -|-------|------|----------|-------------| -| `type` | `"plugin"` | yes | Identifies the step type | -| `path` | string | one of `path`/`url` | Local path to the wasm file, relative to canister directory | -| `url` | string | one of `path`/`url` | Remote URL to download the wasm file from | -| `sha256` | string | required for `url`, optional for `path` | SHA-256 hex digest of the wasm file | -| `dirs` | `[string]` | no | Directories (relative to canister dir) the plugin may read from | - ---- - -## Plugin Interface (WIT) - -The interface is defined in [sync-plugin.wit](sync-plugin.wit) — that file is the -source of truth. Notable design choices: - -- **`result` throughout** — all fallible host functions return - `result<..., string>`, and `exec` returns `result, string>`. - This lets the guest use Rust's `?` operator directly on every host call. - -- **No JSON at the boundary** — types are encoded via the Canonical ABI, which - wasmtime handles transparently. Neither the host nor the plugin deals with - serialization. - -- **`canister-call` takes a request record, not a canister ID** — the host - always calls the canister from `sync-exec-input.canister-id`; the plugin - cannot supply a different target. The restriction is structural, not enforced - by a runtime check on a field value. - ---- - -## Host-Side Enforcement - -The host functions registered via `wasmtime::component::bindgen!` enforce all -restrictions through the host state struct — there is no way for the wasm -component to bypass them: - -### `canister-call` - -``` -Captured: target_canister_id: Principal -Enforcement: always calls target_canister_id regardless of plugin request; - plugin cannot call any other principal -``` - -### `read-file` - -``` -Captured: allowed_dirs: Vec (absolute, canonicalized) -Enforcement: canonicalize(requested_path) must have one of allowed_dirs as a prefix - → if not, return Err(...) to the plugin -``` - -### `list-dir` - -``` -Captured: allowed_dirs: Vec (absolute, canonicalized) -Enforcement: same prefix check as read-file -Result: entries one level deep (name + is-dir flag); caller descends by - calling list-dir again with an appended entry name -``` - -Canonicalization prevents `../` traversal attacks for both `read-file` and -`list-dir`. - -### `log` - -No restrictions — prints to the CLI progress stream (or stdout during testing). - -### Network / other I/O - -The wasmtime Component Model sandbox does not expose WASI socket or filesystem -interfaces to the component unless explicitly linked. Since the host only links -the four declared import functions, the plugin cannot open sockets, write files, -or spawn processes. - ---- - -## Crate Structure - -### `crates/icp-sync-plugin` - -Runtime crate — host-side Component Model integration for sync plugins. - -``` -crates/icp-sync-plugin/ - src/ - lib.rs — public API: run_plugin(...), RunPluginError - runtime.rs — wasmtime component setup, host state, bindgen!, exec() call - sandbox.rs — path canonicalization + allowlist enforcement - Cargo.toml — depends on: wasmtime (component-model feature), candid, - candid-parser, ic-agent, camino, snafu, tokio -``` - -Public function signature: - -```rust -pub fn run_plugin( - wasm_path: Utf8PathBuf, - base_dir: Utf8PathBuf, - allowed_dirs: Vec, - target_canister_id: Principal, - agent: Agent, - environment: String, - stdio: Option>, -) -> Result<(), RunPluginError> -``` - -### Host-Side Pattern (`runtime.rs`) - -```rust -wasmtime::component::bindgen!({ - world: "sync-plugin", - path: "../../sync-plugin/sync-plugin.wit", -}); - -struct HostState { /* target_canister_id, agent, allowed_dirs, base_dir, stdio */ } - -impl SyncPluginImports for HostState { - fn canister_call(&mut self, req: CanisterCallRequest) -> Result { ... } - fn read_file(&mut self, path: String) -> Result { ... } - fn list_dir(&mut self, path: String) -> Result, String> { ... } - fn log(&mut self, message: String) { ... } -} - -// In run_plugin: -let engine = Engine::new(Config::new().wasm_component_model(true))?; -let component = Component::from_file(&engine, &wasm_path)?; -let mut store = Store::new(&engine, host_state); -let (plugin, _) = SyncPlugin::instantiate(&mut store, &component, &linker)?; -let result = plugin.call_exec(&mut store, &input)?; -``` - -The `bindgen!` macro generates `SyncPlugin`, `SyncPluginImports`, and all WIT -types as plain Rust structs/enums — no JSON, no manual serialization. - -### `crates/icp/src/manifest/adapter/plugin.rs` - -Describes the `canister.yaml` fields: - -```rust -pub struct Adapter { - #[serde(flatten)] - pub source: super::prebuilt::SourceField, - pub sha256: Option, - pub dirs: Option>, -} -``` - -### `crates/icp/src/canister/sync/plugin.rs` - -Resolves the wasm, verifies sha256, canonicalizes dirs, then calls -`icp_sync_plugin::run_plugin(...)`. - ---- - -## Writing a Sync Plugin (Rust) - -Plugins are built as WebAssembly components targeting `wasm32-wasip2` using -[`cargo component`](https://github.com/bytecodealliance/cargo-component): - -```bash -cargo install cargo-component -cargo component build --release -``` - -The WIT file (`sync-plugin/sync-plugin.wit`) is distributed with the tool and -referenced in the plugin's `Cargo.toml`: - -```toml -[package.metadata.component] -package = "icp:sync-plugin" -``` - -**`src/lib.rs`** — implement the generated `Guest` trait: - -```rust -cargo_component_bindings::generate!(); - -use bindings::Guest; -use bindings::icp::sync_plugin::types::{CanisterCallRequest, CallType, SyncExecInput}; - -struct MyPlugin; - -impl Guest for MyPlugin { - fn exec(input: SyncExecInput) -> Result, String> { - bindings::log(&format!("syncing canister {}", input.canister_id)); - - let entries = bindings::list_dir("seed-data/")?; - - for entry in entries { - if entry.is_dir { continue; } - - let path = format!("seed-data/{}", entry.name); - let data = bindings::read_file(&path)?; - - bindings::canister_call(CanisterCallRequest { - method: "seed".to_string(), - arg: format!("(\"{}\")", data.trim()), - call_type: Some(CallType::Update), - })?; - - bindings::log(&format!("{path}: ok")); - } - - Ok(Some(format!( - "seeded canister {} in environment {}", - input.canister_id, input.environment - ))) - } -} -``` - -`cargo_component_bindings::generate!()` runs at build time — nothing generated -is committed to the repo. The WIT file is the sole source of truth. - ---- - -## Sandbox Summary - -| Capability | Allowed | Enforcement | -|------------|---------|-------------| -| `canister-call` to target canister | Yes | Host always uses captured `target_canister_id` | -| `canister-call` to any other canister | No | Not a parameter; host ignores any such intent | -| `read-file` within declared `dirs` | Yes | Path allowlist checked after canonicalization | -| `read-file` outside declared `dirs` | No | Returns `Err(...)` to plugin | -| `list-dir` within declared `dirs` | Yes | Path allowlist checked after canonicalization | -| `list-dir` outside declared `dirs` | No | Returns `Err(...)` to plugin | -| `log` (print to CLI output) | Yes | Unrestricted | -| Arbitrary filesystem write | No | No WASI filesystem write interface linked | -| Network access (TCP/UDP/etc.) | No | No WASI socket interface linked | -| Spawning processes | No | No WASI process interface linked | -| Calls to other environments | No | Agent scoped to environment at plugin load time | - ---- - -## Decisions - -**1. No generated file checked in** - -`wasmtime::component::bindgen!` (host side) and `cargo_component_bindings::generate!()` -(guest side) both run at build time — nothing generated is committed to the repo. -The WIT file is the sole source of truth. - -**2. `result` for all fallible functions** - -`exec` returns `result, string>` — the ok arm carries optional -output text, the err arm carries the error message. All host functions follow the -same pattern, so the guest can use `?` uniformly. - -**3. `dirs` resolution** - -Relative to the canister directory. Consistent with other adapters. - -**4. Caching downloaded wasm** - -Not implemented in the POC — deferred. - -**5. Plugin timeout** - -Not implemented in the POC. wasmtime supports epoch-based interruption and -fuel-based metering; adding a configurable `timeout_seconds` field to the -adapter is a follow-up. - ---- - -## Follow-up Items - -- **Wasm caching**: cache remote plugin wasm files in `.icp/cache/`. -- **Plugin timeout**: add `timeout_seconds: Option` to - `adapter::plugin::Adapter`; wire through to wasmtime epoch interruption. diff --git a/sync-plugin/plan.md b/sync-plugin/plan.md deleted file mode 100644 index 5e81cb3ae..000000000 --- a/sync-plugin/plan.md +++ /dev/null @@ -1,359 +0,0 @@ -# Sync Plugin Implementation Plan - -Reference: [sync-plugin/design.md](sync-plugin/design.md) - ---- - -## Step 1 — Create the sync plugin manifest adapter - -**New file**: `crates/icp/src/manifest/adapter/plugin.rs` - -```rust -use super::prebuilt::SourceField; -use crate::prelude::*; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -/// Configuration for a sync plugin step. -#[derive(Clone, Debug, Deserialize, PartialEq, JsonSchema, Serialize)] -pub struct Adapter { - #[serde(flatten)] - pub source: SourceField, // path: or url: - pub sha256: Option, - pub dirs: Option>, // read-access directory allowlist -} -``` - -Add `pub mod plugin;` to `crates/icp/src/manifest/adapter/mod.rs`. - ---- - -## Step 2 — Add `SyncStep::Plugin` to the canister manifest - -**File**: `crates/icp/src/manifest/canister.rs` - -```rust -#[derive(Clone, Debug, Deserialize, PartialEq, JsonSchema, Serialize)] -#[serde(tag = "type", rename_all = "lowercase")] -pub enum SyncStep { - Script(adapter::script::Adapter), - Assets(adapter::assets::Adapter), - Plugin(adapter::plugin::Adapter), // NEW -} -``` - -Update `SyncStep::fmt` to cover the new variant. - -Add a test case for the new YAML syntax in `canister.rs` tests: - -```yaml -sync: - steps: - - type: plugin - path: ./plugins/my-sync.wasm - dirs: - - assets/seed-data/ -``` - ---- - -## Step 3 — Write the WIT interface file - -**File**: `sync-plugin/sync-plugin.wit` (already created) - -The WIT world defines the complete contract between icp-cli and any sync plugin. -It uses: -- `result` for all fallible operations — no `nullable` field workarounds -- `option` for optional values -- Plain `record` and `enum` types that map directly to Rust structs/enums - -The WIT file is the single source of truth for both the host runtime and guest -plugin code — no separate schema file, no generated file checked in. - -Add the WIT file path to the workspace `Cargo.toml` as a note for reviewers, or -document it in the crate README. The `bindgen!` macro on the host side and the -`cargo component` tool on the guest side both resolve the path at build time. - ---- - -## Step 4 — Implement `crates/icp-sync-plugin` with wasmtime - -**Crate**: `crates/icp-sync-plugin/` - -Add to `Cargo.toml`: - -```toml -[dependencies] -camino.workspace = true -candid.workspace = true -candid_parser.workspace = true -hex.workspace = true -ic-agent.workspace = true -snafu.workspace = true -tokio.workspace = true -wasmtime = { workspace = true } -``` - -Add `wasmtime` to the root `Cargo.toml` `[workspace.dependencies]` table with -its version and required features: - -```toml -# root Cargo.toml -[workspace.dependencies] -wasmtime = { version = "X", features = ["component-model"] } -``` - -In `crates/icp-sync-plugin/Cargo.toml` declare it without a version -(`workspace = true` inherits everything from the root). - -### `src/sandbox.rs` - -Already implemented and tested — no changes needed. - -```rust -/// Returns true iff `path` (canonicalized) starts with one of `allowed_dirs`. -pub fn is_path_allowed(path: &Utf8Path, allowed_dirs: &[Utf8PathBuf]) -> bool -``` - -### `src/runtime.rs` - -Replace the stub with the wasmtime Component Model implementation. - -```rust -wasmtime::component::bindgen!({ - world: "sync-plugin", - path: "../../sync-plugin/sync-plugin.wit", -}); - -struct HostState { - target_canister_id: Principal, - agent: Arc, - allowed_dirs: Arc>, - base_dir: Arc, - stdio: Option>, -} - -impl SyncPluginImports for HostState { - fn canister_call(&mut self, req: CanisterCallRequest) -> Result { ... } - fn read_file(&mut self, path: String) -> Result { ... } - fn list_dir(&mut self, path: String) -> Result, String> { ... } - fn log(&mut self, message: String) { ... } -} -``` - -Error variants (one per primary action): - -- `LoadComponent { path }` — wasmtime fails to load or parse the component -- `Instantiate { path }` — linker or store setup failure -- `CallExec { path }` — wasmtime trap or ABI error during the exec() call -- `PluginFailed { message }` — exec() returned `Err(message)` - -`canister_call` in `HostState` blocks the current thread on the async agent call -using `tokio::runtime::Handle::current().block_on(...)` — the host is already -inside a `tokio::task::block_in_place` call in `sync/plugin.rs`. - -### `src/lib.rs` - -Re-exports `run_plugin` and `RunPluginError` — no change to the public API. - ---- - -## Step 5 — Implement `sync/plugin.rs` in the `icp` crate - -**File**: `crates/icp/src/canister/sync/plugin.rs` (already exists as a stub) - -```rust -pub async fn sync( - adapter: &adapter::plugin::Adapter, - params: &Params, - agent: &Agent, - environment: &str, - stdio: Option>, -) -> Result<(), PluginError> -``` - -Responsibilities: -1. Resolve the wasm path: - - `Local`: join with `params.path` (canister directory) - - `Remote`: download to temp file (reuse the download + sha256 utility used - by the prebuilt build adapter) -2. Verify sha256 if present -3. Canonicalize declared `dirs` relative to `params.path` -4. Call `icp_sync_plugin::run_plugin(...)` - -Add `PluginError` variants for each failing action (wasm resolution, download, -sha256 mismatch, run). - ---- - -## Step 6 — Wire `SyncStep::Plugin` into the dispatcher - -**File**: `crates/icp/src/canister/sync/mod.rs` - -```rust -mod plugin; - -// In Syncer::sync(): -SyncStep::Plugin(adapter) => { - Ok(plugin::sync(adapter, params, agent, environment, stdio).await?) -} -``` - -Add `Plugin` variant to `SynchronizeError`. - -The `environment` string must be threaded through from `Params` (add a field) -or passed as a separate parameter — check how `assets::sync` currently receives -it and be consistent. - ---- - -## Step 7 — Build the proof-of-concept plugin - -**Directory**: `sync-plugin/poc/` - -A Rust wasm plugin that: -1. Lists a declared directory and reads each text file found -2. Calls an update method on the canister, passing the file content as a string argument -3. Logs the result of each call - -### Toolchain - -Plugins use plain `cargo build` — no `cargo-component` tool required: - -```bash -rustup target add wasm32-wasip2 -cargo build --target wasm32-wasip2 --release -``` - -The output is a WebAssembly component binary (`.wasm`) that the host loads -directly with `wasmtime::component::Component::from_file`. - -### `Cargo.toml` - -```toml -[package] -name = "icp-sync-plugin-poc" -version = "0.1.0" -edition = "2024" -publish = false - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wit-bindgen = { version = "X", features = ["realloc"] } - -[build-dependencies] -# none — build.rs only emits rerun-if-changed directives -``` - -### `build.rs` - -A minimal build script that tells Cargo to re-run bindings generation whenever -the WIT file changes: - -```rust -fn main() { - println!("cargo:rerun-if-changed=../../sync-plugin/sync-plugin.wit"); -} -``` - -### `src/lib.rs` - -Use the `wit_bindgen::generate!` proc macro (no separate `build.rs` code -generation step — the macro expands at compile time from the WIT path): - -```rust -wit_bindgen::generate!({ - world: "sync-plugin", - path: "../../sync-plugin/sync-plugin.wit", -}); - -use exports::icp::sync_plugin::types::{CanisterCallRequest, CallType, GuestExec, SyncExecInput}; - -struct Plugin; - -impl GuestExec for Plugin { - fn exec(input: SyncExecInput) -> Result, String> { - log(&format!("sync plugin: starting for canister {}", input.canister_id)); - - let entries = list_dir("seed-data/")?; - - for entry in entries { - if entry.is_dir { continue; } - let path = format!("seed-data/{}", entry.name); - let data = read_file(&path)?; - canister_call(CanisterCallRequest { - method: "seed".to_string(), - arg: format!("(\"{}\")", data.trim().replace('"', "\\\"")), - call_type: Some(CallType::Update), - })?; - log(&format!("{path}: ok")); - } - - Ok(Some(format!( - "seeded canister {} in environment {}", - input.canister_id, input.environment - ))) - } -} - -export!(Plugin); -``` - ---- - -## Step 8 — Update JSON schema and CLI docs - -```bash -./scripts/generate-config-schemas.sh # regenerate canister-yaml-schema.json -./scripts/generate-cli-docs.sh # regenerate CLI reference docs -``` - -The new `SyncStep::Plugin` variant and `adapter::plugin::Adapter` implement -`JsonSchema` (via `schemars`), so the schema generator picks them up -automatically once wired in. - ---- - -## Step 9 — Add integration tests - -- A `canister.yaml` fixture with `type: plugin` in `crates/icp-cli/tests/` or - `examples/` -- Unit tests in `adapter/plugin.rs` (YAML round-trip, same style as - `adapter/prebuilt.rs`) -- Unit tests in `sync/plugin.rs` for sha256 verification and path allowlist - enforcement (no network needed — use a minimal hand-crafted wasm component or - build the poc plugin in the test) -- Unit tests in `sandbox.rs` for `list_dir` allowlist enforcement: path outside - allowed dirs, `../` traversal attempts, and a valid listing (these already - exist and pass) - ---- - -## Order of Dependencies - -``` -Step 1 (plugin adapter) ──► Step 2 (SyncStep::Plugin) - └─ Step 6 (dispatcher) -Step 3 (WIT file — already done) -Step 4 (icp-sync-plugin runtime) - └─ Step 5 (sync/plugin.rs) ──► Step 6 (dispatcher) -Step 8 (schema + docs) — after Steps 1–2 -Step 9 (tests) — after Steps 1–6 -``` - -Steps 1–2 (manifest layer) and Step 4 (runtime layer) can be developed -independently and in parallel. Step 5 joins both. Step 6 is the final wire-up. - ---- - -## Follow-up Items (post-POC) - -These are out of scope for the current implementation; tracked here for later: - -- **Wasm caching**: cache remote plugin wasm in `.icp/cache/` to avoid - re-downloading on every sync. -- **Plugin timeout**: add `timeout_seconds: Option` to - `adapter::plugin::Adapter` and wire through to wasmtime's epoch interruption - mechanism. From 7194a6c3b08091d73c1f735d1f085c640d831c3c Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 20 Apr 2026 12:43:28 -0400 Subject: [PATCH 13/23] feat(sync-plugin): forward proxy to sync_many and add direct flag to canister-call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Threads args.proxy from icp deploy through sync_many → Params so plugin syncers can route update calls via the proxy canister. Extends the WIT interface with a direct: bool field on canister-call-request; when true the host bypasses the proxy and calls the target canister directly even if --proxy was set. The assets and script syncers are unaffected. Co-Authored-By: Claude Sonnet 4.6 --- Cargo.lock | 1 + crates/icp-cli/src/commands/deploy.rs | 1 + crates/icp-cli/src/commands/sync.rs | 1 + crates/icp-cli/src/operations/sync.rs | 7 ++- crates/icp-sync-plugin/Cargo.toml | 1 + crates/icp-sync-plugin/src/runtime.rs | 55 ++++++++++++++++++---- crates/icp-sync-plugin/sync-plugin.wit | 5 ++ crates/icp/src/canister/sync/mod.rs | 17 ++++--- crates/icp/src/canister/sync/plugin.rs | 3 ++ examples/icp-sync-plugin/plugin/src/lib.rs | 2 + 10 files changed, 77 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aba43374a..c1a48bd99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3832,6 +3832,7 @@ dependencies = [ "candid", "hex", "ic-agent", + "icp-canister-interfaces", "snafu", "tokio", "wasmtime", diff --git a/crates/icp-cli/src/commands/deploy.rs b/crates/icp-cli/src/commands/deploy.rs index bb799fc2e..8c3fc56d8 100644 --- a/crates/icp-cli/src/commands/deploy.rs +++ b/crates/icp-cli/src/commands/deploy.rs @@ -367,6 +367,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow: agent.clone(), sync_canisters, environment_selection.name().to_owned(), + args.proxy, ctx.debug, ) .await?; diff --git a/crates/icp-cli/src/commands/sync.rs b/crates/icp-cli/src/commands/sync.rs index e662ac2cd..dde0f2989 100644 --- a/crates/icp-cli/src/commands/sync.rs +++ b/crates/icp-cli/src/commands/sync.rs @@ -82,6 +82,7 @@ pub(crate) async fn exec(ctx: &Context, args: &SyncArgs) -> Result<(), anyhow::E agent, sync_canisters, environment_selection.name().to_owned(), + None, ctx.debug, ) .await?; diff --git a/crates/icp-cli/src/operations/sync.rs b/crates/icp-cli/src/operations/sync.rs index 5833ecf7d..b5b36e552 100644 --- a/crates/icp-cli/src/operations/sync.rs +++ b/crates/icp-cli/src/operations/sync.rs @@ -1,5 +1,6 @@ +use candid::Principal; use futures::{StreamExt, stream::FuturesOrdered}; -use ic_agent::{Agent, export::Principal}; +use ic_agent::Agent; use icp::{ Canister, canister::sync::{Params, Synchronize, SynchronizeError}, @@ -33,6 +34,7 @@ async fn sync_canister( canister_id: Principal, canister_info: &Canister, environment: &str, + proxy: Option, pb: &mut MultiStepProgressBar, ) -> Result<(), SynchronizeError> { let step_count = canister_info.sync.steps.len(); @@ -52,6 +54,7 @@ async fn sync_canister( path: canister_path.clone(), cid: canister_id, environment: environment.to_owned(), + proxy, }, agent, Some(tx), @@ -73,6 +76,7 @@ pub(crate) async fn sync_many( agent: Agent, canisters: Vec<(Principal, PathBuf, Canister)>, environment: String, + proxy: Option, debug: bool, ) -> Result<(), SyncOperationError> { let mut futs = FuturesOrdered::new(); @@ -95,6 +99,7 @@ pub(crate) async fn sync_many( cid, &canister_info, &environment, + proxy, &mut pb, ) .await; diff --git a/crates/icp-sync-plugin/Cargo.toml b/crates/icp-sync-plugin/Cargo.toml index f5435cebc..fa2099ae6 100644 --- a/crates/icp-sync-plugin/Cargo.toml +++ b/crates/icp-sync-plugin/Cargo.toml @@ -12,6 +12,7 @@ camino.workspace = true candid.workspace = true hex.workspace = true ic-agent.workspace = true +icp-canister-interfaces.workspace = true snafu.workspace = true tokio.workspace = true wasmtime.workspace = true diff --git a/crates/icp-sync-plugin/src/runtime.rs b/crates/icp-sync-plugin/src/runtime.rs index 87976677b..b6d8bfe33 100644 --- a/crates/icp-sync-plugin/src/runtime.rs +++ b/crates/icp-sync-plugin/src/runtime.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use camino::Utf8PathBuf; -use candid::Principal; +use candid::{Encode, Principal}; use ic_agent::Agent; use snafu::prelude::*; use tokio::sync::mpsc::Sender; @@ -20,6 +20,8 @@ use icp::sync_plugin::types::CallType; struct HostState { target_canister_id: Principal, agent: Arc, + /// Proxy canister to route update calls through, if configured. + proxy: Option, // WASI context. Preopened directories in this context are the only // filesystem locations the plugin can access. wasi_ctx: wasmtime_wasi::WasiCtx, @@ -40,23 +42,56 @@ impl icp::sync_plugin::types::Host for HostState {} impl SyncPluginImports for HostState { fn canister_call(&mut self, req: CanisterCallRequest) -> Result, String> { - let arg_bytes = req.arg; + use icp_canister_interfaces::proxy::{ProxyArgs, ProxyResult}; + let arg_bytes = req.arg; let cid = self.target_canister_id; let method = req.method.clone(); let agent = Arc::clone(&self.agent); let call_type = req.call_type.unwrap_or(CallType::Update); + let proxy = if req.direct { None } else { self.proxy }; // We are already inside tokio::task::block_in_place (see sync/plugin.rs), // so blocking the thread here is safe. - tokio::runtime::Handle::current() - .block_on(async move { - match call_type { - CallType::Update => agent.update(&cid, &method).with_arg(arg_bytes).await, - CallType::Query => agent.query(&cid, &method).with_arg(arg_bytes).call().await, + tokio::runtime::Handle::current().block_on(async move { + match call_type { + CallType::Update => { + if let Some(proxy_cid) = proxy { + let proxy_args = ProxyArgs { + canister_id: cid, + method: method.clone(), + args: arg_bytes, + cycles: candid::Nat::from(0u64), + }; + let encoded = Encode!(&proxy_args) + .map_err(|e| format!("proxy encode failed: {e}"))?; + let raw = agent + .update(&proxy_cid, "proxy") + .with_arg(encoded) + .await + .map_err(|e| format!("proxy call failed: {e}"))?; + let (result,): (ProxyResult,) = candid::decode_args(&raw) + .map_err(|e| format!("proxy decode failed: {e}"))?; + match result { + ProxyResult::Ok(ok) => Ok(ok.result), + ProxyResult::Err(err) => Err(err.format_error()), + } + } else { + agent + .update(&cid, &method) + .with_arg(arg_bytes) + .await + .map_err(|e| format!("canister call failed: {e}")) + } } - }) - .map_err(|e| format!("canister call failed: {e}")) + CallType::Query => agent + .query(&cid, &method) + .with_arg(arg_bytes) + .call() + .await + .map_err(|e| format!("canister call failed: {e}")), + } + }) } } @@ -103,6 +138,7 @@ pub fn run_plugin( files: Vec<(String, String)>, target_canister_id: Principal, agent: Agent, + proxy: Option, environment: String, stdio: Option>, ) -> Result<(), RunPluginError> { @@ -146,6 +182,7 @@ pub fn run_plugin( let host_state = HostState { target_canister_id, agent: Arc::new(agent), + proxy, wasi_ctx: wasi_builder.build(), wasi_table: wasmtime_wasi::ResourceTable::new(), }; diff --git a/crates/icp-sync-plugin/sync-plugin.wit b/crates/icp-sync-plugin/sync-plugin.wit index 5f5bb3b9a..7bdfc0035 100644 --- a/crates/icp-sync-plugin/sync-plugin.wit +++ b/crates/icp-sync-plugin/sync-plugin.wit @@ -38,6 +38,11 @@ interface types { arg: list, /// Defaults to update if omitted. call-type: option, + /// When true, the call bypasses any proxy canister configured via + /// `--proxy`, going directly to the target canister. When false + /// (the default), the host routes the call through the proxy if one + /// is configured. + direct: bool, } } diff --git a/crates/icp/src/canister/sync/mod.rs b/crates/icp/src/canister/sync/mod.rs index d2b8ccb5c..59171c5aa 100644 --- a/crates/icp/src/canister/sync/mod.rs +++ b/crates/icp/src/canister/sync/mod.rs @@ -17,6 +17,8 @@ pub struct Params { /// Name of the environment being synced (e.g. "local", "production"). /// Passed to sync plugin steps via `SyncExecInput`. pub environment: String, + /// Proxy canister to route calls through, if `--proxy` was passed. + pub proxy: Option, } #[derive(Debug, Snafu)] @@ -56,12 +58,15 @@ impl Synchronize for Syncer { match step { SyncStep::Assets(adapter) => Ok(assets::sync(adapter, params, agent).await?), SyncStep::Script(adapter) => Ok(script::sync(adapter, params, stdio).await?), - SyncStep::Plugin(adapter) => { - Ok( - plugin::sync(adapter, params, agent, ¶ms.environment.clone(), stdio) - .await?, - ) - } + SyncStep::Plugin(adapter) => Ok(plugin::sync( + adapter, + params, + agent, + ¶ms.environment.clone(), + params.proxy, + stdio, + ) + .await?), } } } diff --git a/crates/icp/src/canister/sync/plugin.rs b/crates/icp/src/canister/sync/plugin.rs index 368606db7..2201eced1 100644 --- a/crates/icp/src/canister/sync/plugin.rs +++ b/crates/icp/src/canister/sync/plugin.rs @@ -1,4 +1,5 @@ use camino::Utf8PathBuf; +use candid::Principal; use ic_agent::Agent; use icp_sync_plugin::{RunPluginError, run_plugin}; use reqwest::{Client, Method, Request}; @@ -60,6 +61,7 @@ pub(super) async fn sync( params: &Params, agent: &Agent, environment: &str, + proxy: Option, stdio: Option>, ) -> Result<(), PluginError> { // 1. Acquire the wasm bytes — either from a local path or a remote URL. @@ -151,6 +153,7 @@ pub(super) async fn sync( files, params.cid, agent_clone, + proxy, environment_owned, stdio_clone, ) diff --git a/examples/icp-sync-plugin/plugin/src/lib.rs b/examples/icp-sync-plugin/plugin/src/lib.rs index e675d7118..f4e0bbaaa 100644 --- a/examples/icp-sync-plugin/plugin/src/lib.rs +++ b/examples/icp-sync-plugin/plugin/src/lib.rs @@ -25,6 +25,7 @@ impl Guest for Plugin { method: "set_config".to_string(), arg, call_type: Some(icp::sync_plugin::types::CallType::Update), + direct: false, })?; println!("set_config from {}: ok", config.name); } @@ -64,6 +65,7 @@ fn register_dir(dir: &Path) -> Result { method: "register".to_string(), arg, call_type: Some(icp::sync_plugin::types::CallType::Update), + direct: false, })?; println!("{path_str}: ok"); count += 1; From f15b540de458ce0f01748abfbedb2cbbc50bdb5c Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 20 Apr 2026 13:54:29 -0400 Subject: [PATCH 14/23] feat(sync-plugin): exercise direct flag with set_uploader and identity principal - Add identity-principal and proxy-canister-id to sync-exec-input in the WIT interface so plugins can act on the caller's identity and proxy configuration. - Replace set_config with set_uploader(Principal): controller-gated update that stores the uploader; register is now restricted to that principal. - Plugin calls set_uploader via proxy (direct: false) and register directly (direct: true), demonstrating both routing modes in a single sync run. Co-Authored-By: Claude Sonnet 4.6 --- crates/icp-sync-plugin/src/runtime.rs | 3 ++ crates/icp-sync-plugin/sync-plugin.wit | 5 ++++ crates/icp/src/canister/sync/plugin.rs | 8 +++++ examples/icp-sync-plugin/Cargo.lock | 16 +++++----- examples/icp-sync-plugin/canister/src/lib.rs | 30 ++++++++++++++----- examples/icp-sync-plugin/demo.did | 4 +-- examples/icp-sync-plugin/icp.yaml | 6 ++-- examples/icp-sync-plugin/plugin/Cargo.toml | 2 +- examples/icp-sync-plugin/plugin/src/lib.rs | 31 +++++++++++--------- 9 files changed, 68 insertions(+), 37 deletions(-) diff --git a/crates/icp-sync-plugin/src/runtime.rs b/crates/icp-sync-plugin/src/runtime.rs index b6d8bfe33..5e0e34c9d 100644 --- a/crates/icp-sync-plugin/src/runtime.rs +++ b/crates/icp-sync-plugin/src/runtime.rs @@ -139,6 +139,7 @@ pub fn run_plugin( target_canister_id: Principal, agent: Agent, proxy: Option, + identity_principal: Principal, environment: String, stdio: Option>, ) -> Result<(), RunPluginError> { @@ -212,6 +213,8 @@ pub fn run_plugin( .into_iter() .map(|(name, content)| FileInput { name, content }) .collect(), + identity_principal: identity_principal.to_text(), + proxy_canister_id: proxy.map(|p| p.to_text()), }; let result = plugin diff --git a/crates/icp-sync-plugin/sync-plugin.wit b/crates/icp-sync-plugin/sync-plugin.wit index 7bdfc0035..1491b7f8c 100644 --- a/crates/icp-sync-plugin/sync-plugin.wit +++ b/crates/icp-sync-plugin/sync-plugin.wit @@ -27,6 +27,11 @@ interface types { /// Files declared in the manifest step's `files` setting, read by /// the host and passed inline. The plugin decides how to use them. files: list, + /// Textual principal of the signing identity used for canister calls. + identity-principal: string, + /// Textual principal of the proxy canister, if one was configured via + /// `--proxy`. None when no proxy is in use. + proxy-canister-id: option, } /// A request to call a method on the target canister. diff --git a/crates/icp/src/canister/sync/plugin.rs b/crates/icp/src/canister/sync/plugin.rs index 2201eced1..9912192e3 100644 --- a/crates/icp/src/canister/sync/plugin.rs +++ b/crates/icp/src/canister/sync/plugin.rs @@ -47,6 +47,9 @@ pub enum PluginError { #[snafu(display("plugin wasm checksum mismatch, expected: {expected}, actual: {actual}"))] ChecksumMismatch { expected: String, actual: String }, + #[snafu(display("failed to get identity principal: {err}"))] + GetIdentityPrincipal { err: String }, + #[snafu(display("failed to run plugin"))] Run { source: RunPluginError }, @@ -140,6 +143,10 @@ pub(super) async fn sync( } // 4. Run the plugin (blocking call — signal Tokio that this thread will block). + let identity_principal = agent + .get_principal() + .map_err(|err| PluginError::GetIdentityPrincipal { err })?; + let wasm_path_buf = Utf8PathBuf::from(wasm_path.as_str()); let agent_clone = agent.clone(); let environment_owned = environment.to_owned(); @@ -154,6 +161,7 @@ pub(super) async fn sync( params.cid, agent_clone, proxy, + identity_principal, environment_owned, stdio_clone, ) diff --git a/examples/icp-sync-plugin/Cargo.lock b/examples/icp-sync-plugin/Cargo.lock index db4c92bb1..5de2ce58d 100644 --- a/examples/icp-sync-plugin/Cargo.lock +++ b/examples/icp-sync-plugin/Cargo.lock @@ -341,14 +341,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "icp-sync-plugin-poc" -version = "0.1.0" -dependencies = [ - "candid", - "wit-bindgen", -] - [[package]] name = "id-arena" version = "2.3.0" @@ -476,6 +468,14 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "plugin" +version = "0.1.0" +dependencies = [ + "candid", + "wit-bindgen", +] + [[package]] name = "pretty" version = "0.12.5" diff --git a/examples/icp-sync-plugin/canister/src/lib.rs b/examples/icp-sync-plugin/canister/src/lib.rs index f7145ceb0..6fd8fbd8e 100644 --- a/examples/icp-sync-plugin/canister/src/lib.rs +++ b/examples/icp-sync-plugin/canister/src/lib.rs @@ -1,27 +1,41 @@ use std::cell::RefCell; +use candid::Principal; + thread_local! { - static CONFIG: RefCell = RefCell::default(); + static UPLOADER: RefCell> = RefCell::default(); static FRUITS: RefCell> = RefCell::default(); } -// Upload the config value (called once by the sync plugin). +// Set the uploader principal (controller-only). Called once via the proxy canister. #[ic_cdk::update] -fn set_config(value: String) { - CONFIG.with_borrow_mut(|c| *c = value); +fn set_uploader(uploader: Principal) { + let caller = ic_cdk::api::msg_caller(); + assert!( + ic_cdk::api::is_controller(&caller), + "only a controller can call set_uploader" + ); + UPLOADER.with_borrow_mut(|u| *u = Some(uploader)); } -// Register a (name, content) fruit pair (called by the sync plugin for each file). +// Register a (name, content) fruit pair. Restricted to the stored uploader. #[ic_cdk::update] fn register(name: String, content: String) { + let caller = ic_cdk::api::msg_caller(); + let uploader = UPLOADER.with_borrow(|u| *u); + assert_eq!( + Some(caller), + uploader, + "only the uploader can call register" + ); FRUITS.with_borrow_mut(|f| f.push((name, content))); } -// Return the stored config and every registered fruit. +// Return the stored uploader principal and every registered fruit. #[ic_cdk::query] -fn show() -> (String, Vec<(String, String)>) { +fn show() -> (Option, Vec<(String, String)>) { ( - CONFIG.with_borrow(|c| c.clone()), + UPLOADER.with_borrow(|u| *u), FRUITS.with_borrow(|f| f.clone()), ) } diff --git a/examples/icp-sync-plugin/demo.did b/examples/icp-sync-plugin/demo.did index 1f1a239ca..95328f1b3 100644 --- a/examples/icp-sync-plugin/demo.did +++ b/examples/icp-sync-plugin/demo.did @@ -1,5 +1,5 @@ service : { - set_config : (text) -> (); + set_uploader : (principal) -> (); register : (text, text) -> (); - show : () -> (text, vec record { text; text }) query; + show : () -> (opt principal, vec record { text; text }) query; } diff --git a/examples/icp-sync-plugin/icp.yaml b/examples/icp-sync-plugin/icp.yaml index eacd3ee5e..134cfd827 100644 --- a/examples/icp-sync-plugin/icp.yaml +++ b/examples/icp-sync-plugin/icp.yaml @@ -16,9 +16,7 @@ canisters: steps: - type: plugin # Path to the compiled PoC plugin wasm, relative to this directory. - # Build it first: cd plugin && cargo build --target wasm32-wasip2 --release - path: plugin/target/wasm32-wasip2/release/icp_sync_plugin_poc.wasm + # Build it first: cargo build -p plugin --target wasm32-wasip2 --release + path: target/wasm32-wasip2/release/plugin.wasm dirs: - seed-data - files: - - config.txt diff --git a/examples/icp-sync-plugin/plugin/Cargo.toml b/examples/icp-sync-plugin/plugin/Cargo.toml index 236f0fc35..cbecad57f 100644 --- a/examples/icp-sync-plugin/plugin/Cargo.toml +++ b/examples/icp-sync-plugin/plugin/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "icp-sync-plugin-poc" +name = "plugin" version = "0.1.0" edition = "2024" publish = false diff --git a/examples/icp-sync-plugin/plugin/src/lib.rs b/examples/icp-sync-plugin/plugin/src/lib.rs index f4e0bbaaa..e4a3fdc44 100644 --- a/examples/icp-sync-plugin/plugin/src/lib.rs +++ b/examples/icp-sync-plugin/plugin/src/lib.rs @@ -6,7 +6,7 @@ wit_bindgen::generate!({ use std::fs; use std::path::Path; -use candid::Encode; +use candid::{Encode, Principal}; struct Plugin; @@ -17,20 +17,23 @@ impl Guest for Plugin { input.canister_id, input.environment ); - // 1. Upload the config value — the first file the manifest declared. - if let Some(config) = input.files.first() { - let arg = Encode!(&config.content.trim()) - .map_err(|e| format!("encode set_config arg: {e}"))?; - canister_call(&CanisterCallRequest { - method: "set_config".to_string(), - arg, - call_type: Some(icp::sync_plugin::types::CallType::Update), - direct: false, - })?; - println!("set_config from {}: ok", config.name); - } + // 1. Set the uploader to the current identity principal. + // Routed through the proxy (direct: false) so the controller-gated + // call is signed by the proxy canister, which is a controller. + let uploader = Principal::from_text(&input.identity_principal) + .map_err(|e| format!("invalid identity principal: {e}"))?; + let arg = Encode!(&uploader).map_err(|e| format!("encode set_uploader arg: {e}"))?; + canister_call(&CanisterCallRequest { + method: "set_uploader".to_string(), + arg, + call_type: Some(icp::sync_plugin::types::CallType::Update), + direct: false, + })?; + println!("set_uploader ({}): ok", input.identity_principal); // 2. Register every file found by traversing the preopened dirs. + // Direct calls (direct: true) because register is gated on the + // uploader principal, which is the current identity — not the proxy. let mut registered = 0u32; for dir in &input.dirs { registered += register_dir(Path::new(dir))?; @@ -65,7 +68,7 @@ fn register_dir(dir: &Path) -> Result { method: "register".to_string(), arg, call_type: Some(icp::sync_plugin::types::CallType::Update), - direct: false, + direct: true, })?; println!("{path_str}: ok"); count += 1; From e4bfae83b9ea7be8e40d128870a7c102df6036a3 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 20 Apr 2026 13:57:04 -0400 Subject: [PATCH 15/23] docs(sync-plugin): add README for icp-sync-plugin example Covers project structure (canister, plugin, seed-data), the role of each component, and a walkthrough of how the direct flag is exercised across the two canister calls made during a sync run. Co-Authored-By: Claude Sonnet 4.6 --- examples/icp-sync-plugin/README.md | 83 ++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 examples/icp-sync-plugin/README.md diff --git a/examples/icp-sync-plugin/README.md b/examples/icp-sync-plugin/README.md new file mode 100644 index 000000000..57518b0ab --- /dev/null +++ b/examples/icp-sync-plugin/README.md @@ -0,0 +1,83 @@ +# icp-sync-plugin example + +This example demonstrates the sync plugin system: a Wasm component that runs +inside `icp sync` and drives canister update calls on behalf of the user. + +## Project structure + +``` +icp-sync-plugin/ +├── canister/ # The target canister (compiled to wasm32-unknown-unknown) +├── plugin/ # The sync plugin (compiled to wasm32-wasip2) +├── seed-data/ # Fruit files the plugin registers (preopened via WASI) +└── icp.yaml # Manifest wiring the build and sync steps together +``` + +### `canister/` + +A simple Rust canister with three methods: + +| Method | Type | Description | +|---|---|---| +| `set_uploader(principal)` | update | Stores a principal as the authorised uploader. Restricted to canister controllers. | +| `register(name, content)` | update | Appends a `(name, content)` fruit pair. Restricted to the stored uploader. | +| `show()` | query | Returns the current uploader principal and all registered fruits. | + +### `plugin/` + +A Rust Wasm component that implements the `sync-plugin` world defined in +`crates/icp-sync-plugin/sync-plugin.wit`. The host runtime calls its `exec` +export and provides a `canister-call` import the plugin uses to reach the +canister. + +## How the plugin system is exercised + +This example is designed to demonstrate both routing modes of the +`canister-call` import — the `direct` flag — in a single sync run. + +### Call 1 — `set_uploader` via proxy (`direct: false`) + +The plugin reads `identity-principal` from `sync-exec-input` (the signing +identity the CLI is using) and calls `set_uploader` with it. The call is +routed through the proxy canister (`direct: false`), so it arrives at the +target canister with the **proxy's principal as the caller**. Because the proxy +canister is listed as a controller of the target, the controller guard passes. + +This models a pattern where privileged, one-time setup calls must come from a +known controller — not directly from an end-user identity. + +### Call 2 — `register` directly (`direct: true`) + +For each file under `seed-data/`, the plugin calls `register` with +`direct: true`, bypassing the proxy entirely. The call arrives at the canister +with the **user's identity principal as the caller**, which is exactly the +uploader stored in step 1, so the uploader guard passes. + +This models a pattern where bulk data-upload calls must be attributable to the +actual user identity rather than a shared proxy. + +### Data flow summary + +``` +icp sync + └─ host runtime loads plugin.wasm + ├─ exec(sync-exec-input) called + │ identity-principal = + │ proxy-canister-id = + │ + ├─ canister-call set_uploader() direct=false → proxy → canister + │ canister stores uploader = + │ + └─ canister-call register(name, content) direct=true → canister (× N files) + canister checks caller == uploader ✓ +``` + +## Building + +```bash +# Build the canister +cargo build --target wasm32-unknown-unknown --release -p canister + +# Build the plugin +cargo build --target wasm32-wasip2 --release -p plugin +``` From 9a5c99eb64461ed885f6fc4af74a742ae7eaf1f9 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 20 Apr 2026 14:02:10 -0400 Subject: [PATCH 16/23] feat(sync): add --proxy flag to icp sync command Co-Authored-By: Claude Sonnet 4.6 --- crates/icp-cli/src/commands/sync.rs | 7 ++++++- docs/reference/cli.md | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/icp-cli/src/commands/sync.rs b/crates/icp-cli/src/commands/sync.rs index dde0f2989..81be12170 100644 --- a/crates/icp-cli/src/commands/sync.rs +++ b/crates/icp-cli/src/commands/sync.rs @@ -1,3 +1,4 @@ +use candid::Principal; use clap::Args; use futures::future::try_join_all; use icp::context::{CanisterSelection, Context, EnvironmentSelection}; @@ -15,6 +16,10 @@ pub(crate) struct SyncArgs { /// Canister names (if empty, sync all canisters in environment) pub(crate) canisters: Vec, + /// Principal of a proxy canister to route management canister calls through. + #[arg(long)] + pub(crate) proxy: Option, + #[command(flatten)] pub(crate) environment: EnvironmentOpt, @@ -82,7 +87,7 @@ pub(crate) async fn exec(ctx: &Context, args: &SyncArgs) -> Result<(), anyhow::E agent, sync_canisters, environment_selection.name().to_owned(), - None, + args.proxy, ctx.debug, ) .await?; diff --git a/docs/reference/cli.md b/docs/reference/cli.md index c9e41b64e..b47dc1433 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -1463,6 +1463,7 @@ Synchronize canisters ###### **Options:** +* `--proxy ` — Principal of a proxy canister to route management canister calls through * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as From ebeefd5ed927e09d6fb1e8d1446d49cdadecf051 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 20 Apr 2026 14:36:18 -0400 Subject: [PATCH 17/23] chore: upgrade wasmtime to 43 and bump toolchain to 1.91.0 wasmtime 43 requires Rust 1.91.0 (MSRV bump) and changed its error type from anyhow::Error to wasmtime::Error. Update snafu source fields in icp-sync-plugin accordingly and drop the now-unused anyhow dependency. Co-Authored-By: Claude Sonnet 4.6 --- Cargo.lock | 364 +++++++++++++------------- Cargo.toml | 4 +- crates/icp-sync-plugin/Cargo.toml | 1 - crates/icp-sync-plugin/src/runtime.rs | 10 +- rust-toolchain.toml | 2 +- 5 files changed, 185 insertions(+), 196 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 51ac76347..18eb3f797 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "addr2line" -version = "0.25.1" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" dependencies = [ "gimli", ] @@ -144,7 +144,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -155,7 +155,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -176,7 +176,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" dependencies = [ - "object", + "object 0.37.3", ] [[package]] @@ -1060,7 +1060,7 @@ dependencies = [ "cap-primitives", "cap-std", "io-lifetimes", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1089,7 +1089,7 @@ dependencies = [ "maybe-owned", "rustix 1.1.4", "rustix-linux-procfs", - "windows-sys 0.59.0", + "windows-sys 0.52.0", "winx", ] @@ -1478,46 +1478,48 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.128.4" +version = "0.130.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50a04121a197fde2fe896f8e7cac9812fc41ed6ee9c63e1906090f9f497845f6" +checksum = "046d4b584c3bb9b5eb500c8f29549bec36be11000f1ba2a927cef3d1a9875691" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.128.4" +version = "0.130.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a09e699a94f477303820fb2167024f091543d6240783a2d3b01a3f21c42bc744" +checksum = "b9b194a7870becb1490366fc0ae392ccd188065ff35f8391e77ac659db6fb977" dependencies = [ "cranelift-srcgen", ] [[package]] name = "cranelift-bforest" -version = "0.128.4" +version = "0.130.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f07732c662a9755529e332d86f8c5842171f6e98ba4d5976a178043dad838654" +checksum = "bb6a4ab44c6b371e661846b97dab687387a60ac4e2f864e2d4257284aad9e889" dependencies = [ "cranelift-entity", + "wasmtime-internal-core", ] [[package]] name = "cranelift-bitset" -version = "0.128.4" +version = "0.130.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18391da761cf362a06def7a7cf11474d79e55801dd34c2e9ba105b33dc0aef88" +checksum = "b8b7a44150c2f471a94023482bda1902710746e4bed9f9973d60c5a94319b06d" dependencies = [ "serde", "serde_derive", + "wasmtime-internal-core", ] [[package]] name = "cranelift-codegen" -version = "0.128.4" +version = "0.130.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b3a09b3042c69810d255aef59ddc3b3e4c0644d1d90ecfd6e3837798cc88a3c" +checksum = "01b06598133b1dd76758b8b95f8d6747c124124aade50cea96a3d88b962da9fa" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -1529,7 +1531,8 @@ dependencies = [ "cranelift-entity", "cranelift-isle", "gimli", - "hashbrown 0.15.5", + "hashbrown 0.16.1", + "libm", "log", "pulley-interpreter", "regalloc2", @@ -1537,14 +1540,14 @@ dependencies = [ "serde", "smallvec", "target-lexicon", - "wasmtime-internal-math", + "wasmtime-internal-core", ] [[package]] name = "cranelift-codegen-meta" -version = "0.128.4" +version = "0.130.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75817926ec812241889208d1b190cadb7fedded4592a4bb01b8524babb9e4849" +checksum = "6190e2e7bcf0a678da2f715363d34ed530fedf7a2f0ab75edaefef72a70465ff" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", @@ -1555,35 +1558,36 @@ dependencies = [ [[package]] name = "cranelift-codegen-shared" -version = "0.128.4" +version = "0.130.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859158f87a59476476eda3884d883c32e08a143cf3d315095533b362a3250a63" +checksum = "f583cf203d1aa8b79560e3b01f929bdacf9070b015eec4ea9c46e22a3f83e4a0" [[package]] name = "cranelift-control" -version = "0.128.4" +version = "0.130.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03b65a9aec442d715cbf54d14548b8f395476c09cef7abe03e104a378291ab88" +checksum = "803159df35cc398ae54473c150b16d6c77e92ab2948be638488de126a3328fbc" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.128.4" +version = "0.130.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8334c99a7e86060c24028732efd23bac84585770dcb752329c69f135d64f2fc1" +checksum = "3109e417257082d88087f5bcce677525bdaa8322b88dd7f175ed1a1fd41d546c" dependencies = [ "cranelift-bitset", "serde", "serde_derive", + "wasmtime-internal-core", ] [[package]] name = "cranelift-frontend" -version = "0.128.4" +version = "0.130.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43ac6c095aa5b3e845d7ca3461e67e2b65249eb5401477a5ff9100369b745111" +checksum = "14db6b0e0e4994c581092df78d837be2072578f7cb2528f96a6cf895e56dee63" dependencies = [ "cranelift-codegen", "log", @@ -1593,15 +1597,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.128.4" +version = "0.130.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d3d992870ed4f0f2e82e2175275cb3a123a46e9660c6558c46417b822c91fa" +checksum = "ec66ea5025c7317383699778282ac98741d68444f956e3b1d7b62f12b7216e67" [[package]] name = "cranelift-native" -version = "0.128.4" +version = "0.130.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee32e36beaf80f309edb535274cfe0349e1c5cf5799ba2d9f42e828285c6b52e" +checksum = "373ade56438e6232619d85678477d0a88a31b3581936e0503e61e96b546b0800" dependencies = [ "cranelift-codegen", "libc", @@ -1610,9 +1614,9 @@ dependencies = [ [[package]] name = "cranelift-srcgen" -version = "0.128.4" +version = "0.130.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "903adeaf4938e60209a97b53a2e4326cd2d356aab9764a1934630204bae381c9" +checksum = "ef53619d3cd5c78fd998c6d9420547af26b72e6456f94c2a8a2334cb76b42baa" [[package]] name = "crc32fast" @@ -2039,7 +2043,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2298,7 +2302,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2328,12 +2332,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - [[package]] name = "fancy-regex" version = "0.17.0" @@ -2369,7 +2367,7 @@ checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2511,7 +2509,7 @@ checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" dependencies = [ "io-lifetimes", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2739,11 +2737,12 @@ dependencies = [ [[package]] name = "gimli" -version = "0.32.3" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" dependencies = [ - "fallible-iterator", + "fnv", + "hashbrown 0.16.1", "indexmap", "stable_deref_trait", ] @@ -3099,7 +3098,6 @@ dependencies = [ "allocator-api2", "equivalent", "foldhash 0.1.5", - "serde", ] [[package]] @@ -3389,7 +3387,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core 0.61.2", ] [[package]] @@ -3880,7 +3878,6 @@ dependencies = [ name = "icp-sync-plugin" version = "0.2.3" dependencies = [ - "anyhow", "camino", "candid", "hex", @@ -4164,7 +4161,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65" dependencies = [ "io-lifetimes", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -4260,7 +4257,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4991,7 +4988,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5146,9 +5143,18 @@ name = "object" version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "object" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271638cd5fa9cca89c4c304675ca658efc4e64a66c716b7cfe1afb4b9611dbbc" dependencies = [ "crc32fast", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "indexmap", "memchr", ] @@ -5785,21 +5791,21 @@ dependencies = [ [[package]] name = "pulley-interpreter" -version = "41.0.4" +version = "43.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9812652c1feb63cf39f8780cecac154a32b22b3665806c733cd4072547233a4" +checksum = "010dec3755eb61b2f1051ecb3611b718460b7a74c131e474de2af20a845938af" dependencies = [ "cranelift-bitset", "log", "pulley-macros", - "wasmtime-internal-math", + "wasmtime-internal-core", ] [[package]] name = "pulley-macros" -version = "41.0.4" +version = "43.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56000349b6896e3d44286eb9c330891237f40b27fd43c1ccc84547d0b463cb40" +checksum = "ad360c32e85ca4b083ac0e2b6856e8f11c3d5060dafa7d5dc57b370857fa3018" dependencies = [ "proc-macro2", "quote", @@ -6092,13 +6098,13 @@ dependencies = [ [[package]] name = "regalloc2" -version = "0.13.5" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08effbc1fa53aaebff69521a5c05640523fab037b34a4a2c109506bc938246fa" +checksum = "de2c52737737f8609e94f975dee22854a2d5c125772d4b1cf292120f4d45c186" dependencies = [ "allocator-api2", "bumpalo", - "hashbrown 0.15.5", + "hashbrown 0.17.0", "log", "rustc-hash 2.1.2", "smallvec", @@ -6344,7 +6350,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -6357,7 +6363,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6424,7 +6430,7 @@ dependencies = [ "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -7094,7 +7100,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -7123,6 +7129,7 @@ dependencies = [ "cfg-if", "libc", "psm", + "windows-sys 0.52.0", "windows-sys 0.59.0", ] @@ -7306,7 +7313,7 @@ dependencies = [ "fd-lock", "io-lifetimes", "rustix 0.38.44", - "windows-sys 0.59.0", + "windows-sys 0.52.0", "winx", ] @@ -7343,7 +7350,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -7383,7 +7390,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -7825,7 +7832,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -8089,9 +8096,9 @@ dependencies = [ [[package]] name = "wasm-compose" -version = "0.243.0" +version = "0.245.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af801b6f36459023eaec63fdbaedad2fd5a4ab7dc74ecc110a8b5d375c5775e4" +checksum = "5fd23d12cc95c451c1306db5bc63075fbebb612bb70c53b4237b1ce5bc178343" dependencies = [ "anyhow", "heck", @@ -8103,29 +8110,29 @@ dependencies = [ "serde_derive", "serde_yaml", "smallvec", - "wasm-encoder 0.243.0", - "wasmparser 0.243.0", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", "wat", ] [[package]] name = "wasm-encoder" -version = "0.243.0" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55db9c896d70bd9fa535ce83cd4e1f2ec3726b0edd2142079f594fc3be1cb35" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" dependencies = [ "leb128fmt", - "wasmparser 0.243.0", + "wasmparser 0.244.0", ] [[package]] name = "wasm-encoder" -version = "0.244.0" +version = "0.245.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +checksum = "3f9dca005e69bf015e45577e415b9af8c67e8ee3c0e38b5b0add5aa92581ed5c" dependencies = [ "leb128fmt", - "wasmparser 0.244.0", + "wasmparser 0.245.1", ] [[package]] @@ -8163,19 +8170,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wasmparser" -version = "0.243.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d8db401b0528ec316dfbe579e6ab4152d61739cfe076706d2009127970159d" -dependencies = [ - "bitflags 2.11.1", - "hashbrown 0.15.5", - "indexmap", - "semver", - "serde", -] - [[package]] name = "wasmparser" version = "0.244.0" @@ -8214,23 +8208,22 @@ dependencies = [ [[package]] name = "wasmprinter" -version = "0.243.0" +version = "0.245.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2b6035559e146114c29a909a3232928ee488d6507a1504d8934e8607b36d7b" +checksum = "5f41517a3716fbb8ccf46daa9c1325f760fcbff5168e75c7392288e410b91ac8" dependencies = [ "anyhow", "termcolor", - "wasmparser 0.243.0", + "wasmparser 0.245.1", ] [[package]] name = "wasmtime" -version = "41.0.4" +version = "43.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2a83182bf04af87571b4c642300479501684f26bab5597f68f68cded5b098fd" +checksum = "ce205cd643d661b5ba5ba4717e13730262e8cdbc8f2eacbc7b906d45c1a74026" dependencies = [ "addr2line", - "anyhow", "async-trait", "bitflags 2.11.1", "bumpalo", @@ -8240,14 +8233,12 @@ dependencies = [ "futures", "fxprof-processed-profile", "gimli", - "hashbrown 0.15.5", - "indexmap", "ittapi", "libc", "log", "mach2", "memfd", - "object", + "object 0.38.1", "once_cell", "postcard", "pulley-interpreter", @@ -8261,18 +8252,17 @@ dependencies = [ "target-lexicon", "tempfile", "wasm-compose", - "wasm-encoder 0.243.0", - "wasmparser 0.243.0", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", "wasmtime-environ", "wasmtime-internal-cache", "wasmtime-internal-component-macro", "wasmtime-internal-component-util", + "wasmtime-internal-core", "wasmtime-internal-cranelift", "wasmtime-internal-fiber", "wasmtime-internal-jit-debug", "wasmtime-internal-jit-icache-coherence", - "wasmtime-internal-math", - "wasmtime-internal-slab", "wasmtime-internal-unwinder", "wasmtime-internal-versioned-export-macros", "wasmtime-internal-winch", @@ -8282,36 +8272,40 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "41.0.4" +version = "43.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb201c41aa23a3642365cfb2e4a183573d85127a3c9d528f56b9997c984541ab" +checksum = "0b8b78abf3677d4a0a5db82e5015b4d085ff3a1b8b472cbb8c70d4b769f019ce" dependencies = [ "anyhow", "cpp_demangle", + "cranelift-bforest", "cranelift-bitset", "cranelift-entity", "gimli", + "hashbrown 0.16.1", "indexmap", "log", - "object", + "object 0.38.1", "postcard", "rustc-demangle", "semver", "serde", "serde_derive", + "sha2 0.10.9", "smallvec", "target-lexicon", - "wasm-encoder 0.243.0", - "wasmparser 0.243.0", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", "wasmprinter", "wasmtime-internal-component-util", + "wasmtime-internal-core", ] [[package]] name = "wasmtime-internal-cache" -version = "41.0.4" +version = "43.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5b3069d1a67ba5969d0eb1ccd7e141367d4e713f4649aa90356c98e8f19bea" +checksum = "8e4fd4103ba413c0da2e636f73490c6c8e446d708cbde7573703941bc3d6a448" dependencies = [ "base64", "directories-next", @@ -8329,9 +8323,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-macro" -version = "41.0.4" +version = "43.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c924400db7b6ca996fef1b23beb0f41d5c809836b1ec60fc25b4057e2d25d9b" +checksum = "0d3d6914f34be2f9d78d8ee9f422e834dfc204e71ccce697205fae95fed87892" dependencies = [ "anyhow", "proc-macro2", @@ -8339,20 +8333,32 @@ dependencies = [ "syn 2.0.117", "wasmtime-internal-component-util", "wasmtime-internal-wit-bindgen", - "wit-parser 0.243.0", + "wit-parser 0.245.1", ] [[package]] name = "wasmtime-internal-component-util" -version = "41.0.4" +version = "43.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d3f65daf4bf3d74ca2fbbe20af0589c42e2b398a073486451425d94fd4afef4" +checksum = "3751b0616b914fdd87fe1bf804694a078f321b000338e6476bc48a4d6e454f21" + +[[package]] +name = "wasmtime-internal-core" +version = "43.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22632b187e1b0716f1b9ac57ad29013bed33175fcb19e10bb6896126f82fac67" +dependencies = [ + "anyhow", + "hashbrown 0.16.1", + "libm", + "serde", +] [[package]] name = "wasmtime-internal-cranelift" -version = "41.0.4" +version = "43.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633e889cdae76829738db0114ab3b02fce51ea4a1cd9675a67a65fce92e8b418" +checksum = "8b3ca07b3e0bb3429674b173b5800577719d600774dd81bff58f775c0aaa64ee" dependencies = [ "cfg-if", "cranelift-codegen", @@ -8363,23 +8369,23 @@ dependencies = [ "gimli", "itertools 0.14.0", "log", - "object", + "object 0.38.1", "pulley-interpreter", "smallvec", "target-lexicon", "thiserror 2.0.18", - "wasmparser 0.243.0", + "wasmparser 0.245.1", "wasmtime-environ", - "wasmtime-internal-math", + "wasmtime-internal-core", "wasmtime-internal-unwinder", "wasmtime-internal-versioned-export-macros", ] [[package]] name = "wasmtime-internal-fiber" -version = "41.0.4" +version = "43.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb126adc5d0c72695cfb77260b357f1b81705a0f8fa30b3944e7c2219c17341" +checksum = "20c8b2c9704eb1f33ead025ec16038277ccb63d0a14c31e99d5b765d7c36da55" dependencies = [ "cc", "cfg-if", @@ -8392,61 +8398,46 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-debug" -version = "41.0.4" +version = "43.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e66ff7f90a8002187691ff6237ffd09f954a0ebb9de8b2ff7f5c62632134120" +checksum = "d950310d07391d34369f62c48336ebb14eacbd4d6f772bb5f349c24e838e0664" dependencies = [ "cc", - "object", + "object 0.38.1", "rustix 1.1.4", "wasmtime-internal-versioned-export-macros", ] [[package]] name = "wasmtime-internal-jit-icache-coherence" -version = "41.0.4" +version = "43.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b96df23179ae16d54fb3a420f84ffe4383ec9dd06fad3e5bc782f85f66e8e08" +checksum = "3606662c156962d096be3127b8b8ae8ee2f8be3f896dad29259ff01ddb64abfd" dependencies = [ - "anyhow", "cfg-if", "libc", + "wasmtime-internal-core", "windows-sys 0.61.2", ] -[[package]] -name = "wasmtime-internal-math" -version = "41.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86d1380926682b44c383e9a67f47e7a95e60c6d3fa8c072294dab2c7de6168a0" -dependencies = [ - "libm", -] - -[[package]] -name = "wasmtime-internal-slab" -version = "41.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b63cbea1c0192c7feb7c0dfb35f47166988a3742f29f46b585ef57246c65764" - [[package]] name = "wasmtime-internal-unwinder" -version = "41.0.4" +version = "43.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f25c392c7e5fb891a7416e3c34cfbd148849271e8c58744fda875dde4bec4d6a" +checksum = "75eef0747e52dc545b075f64fd0e0cc237ae738e641266b1970e07e2d744bc32" dependencies = [ "cfg-if", "cranelift-codegen", "log", - "object", + "object 0.38.1", "wasmtime-environ", ] [[package]] name = "wasmtime-internal-versioned-export-macros" -version = "41.0.4" +version = "43.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70f8b9796a3f0451a7b702508b303d654de640271ac80287176de222f187a237" +checksum = "d8b0a5dab02a8fb527f547855ecc0e05f9fdc3d5bd57b8b080349408f9a6cece" dependencies = [ "proc-macro2", "quote", @@ -8455,16 +8446,16 @@ dependencies = [ [[package]] name = "wasmtime-internal-winch" -version = "41.0.4" +version = "43.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0063e61f1d0b2c20e9cfc58361a6513d074a23c80b417aac3033724f51648a0" +checksum = "8007342bd12ff400293a817973f7ecd6f1d9a8549a53369a9c1af357166f1f1e" dependencies = [ "cranelift-codegen", "gimli", "log", - "object", + "object 0.38.1", "target-lexicon", - "wasmparser 0.243.0", + "wasmparser 0.245.1", "wasmtime-environ", "wasmtime-internal-cranelift", "winch-codegen", @@ -8472,24 +8463,23 @@ dependencies = [ [[package]] name = "wasmtime-internal-wit-bindgen" -version = "41.0.4" +version = "43.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "587699ca7cae16b4a234ffcc834f37e75675933d533809919b52975f5609e2ef" +checksum = "7900c3e3c1d6e475bc225d73b02d6d5484815f260022e6964dca9558e50dd01a" dependencies = [ "anyhow", "bitflags 2.11.1", "heck", "indexmap", - "wit-parser 0.243.0", + "wit-parser 0.245.1", ] [[package]] name = "wasmtime-wasi" -version = "41.0.4" +version = "43.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2eb9dc95baed3cd86fdfebf9f9f333337eb308bf8bd973e0c7b06d9418c35f" +checksum = "ed3e3ddcfad69e9eb025bd19bff70dad45bafe1d6eacd134c0ffdfc4c161d045" dependencies = [ - "anyhow", "async-trait", "bitflags 2.11.1", "bytes", @@ -8516,14 +8506,14 @@ dependencies = [ [[package]] name = "wasmtime-wasi-io" -version = "41.0.4" +version = "43.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0b8402f1e04385071fdd96aca97cba995d7376b572e42ce5841d5b6aaf6fa30" +checksum = "3ca5dd3b9f04a851c422d05f333366722742da46bff9369ae0191f32cf83565a" dependencies = [ - "anyhow", "async-trait", "bytes", "futures", + "tracing", "wasmtime", ] @@ -8595,37 +8585,37 @@ checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" [[package]] name = "wiggle" -version = "41.0.4" +version = "43.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69a60bcbe1475c5dc9ec89210ade54823d44f742e283cba64f98f89697c4cec" +checksum = "cc1b1135efc8e5a008971897bea8d41ca56d8d501d4efb807842ae0a1c78f639" dependencies = [ - "anyhow", "bitflags 2.11.1", "thiserror 2.0.18", "tracing", "wasmtime", + "wasmtime-environ", "wiggle-macro", ] [[package]] name = "wiggle-generate" -version = "41.0.4" +version = "43.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21f3dc0fd4dcfc7736434bb216179a2147835309abc09bf226736a40d484548f" +checksum = "a7bc2b0d50ec8773b44fbfe1da6cb5cc44a92deaf8483233dcf0831e6db33172" dependencies = [ - "anyhow", "heck", "proc-macro2", "quote", "syn 2.0.117", + "wasmtime-environ", "witx", ] [[package]] name = "wiggle-macro" -version = "41.0.4" +version = "43.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea2aea744eded58ae092bf57110c27517dab7d5a300513ff13897325c5c5021" +checksum = "2d6c7d44ea552e1fbfdcd7a2cd83f5c2d1e803d5b1a11e3462c06888b77f455f" dependencies = [ "proc-macro2", "quote", @@ -8655,7 +8645,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -8666,11 +8656,10 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winch-codegen" -version = "41.0.4" +version = "43.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55de3ac5b8bd71e5f6c87a9e511dd3ceb194bdb58183c6a7bf21cd8c0e46fbc" +checksum = "eb9f45f7172a2628c8317766e427babc0a400f9d10b1c0f0b0617c5ed5b79de6" dependencies = [ - "anyhow", "cranelift-assembler-x64", "cranelift-codegen", "gimli", @@ -8678,10 +8667,10 @@ dependencies = [ "smallvec", "target-lexicon", "thiserror 2.0.18", - "wasmparser 0.243.0", + "wasmparser 0.245.1", "wasmtime-environ", + "wasmtime-internal-core", "wasmtime-internal-cranelift", - "wasmtime-internal-math", ] [[package]] @@ -9150,7 +9139,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" dependencies = [ "cfg-if", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -9160,7 +9149,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" dependencies = [ "bitflags 2.11.1", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -9241,9 +9230,9 @@ dependencies = [ [[package]] name = "wit-parser" -version = "0.243.0" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df983a8608e513d8997f435bb74207bf0933d0e49ca97aa9d8a6157164b9b7fc" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", @@ -9254,16 +9243,17 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", - "wasmparser 0.243.0", + "wasmparser 0.244.0", ] [[package]] name = "wit-parser" -version = "0.244.0" +version = "0.245.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +checksum = "330698718e82983499419494dd1e3d7811a457a9bf9f69734e8c5f07a2547929" dependencies = [ "anyhow", + "hashbrown 0.16.1", "id-arena", "indexmap", "log", @@ -9272,7 +9262,7 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", - "wasmparser 0.244.0", + "wasmparser 0.245.1", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0730ccbd3..969e91d61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -108,8 +108,8 @@ tracing-subscriber = "0.3.20" url = { version = "2.5.4", features = ["serde"] } uuid = { version = "1.16.0", features = ["serde", "v4"] } wasmparser = "0.245.1" -wasmtime = { version = "41", features = ["component-model"] } -wasmtime-wasi = { version = "41" } +wasmtime = { version = "43", features = ["component-model"] } +wasmtime-wasi = { version = "43" } winreg = "0.56.0" wslpath2 = "0.1" zeroize = "1.8.1" diff --git a/crates/icp-sync-plugin/Cargo.toml b/crates/icp-sync-plugin/Cargo.toml index fa2099ae6..242786139 100644 --- a/crates/icp-sync-plugin/Cargo.toml +++ b/crates/icp-sync-plugin/Cargo.toml @@ -7,7 +7,6 @@ repository.workspace = true publish.workspace = true [dependencies] -anyhow.workspace = true camino.workspace = true candid.workspace = true hex.workspace = true diff --git a/crates/icp-sync-plugin/src/runtime.rs b/crates/icp-sync-plugin/src/runtime.rs index 5e0e34c9d..8cccfb0cd 100644 --- a/crates/icp-sync-plugin/src/runtime.rs +++ b/crates/icp-sync-plugin/src/runtime.rs @@ -99,31 +99,31 @@ impl SyncPluginImports for HostState { pub enum RunPluginError { #[snafu(display("failed to create wasmtime engine for plugin at {path}"))] CreateEngine { - source: anyhow::Error, + source: wasmtime::Error, path: Utf8PathBuf, }, #[snafu(display("failed to load wasm component from {path}"))] LoadComponent { - source: anyhow::Error, + source: wasmtime::Error, path: Utf8PathBuf, }, #[snafu(display("failed to preopen directory '{dir}' for the plugin"))] PreopenDir { - source: anyhow::Error, + source: wasmtime::Error, dir: Utf8PathBuf, }, #[snafu(display("failed to instantiate wasm component at {path}"))] Instantiate { - source: anyhow::Error, + source: wasmtime::Error, path: Utf8PathBuf, }, #[snafu(display("failed to call exec() on plugin at {path}"))] CallExec { - source: anyhow::Error, + source: wasmtime::Error, path: Utf8PathBuf, }, diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 43e5784a1..cdeba7a2b 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.90.0" +channel = "1.91.0" components = ["rustfmt", "clippy"] From 504a6c7e40e8d136936a573d4a863c6428635fa4 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 27 Apr 2026 20:33:29 -0400 Subject: [PATCH 18/23] refactor: centralize wasm fetch/cache in canister::wasm module Extract shared wasm resolution logic (HTTP fetch, checksum verification, cache read/write) from prebuilt build and plugin sync into a single private canister::wasm module. Rename package cache abstractions from canister/prebuilt-specific names (CanisterCache, canisters_dir, canister_sha, read_cached_prebuilt, cache_prebuilt) to generic wasm equivalents, and move the on-disk subdirectory from "canisters/" to "wasms/" to reflect that plugin wasms are now cached there too. Co-Authored-By: Claude Sonnet 4.6 --- crates/icp-cli/src/commands/deploy.rs | 2 + crates/icp-cli/src/commands/sync.rs | 2 + crates/icp-cli/src/operations/sync.rs | 5 + crates/icp-sync-plugin/TODO.md | 6 - crates/icp/src/canister/build/prebuilt.rs | 153 ++-------------------- crates/icp/src/canister/mod.rs | 1 + crates/icp/src/canister/sync/mod.rs | 5 + crates/icp/src/canister/sync/plugin.rs | 143 +++++++------------- crates/icp/src/canister/wasm.rs | 152 +++++++++++++++++++++ crates/icp/src/package.rs | 24 ++-- 10 files changed, 243 insertions(+), 250 deletions(-) create mode 100644 crates/icp/src/canister/wasm.rs diff --git a/crates/icp-cli/src/commands/deploy.rs b/crates/icp-cli/src/commands/deploy.rs index 8c3fc56d8..e20ccdb57 100644 --- a/crates/icp-cli/src/commands/deploy.rs +++ b/crates/icp-cli/src/commands/deploy.rs @@ -362,6 +362,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow: // method to permit the user identity to upload assets directly before syncing. info!("Syncing canisters:"); + let pkg_cache = ctx.dirs.package_cache()?; sync_many( ctx.syncer.clone(), agent.clone(), @@ -369,6 +370,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow: environment_selection.name().to_owned(), args.proxy, ctx.debug, + &pkg_cache, ) .await?; } diff --git a/crates/icp-cli/src/commands/sync.rs b/crates/icp-cli/src/commands/sync.rs index 81be12170..aadeec0c8 100644 --- a/crates/icp-cli/src/commands/sync.rs +++ b/crates/icp-cli/src/commands/sync.rs @@ -82,6 +82,7 @@ pub(crate) async fn exec(ctx: &Context, args: &SyncArgs) -> Result<(), anyhow::E info!("Syncing canisters:"); + let pkg_cache = ctx.dirs.package_cache()?; sync_many( ctx.syncer.clone(), agent, @@ -89,6 +90,7 @@ pub(crate) async fn exec(ctx: &Context, args: &SyncArgs) -> Result<(), anyhow::E environment_selection.name().to_owned(), args.proxy, ctx.debug, + &pkg_cache, ) .await?; diff --git a/crates/icp-cli/src/operations/sync.rs b/crates/icp-cli/src/operations/sync.rs index b5b36e552..e0f6d4227 100644 --- a/crates/icp-cli/src/operations/sync.rs +++ b/crates/icp-cli/src/operations/sync.rs @@ -4,6 +4,7 @@ use ic_agent::Agent; use icp::{ Canister, canister::sync::{Params, Synchronize, SynchronizeError}, + package::PackageCache, prelude::PathBuf, }; use snafu::prelude::*; @@ -36,6 +37,7 @@ async fn sync_canister( environment: &str, proxy: Option, pb: &mut MultiStepProgressBar, + pkg_cache: &PackageCache, ) -> Result<(), SynchronizeError> { let step_count = canister_info.sync.steps.len(); @@ -58,6 +60,7 @@ async fn sync_canister( }, agent, Some(tx), + pkg_cache, ) .await; @@ -78,6 +81,7 @@ pub(crate) async fn sync_many( environment: String, proxy: Option, debug: bool, + pkg_cache: &PackageCache, ) -> Result<(), SyncOperationError> { let mut futs = FuturesOrdered::new(); let progress_manager = ProgressManager::new(ProgressManagerSettings { hidden: debug }); @@ -101,6 +105,7 @@ pub(crate) async fn sync_many( &environment, proxy, &mut pb, + pkg_cache, ) .await; diff --git a/crates/icp-sync-plugin/TODO.md b/crates/icp-sync-plugin/TODO.md index 52f73bab7..5f88208b8 100644 --- a/crates/icp-sync-plugin/TODO.md +++ b/crates/icp-sync-plugin/TODO.md @@ -1,11 +1,5 @@ # Sync Plugin TODO -## Wasm caching - -Cache remote plugin wasm files in `.icp/cache/` so they are not re-downloaded -on every sync. Key the cache entry on the sha256 checksum. When a remote step -has a sha256 and the cached file matches, skip the HTTP fetch entirely. - ## Plugin timeout Add `timeout_seconds: Option` to `manifest::adapter::plugin::Adapter` diff --git a/crates/icp/src/canister/build/prebuilt.rs b/crates/icp/src/canister/build/prebuilt.rs index 2131a7790..b7bc2431d 100644 --- a/crates/icp/src/canister/build/prebuilt.rs +++ b/crates/icp/src/canister/build/prebuilt.rs @@ -1,15 +1,8 @@ -use std::str::FromStr; - -use reqwest::{Client, Method, Request}; -use sha2::{Digest, Sha256}; use snafu::prelude::*; use tokio::sync::mpsc::Sender; -use url::Url; use crate::{ - fs::{read, write}, - manifest::adapter::prebuilt::{Adapter, SourceField}, - package::{PackageCache, cache_prebuilt, read_cached_prebuilt}, + canister::wasm, fs::write, manifest::adapter::prebuilt::Adapter, package::PackageCache, }; use super::Params; @@ -21,35 +14,11 @@ pub enum PrebuiltError { source: tokio::sync::mpsc::error::SendError, }, - #[snafu(display("failed to read prebuilt canister file"))] - ReadFile { source: crate::fs::IoError }, - - #[snafu(display("failed to parse prebuilt canister url"))] - ParseUrl { source: url::ParseError }, - - #[snafu(display("failed to fetch prebuilt canister file"))] - HttpRequest { source: reqwest::Error }, - - #[snafu(display("http request failed: {status}"))] - HttpStatus { status: reqwest::StatusCode }, - - #[snafu(display("failed to read http response"))] - HttpResponse { source: reqwest::Error }, - - #[snafu(display("checksum mismatch, expected: {expected}, actual: {actual}"))] - ChecksumMismatch { expected: String, actual: String }, + #[snafu(transparent)] + Wasm { source: wasm::WasmError }, #[snafu(display("failed to write wasm output file"))] WriteFile { source: crate::fs::IoError }, - - #[snafu(display("failed to read cached prebuilt canister file"))] - ReadCache { source: crate::fs::IoError }, - - #[snafu(display("failed to cache wasm file"))] - CacheFile { source: crate::fs::IoError }, - - #[snafu(display("failed to acquire lock on package cache"))] - LockCache { source: crate::fs::lock::LockError }, } pub(super) async fn build( @@ -58,115 +27,21 @@ pub(super) async fn build( stdio: Option>, pkg_cache: &PackageCache, ) -> Result<(), PrebuiltError> { - let wasm = match &adapter.source { - // Local path - SourceField::Local(s) => { - if let Some(stdio) = &stdio { - stdio - .send(format!("Reading local file: {}", s.path)) - .await - .context(LogSnafu)?; - } - read(¶ms.path.join(&s.path)).context(ReadFileSnafu)? - } - - // Remote url - SourceField::Remote(s) => 'wasm: { - // If it's already cached, use it instead of downloading again - if let Some(expected) = &adapter.sha256 { - let maybe_cached = pkg_cache - .with_read(async |r| read_cached_prebuilt(r, expected).context(ReadCacheSnafu)) - .await - .context(LockCacheSnafu)?; - if let Some(cached) = maybe_cached? { - if let Some(stdio) = &stdio { - stdio - .send("Using cached file".to_string()) - .await - .context(LogSnafu)?; - } - break 'wasm cached; - } - } - // Initialize a new http client - let http_client = Client::new(); - - // Parse Url - let u = Url::from_str(&s.url).context(ParseUrlSnafu)?; - if let Some(stdio) = &stdio { - stdio - .send(format!("Fetching remote file: {}", u)) - .await - .context(LogSnafu)?; - } - - // Construct request - let req = Request::new( - Method::GET, // method - u.to_owned(), // url - ); - - // Execute request - let resp = http_client.execute(req).await.context(HttpRequestSnafu)?; - - let status = resp.status(); - - // Check for success - if !status.is_success() { - return HttpStatusSnafu { status }.fail(); - } - - // Read response body - resp.bytes().await.context(HttpResponseSnafu)?.to_vec() - } - }; - - // Calculate checksum - let cksum = hex::encode({ - let mut h = Sha256::new(); - h.update(&wasm); - h.finalize() - }); - - // Verify the checksum if it's provided - if let Some(expected) = &adapter.sha256 { - if let Some(stdio) = &stdio { - stdio - .send("Verifying checksum".to_string()) - .await - .context(LogSnafu)?; - } - - // Verify Checksum - if &cksum != expected { - return ChecksumMismatchSnafu { - expected: expected.to_owned(), - actual: cksum, - } - .fail(); - } - } - - if matches!(&adapter.source, SourceField::Remote(_)) { - // Cache to disk - pkg_cache - .with_write(async |w| cache_prebuilt(w, &cksum, &wasm).context(CacheFileSnafu)) - .await - .context(LockCacheSnafu)??; - } + let wasm_bytes = wasm::resolve( + &adapter.source, + ¶ms.path, + adapter.sha256.as_deref(), + stdio.as_ref(), + pkg_cache, + ) + .await?; - // Set WASM file - if let Some(stdio) = stdio { - stdio - .send(format!("Writing WASM file: {}", params.output)) + if let Some(tx) = &stdio { + tx.send(format!("Writing WASM file: {}", params.output)) .await .context(LogSnafu)?; } - write( - ¶ms.output, // path - &wasm, // contents - ) - .context(WriteFileSnafu)?; + write(¶ms.output, &wasm_bytes).context(WriteFileSnafu)?; Ok(()) } diff --git a/crates/icp/src/canister/mod.rs b/crates/icp/src/canister/mod.rs index 791ca7888..12d8c3646 100644 --- a/crates/icp/src/canister/mod.rs +++ b/crates/icp/src/canister/mod.rs @@ -12,6 +12,7 @@ pub mod recipe; pub mod sync; mod script; +mod wasm; /// Controls who can read canister logs. /// Supports both string format ("controllers", "public") and object format ({ allowed_viewers: [...] }). diff --git a/crates/icp/src/canister/sync/mod.rs b/crates/icp/src/canister/sync/mod.rs index 59171c5aa..194ee6e94 100644 --- a/crates/icp/src/canister/sync/mod.rs +++ b/crates/icp/src/canister/sync/mod.rs @@ -5,6 +5,7 @@ use snafu::prelude::*; use tokio::sync::mpsc::Sender; use crate::manifest::canister::SyncStep; +use crate::package::PackageCache; use crate::prelude::*; mod assets; @@ -41,6 +42,7 @@ pub trait Synchronize: Sync + Send { params: &Params, agent: &Agent, stdio: Option>, + pkg_cache: &PackageCache, ) -> Result<(), SynchronizeError>; } @@ -54,6 +56,7 @@ impl Synchronize for Syncer { params: &Params, agent: &Agent, stdio: Option>, + pkg_cache: &PackageCache, ) -> Result<(), SynchronizeError> { match step { SyncStep::Assets(adapter) => Ok(assets::sync(adapter, params, agent).await?), @@ -65,6 +68,7 @@ impl Synchronize for Syncer { ¶ms.environment.clone(), params.proxy, stdio, + pkg_cache, ) .await?), } @@ -85,6 +89,7 @@ impl Synchronize for UnimplementedMockSyncer { _params: &Params, _agent: &Agent, _stdio: Option>, + _pkg_cache: &PackageCache, ) -> Result<(), SynchronizeError> { unimplemented!("UnimplementedMockSyncer::sync") } diff --git a/crates/icp/src/canister/sync/plugin.rs b/crates/icp/src/canister/sync/plugin.rs index 9912192e3..82c782ba6 100644 --- a/crates/icp/src/canister/sync/plugin.rs +++ b/crates/icp/src/canister/sync/plugin.rs @@ -2,61 +2,40 @@ use camino::Utf8PathBuf; use candid::Principal; use ic_agent::Agent; use icp_sync_plugin::{RunPluginError, run_plugin}; -use reqwest::{Client, Method, Request}; -use sha2::{Digest, Sha256}; use snafu::prelude::*; use tokio::sync::mpsc::Sender; -use url::Url; use crate::{ - fs::{read, read_to_string, write}, + canister::wasm, + fs::{read_to_string, write}, manifest::adapter::{plugin::Adapter, prebuilt::SourceField}, + package::PackageCache, }; use super::Params; #[derive(Debug, Snafu)] pub enum PluginError { - #[snafu(display("failed to read plugin wasm at '{path}'"))] - ReadWasm { - source: crate::fs::IoError, - path: Utf8PathBuf, - }, - #[snafu(display("failed to read plugin input file at '{path}'"))] ReadFile { source: crate::fs::IoError, path: Utf8PathBuf, }, - #[snafu(display("failed to parse plugin url"))] - ParseUrl { source: url::ParseError }, - - #[snafu(display("failed to fetch plugin wasm file"))] - HttpRequest { source: reqwest::Error }, - - #[snafu(display("http request failed: {status}"))] - HttpStatus { status: reqwest::StatusCode }, - - #[snafu(display("failed to read http response for plugin"))] - HttpResponse { source: reqwest::Error }, - #[snafu(display("failed to write downloaded plugin wasm to temp file"))] WriteTempWasm { source: crate::fs::IoError }, - #[snafu(display("plugin wasm checksum mismatch, expected: {expected}, actual: {actual}"))] - ChecksumMismatch { expected: String, actual: String }, + #[snafu(transparent)] + Wasm { source: wasm::WasmError }, #[snafu(display("failed to get identity principal: {err}"))] GetIdentityPrincipal { err: String }, + #[snafu(display("failed to acquire lock on package cache"))] + LockCache { source: crate::fs::lock::LockError }, + #[snafu(display("failed to run plugin"))] Run { source: RunPluginError }, - - #[snafu(display("failed to send log message"))] - Log { - source: tokio::sync::mpsc::error::SendError, - }, } pub(super) async fn sync( @@ -66,71 +45,50 @@ pub(super) async fn sync( environment: &str, proxy: Option, stdio: Option>, + pkg_cache: &PackageCache, ) -> Result<(), PluginError> { - // 1. Acquire the wasm bytes — either from a local path or a remote URL. - let (wasm_bytes, wasm_path) = match &adapter.source { - SourceField::Local(s) => { - let full_path = params.path.join(&s.path); - if let Some(tx) = &stdio { - tx.send(format!("Reading plugin wasm: {full_path}")) - .await - .context(LogSnafu)?; - } - let bytes = read(full_path.as_ref()).context(ReadWasmSnafu { - path: full_path.clone(), - })?; - (bytes, full_path) - } - - SourceField::Remote(s) => { - let url = Url::parse(&s.url).context(ParseUrlSnafu)?; - if let Some(tx) = &stdio { - tx.send(format!("Fetching plugin wasm: {url}")) + // 1. Determine the on-disk path for the wasm. run_plugin needs a path, not raw bytes. + // - Local: use the manifest path directly. + // - Remote + sha256 known: resolve via cache (download once, reuse thereafter); + // the stable cache path avoids a temp file. + // - Remote + no sha256: download and write to a temp file (cleaned up after). + let (wasm_path, is_temp) = match &adapter.source { + SourceField::Local(s) => (params.path.join(&s.path), false), + SourceField::Remote(_) => match &adapter.sha256 { + Some(sha) => { + wasm::resolve( + &adapter.source, + ¶ms.path, + Some(sha), + stdio.as_ref(), + pkg_cache, + ) + .await?; + let path = wasm::cached_path(pkg_cache, sha) .await - .context(LogSnafu)?; + .context(LockCacheSnafu)?; + (path, false) } - let client = Client::new(); - let req = Request::new(Method::GET, url); - let resp = client.execute(req).await.context(HttpRequestSnafu)?; - let status = resp.status(); - if !status.is_success() { - return HttpStatusSnafu { status }.fail(); + None => { + let wasm_bytes = wasm::resolve( + &adapter.source, + ¶ms.path, + None, + stdio.as_ref(), + pkg_cache, + ) + .await?; + let tmp = params.path.join(format!( + ".icp-plugin-{}.wasm", + hex::encode(&wasm_bytes[..std::cmp::min(8, wasm_bytes.len())]) + )); + write(tmp.as_ref(), &wasm_bytes).context(WriteTempWasmSnafu)?; + (tmp, true) } - let bytes = resp.bytes().await.context(HttpResponseSnafu)?.to_vec(); - - // Write to a temp file so we can pass a path to `run_plugin`. - let tmp_path = params.path.join(format!( - ".icp-plugin-{}.wasm", - hex::encode(&bytes[..std::cmp::min(8, bytes.len())]) - )); - write(tmp_path.as_ref(), &bytes).context(WriteTempWasmSnafu)?; - (bytes, tmp_path) - } + }, }; - // 2. Verify sha256 checksum if provided. - let cksum = hex::encode({ - let mut h = Sha256::new(); - h.update(&wasm_bytes); - h.finalize() - }); - - if let Some(expected) = &adapter.sha256 { - if let Some(tx) = &stdio { - tx.send("Verifying plugin wasm checksum".to_string()) - .await - .context(LogSnafu)?; - } - if &cksum != expected { - return ChecksumMismatchSnafu { - expected: expected.clone(), - actual: cksum, - } - .fail(); - } - } - - // 3. Collect inputs: `dirs` stays as manifest strings (runtime preopens them), + // 2. Collect inputs: `dirs` stays as manifest strings (runtime preopens them), // `files` are read on the host and passed inline. let base_dir = Utf8PathBuf::from(params.path.as_str()); let dirs: Vec = adapter.dirs.clone().unwrap_or_default(); @@ -142,19 +100,18 @@ pub(super) async fn sync( files.push((name.clone(), content)); } - // 4. Run the plugin (blocking call — signal Tokio that this thread will block). + // 3. Run the plugin (blocking call — signal Tokio that this thread will block). let identity_principal = agent .get_principal() .map_err(|err| PluginError::GetIdentityPrincipal { err })?; - let wasm_path_buf = Utf8PathBuf::from(wasm_path.as_str()); let agent_clone = agent.clone(); let environment_owned = environment.to_owned(); let stdio_clone = stdio.clone(); tokio::task::block_in_place(|| { run_plugin( - wasm_path_buf, + wasm_path.clone(), base_dir, dirs, files, @@ -168,8 +125,8 @@ pub(super) async fn sync( }) .context(RunSnafu)?; - // Clean up temp file if we downloaded from a remote URL. - if matches!(&adapter.source, SourceField::Remote(_)) { + // Clean up temp file if we downloaded from a remote URL with no sha256. + if is_temp { let _ = std::fs::remove_file(wasm_path.as_std_path()); } diff --git a/crates/icp/src/canister/wasm.rs b/crates/icp/src/canister/wasm.rs new file mode 100644 index 000000000..b43cfecf7 --- /dev/null +++ b/crates/icp/src/canister/wasm.rs @@ -0,0 +1,152 @@ +use camino::{Utf8Path, Utf8PathBuf}; +use reqwest::{Client, Method, Request}; +use sha2::{Digest, Sha256}; +use snafu::prelude::*; +use tokio::sync::mpsc::Sender; +use url::Url; + +use crate::{ + fs::read, + manifest::adapter::prebuilt::SourceField, + package::{PackageCache, cache_wasm, read_cached_wasm}, +}; + +#[derive(Debug, Snafu)] +pub enum WasmError { + #[snafu(display("failed to read wasm file at '{path}'"))] + ReadLocal { + source: crate::fs::IoError, + path: Utf8PathBuf, + }, + + #[snafu(display("failed to parse wasm url"))] + ParseUrl { source: url::ParseError }, + + #[snafu(display("failed to fetch wasm file"))] + HttpRequest { source: reqwest::Error }, + + #[snafu(display("http request failed: {status}"))] + HttpStatus { status: reqwest::StatusCode }, + + #[snafu(display("failed to read http response"))] + HttpResponse { source: reqwest::Error }, + + #[snafu(display("checksum mismatch, expected: {expected}, actual: {actual}"))] + ChecksumMismatch { expected: String, actual: String }, + + #[snafu(display("failed to send log message"))] + Log { + source: tokio::sync::mpsc::error::SendError, + }, + + #[snafu(display("failed to read cached wasm file"))] + ReadCache { source: crate::fs::IoError }, + + #[snafu(display("failed to cache wasm file"))] + CacheFile { source: crate::fs::IoError }, + + #[snafu(display("failed to acquire lock on package cache"))] + LockCache { source: crate::fs::lock::LockError }, +} + +/// Fetch wasm bytes from a `SourceField` (local path or remote URL), optionally verifying +/// the sha256 checksum. Does not interact with the cache. +async fn fetch( + source: &SourceField, + base_dir: &Utf8Path, + sha256: Option<&str>, + stdio: Option<&Sender>, +) -> Result, WasmError> { + let bytes = match source { + SourceField::Local(s) => { + let path = base_dir.join(&s.path); + if let Some(tx) = stdio { + tx.send(format!("Reading wasm: {path}")) + .await + .context(LogSnafu)?; + } + read(&path).context(ReadLocalSnafu { path })? + } + SourceField::Remote(s) => { + let url = Url::parse(&s.url).context(ParseUrlSnafu)?; + if let Some(tx) = stdio { + tx.send(format!("Fetching wasm: {url}")) + .await + .context(LogSnafu)?; + } + let resp = Client::new() + .execute(Request::new(Method::GET, url)) + .await + .context(HttpRequestSnafu)?; + let status = resp.status(); + if !status.is_success() { + return HttpStatusSnafu { status }.fail(); + } + resp.bytes().await.context(HttpResponseSnafu)?.to_vec() + } + }; + + if let Some(expected) = sha256 { + if let Some(tx) = stdio { + tx.send("Verifying checksum".to_string()) + .await + .context(LogSnafu)?; + } + let actual = hex::encode(Sha256::digest(&bytes)); + ensure!( + actual == expected, + ChecksumMismatchSnafu { + expected: expected.to_owned(), + actual, + } + ); + } + + Ok(bytes) +} + +/// Resolve wasm bytes from a `SourceField` (local path or remote URL), optionally verifying +/// the sha256 checksum. For remote sources, checks the local cache before downloading and +/// stores the result afterwards. +pub async fn resolve( + source: &SourceField, + base_dir: &Utf8Path, + sha256: Option<&str>, + stdio: Option<&Sender>, + pkg_cache: &PackageCache, +) -> Result, WasmError> { + if let (SourceField::Remote(_), Some(expected)) = (source, sha256) { + let maybe_cached = pkg_cache + .with_read(async |r| read_cached_wasm(r, expected).context(ReadCacheSnafu)) + .await + .context(LockCacheSnafu)?; + if let Some(cached) = maybe_cached? { + if let Some(tx) = stdio { + tx.send("Using cached file".to_string()) + .await + .context(LogSnafu)?; + } + return Ok(cached); + } + } + + let bytes = fetch(source, base_dir, sha256, stdio).await?; + + if matches!(source, SourceField::Remote(_)) { + let cksum = hex::encode(Sha256::digest(&bytes)); + pkg_cache + .with_write(async |w| cache_wasm(w, &cksum, &bytes).context(CacheFileSnafu)) + .await + .context(LockCacheSnafu)??; + } + + Ok(bytes) +} + +/// Returns the stable on-disk path for a cached wasm by sha256. +pub async fn cached_path( + pkg_cache: &PackageCache, + sha: &str, +) -> Result { + pkg_cache.with_read(async |r| r.wasm_sha(sha).wasm()).await +} diff --git a/crates/icp/src/package.rs b/crates/icp/src/package.rs index 3bc2d56fd..e031f681b 100644 --- a/crates/icp/src/package.rs +++ b/crates/icp/src/package.rs @@ -22,15 +22,15 @@ impl PackageCachePaths { pub fn project_templates_dir(&self) -> PathBuf { self.root.join("project-templates") } - pub fn canisters_dir(&self) -> PathBuf { - self.root.join("canisters") + pub fn wasms_dir(&self) -> PathBuf { + self.root.join("wasms") } pub fn launcher_version(&self, version: &str) -> PathBuf { self.launcher_dir().join(version) } - pub fn canister_sha(&self, sha: &str) -> CanisterCache { - CanisterCache { - dir: self.canisters_dir().join(sha), + pub fn wasm_sha(&self, sha: &str) -> WasmCache { + WasmCache { + dir: self.wasms_dir().join(sha), } } pub fn recipe_sha(&self, sha: &str) -> RecipeCache { @@ -46,16 +46,16 @@ impl PackageCachePaths { } } -pub struct CanisterCache { +pub struct WasmCache { dir: PathBuf, } -impl CanisterCache { +impl WasmCache { pub fn dir(&self) -> &Path { &self.dir } pub fn wasm(&self) -> PathBuf { - self.dir.join("canister.wasm") + self.dir.join("module.wasm") } pub fn atime(&self) -> PathBuf { self.dir.join(".atime") @@ -78,11 +78,11 @@ impl RecipeCache { } } -pub fn read_cached_prebuilt( +pub fn read_cached_wasm( cache: LRead<&PackageCachePaths>, sha: &str, ) -> Result>, crate::fs::IoError> { - let cache_path = cache.canister_sha(sha); + let cache_path = cache.wasm_sha(sha); let cache_wasm_path = cache_path.wasm(); if cache_wasm_path.exists() { let wasm = crate::fs::read(&cache_wasm_path)?; @@ -93,12 +93,12 @@ pub fn read_cached_prebuilt( } } -pub fn cache_prebuilt( +pub fn cache_wasm( cache: LWrite<&PackageCachePaths>, sha: &str, wasm: &[u8], ) -> Result<(), crate::fs::IoError> { - let cache_path = cache.canister_sha(sha); + let cache_path = cache.wasm_sha(sha); let cache_wasm_path = cache_path.wasm(); if !cache_wasm_path.exists() { crate::fs::create_dir_all(cache_path.dir())?; From 56abfd16bb5590e84eb57a6f243921394f2510dd Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 27 Apr 2026 21:01:22 -0400 Subject: [PATCH 19/23] test(icp-sync-plugin): add unit tests for run_plugin error paths and execution semantics Adds a wasm32-wasip2 test fixture crate that implements the sync-plugin WIT world with behaviour controlled via the `environment` field, and a build.rs step that compiles it into OUT_DIR and exposes the path via TEST_PLUGIN_WASM. Five tests cover: missing WASM (LoadComponent), missing preopened dir (PreopenDir), plugin Ok/Err returns, and stdout capture through the stdio channel. Co-Authored-By: Claude Sonnet 4.6 --- crates/icp-sync-plugin/Cargo.toml | 3 + crates/icp-sync-plugin/build.rs | 39 ++ crates/icp-sync-plugin/src/runtime.rs | 134 +++++++ .../tests/fixtures/test-plugin/Cargo.lock | 338 ++++++++++++++++++ .../tests/fixtures/test-plugin/Cargo.toml | 13 + .../tests/fixtures/test-plugin/build.rs | 3 + .../tests/fixtures/test-plugin/src/lib.rs | 22 ++ 7 files changed, 552 insertions(+) create mode 100644 crates/icp-sync-plugin/tests/fixtures/test-plugin/Cargo.lock create mode 100644 crates/icp-sync-plugin/tests/fixtures/test-plugin/Cargo.toml create mode 100644 crates/icp-sync-plugin/tests/fixtures/test-plugin/build.rs create mode 100644 crates/icp-sync-plugin/tests/fixtures/test-plugin/src/lib.rs diff --git a/crates/icp-sync-plugin/Cargo.toml b/crates/icp-sync-plugin/Cargo.toml index 242786139..4cc62252b 100644 --- a/crates/icp-sync-plugin/Cargo.toml +++ b/crates/icp-sync-plugin/Cargo.toml @@ -17,5 +17,8 @@ tokio.workspace = true wasmtime.workspace = true wasmtime-wasi.workspace = true +[build-dependencies] +camino.workspace = true + [lints] workspace = true diff --git a/crates/icp-sync-plugin/build.rs b/crates/icp-sync-plugin/build.rs index 44680d997..5e19e195f 100644 --- a/crates/icp-sync-plugin/build.rs +++ b/crates/icp-sync-plugin/build.rs @@ -1,3 +1,42 @@ +use camino::Utf8PathBuf; +use std::process::Command; + fn main() { println!("cargo:rerun-if-changed=sync-plugin.wit"); + println!("cargo:rerun-if-changed=tests/fixtures/test-plugin/src/lib.rs"); + println!("cargo:rerun-if-changed=tests/fixtures/test-plugin/Cargo.toml"); + + build_test_fixture(); +} + +fn build_test_fixture() { + let manifest_dir = Utf8PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); + let out_dir = Utf8PathBuf::from(std::env::var("OUT_DIR").unwrap()); + let fixture_manifest = manifest_dir.join("tests/fixtures/test-plugin/Cargo.toml"); + let fixture_target_dir = out_dir.join("fixture-target"); + let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); + + let status = Command::new(&cargo) + .args([ + "build", + "--target", + "wasm32-wasip2", + "--release", + "--manifest-path", + fixture_manifest.as_str(), + "--target-dir", + fixture_target_dir.as_str(), + ]) + .status(); + + match status { + Ok(s) if s.success() => { + let wasm = fixture_target_dir.join("wasm32-wasip2/release/test_plugin.wasm"); + println!("cargo:rustc-env=TEST_PLUGIN_WASM={wasm}"); + } + _ => { + // wasm32-wasip2 target not installed or build failed; fixture-dependent + // tests will be skipped via option_env!("TEST_PLUGIN_WASM"). + } + } } diff --git a/crates/icp-sync-plugin/src/runtime.rs b/crates/icp-sync-plugin/src/runtime.rs index 8cccfb0cd..3dfc22870 100644 --- a/crates/icp-sync-plugin/src/runtime.rs +++ b/crates/icp-sync-plugin/src/runtime.rs @@ -242,3 +242,137 @@ pub fn run_plugin( Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + use candid::Principal; + use ic_agent::Agent; + + fn dummy_agent() -> Agent { + Agent::builder() + .with_url("http://127.0.0.1:4943") + .build() + .expect("build test agent") + } + + fn anon() -> Principal { + Principal::anonymous() + } + + // ------------------------------------------------------------------------- + // Error-path tests — no fixture WASM needed + // ------------------------------------------------------------------------- + + #[test] + fn load_component_error_on_missing_file() { + let result = run_plugin( + "nonexistent.wasm".into(), + ".".into(), + vec![], + vec![], + anon(), + dummy_agent(), + None, + anon(), + "test".to_string(), + None, + ); + assert!(matches!(result, Err(RunPluginError::LoadComponent { .. }))); + } + + // ------------------------------------------------------------------------- + // Fixture-dependent tests — skipped when TEST_PLUGIN_WASM is not set + // ------------------------------------------------------------------------- + + #[test] + fn preopen_dir_error_on_missing_dir() { + let Some(wasm_path) = option_env!("TEST_PLUGIN_WASM") else { + eprintln!("skipping: TEST_PLUGIN_WASM not set (install wasm32-wasip2 target)"); + return; + }; + let result = run_plugin( + wasm_path.into(), + ".".into(), + vec!["nonexistent_dir".to_string()], + vec![], + anon(), + dummy_agent(), + None, + anon(), + "test".to_string(), + None, + ); + assert!(matches!(result, Err(RunPluginError::PreopenDir { .. }))); + } + + #[test] + fn plugin_success_returns_ok() { + let Some(wasm_path) = option_env!("TEST_PLUGIN_WASM") else { + eprintln!("skipping: TEST_PLUGIN_WASM not set (install wasm32-wasip2 target)"); + return; + }; + let result = run_plugin( + wasm_path.into(), + ".".into(), + vec![], + vec![], + anon(), + dummy_agent(), + None, + anon(), + "ok".to_string(), + None, + ); + assert!(result.is_ok()); + } + + #[test] + fn plugin_failure_maps_to_run_plugin_error() { + let Some(wasm_path) = option_env!("TEST_PLUGIN_WASM") else { + eprintln!("skipping: TEST_PLUGIN_WASM not set (install wasm32-wasip2 target)"); + return; + }; + let result = run_plugin( + wasm_path.into(), + ".".into(), + vec![], + vec![], + anon(), + dummy_agent(), + None, + anon(), + "error".to_string(), + None, + ); + assert!(matches!( + result, + Err(RunPluginError::PluginFailed { ref message }) if message == "deliberate failure" + )); + } + + #[test] + fn plugin_stdout_forwarded_through_stdio_channel() { + let Some(wasm_path) = option_env!("TEST_PLUGIN_WASM") else { + eprintln!("skipping: TEST_PLUGIN_WASM not set (install wasm32-wasip2 target)"); + return; + }; + let (tx, mut rx) = tokio::sync::mpsc::channel::(16); + let result = run_plugin( + wasm_path.into(), + ".".into(), + vec![], + vec![], + anon(), + dummy_agent(), + None, + anon(), + "print".to_string(), + Some(tx), + ); + assert!(result.is_ok()); + let msg = rx.try_recv().expect("expected stdout message on channel"); + assert!(msg.contains("stdout from plugin"), "got: {msg}"); + } +} diff --git a/crates/icp-sync-plugin/tests/fixtures/test-plugin/Cargo.lock b/crates/icp-sync-plugin/tests/fixtures/test-plugin/Cargo.lock new file mode 100644 index 000000000..9bafdf433 --- /dev/null +++ b/crates/icp-sync-plugin/tests/fixtures/test-plugin/Cargo.lock @@ -0,0 +1,338 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "macro-string" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a9dbbfc75d2688ed057456ce8a3ee3f48d12eec09229f560f3643b9f275653" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "test-plugin" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "wasm-encoder" +version = "0.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61fb705ce81adde29d2a8e99d87995e39a6e927358c91398f374474746070ef7" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e4c2aa916c425dcca61a6887d3e135acdee2c6d0ed51fd61c08d41ddaf62b1" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71cde4757396defafd25417cfb36aa3161027d06d865b0c24baaae229aac005d" +dependencies = [ + "bitflags", + "hashbrown 0.16.1", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7607d30e7e5e8fd5a0695f7cb8b2128829e0bf9dca7a1fe8c4d6ed3ca1058fce" +dependencies = [ + "bitflags", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda3a4ce47c08d27f575d451a60102bab5251776abd0a7a323d1f038eb6339ab" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "920a1c8c0f89397431db4900a7bf7c511b78e1b7068289fe812dc76e993f1491" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "857a143d2373abfcd31ad946393efe775ed8c90a2a365ce73c61bf38f36a1000" +dependencies = [ + "anyhow", + "macro-string", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1936c26cb24b93dc36bf78fb5dc35c55cd37f66ecdc2d2663a717d9fb3ee951e" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.246.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd979042b5ff288607ccf3b314145435453f20fc67173195f91062d2289b204d" +dependencies = [ + "anyhow", + "hashbrown 0.16.1", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/crates/icp-sync-plugin/tests/fixtures/test-plugin/Cargo.toml b/crates/icp-sync-plugin/tests/fixtures/test-plugin/Cargo.toml new file mode 100644 index 000000000..e2283428e --- /dev/null +++ b/crates/icp-sync-plugin/tests/fixtures/test-plugin/Cargo.toml @@ -0,0 +1,13 @@ +[workspace] + +[package] +name = "test-plugin" +version = "0.1.0" +edition = "2024" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = { version = "0.56", features = ["realloc"] } diff --git a/crates/icp-sync-plugin/tests/fixtures/test-plugin/build.rs b/crates/icp-sync-plugin/tests/fixtures/test-plugin/build.rs new file mode 100644 index 000000000..5edaa79cf --- /dev/null +++ b/crates/icp-sync-plugin/tests/fixtures/test-plugin/build.rs @@ -0,0 +1,3 @@ +fn main() { + println!("cargo:rerun-if-changed=../../../sync-plugin.wit"); +} diff --git a/crates/icp-sync-plugin/tests/fixtures/test-plugin/src/lib.rs b/crates/icp-sync-plugin/tests/fixtures/test-plugin/src/lib.rs new file mode 100644 index 000000000..f1f5b0067 --- /dev/null +++ b/crates/icp-sync-plugin/tests/fixtures/test-plugin/src/lib.rs @@ -0,0 +1,22 @@ +wit_bindgen::generate!({ + world: "sync-plugin", + path: "../../../sync-plugin.wit", +}); + +struct TestPlugin; + +impl Guest for TestPlugin { + fn exec(input: SyncExecInput) -> Result, String> { + match input.environment.as_str() { + "error" => Err("deliberate failure".to_string()), + "hello" => Ok(Some("hello".to_string())), + "print" => { + println!("stdout from plugin"); + Ok(None) + } + _ => Ok(None), + } + } +} + +export!(TestPlugin); From dd02be6cd7b7e4273f218c3d6608cb1418449318 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 27 Apr 2026 21:37:14 -0400 Subject: [PATCH 20/23] test(icp-sync-plugin): add e2e integration test for sync plugin happy path Covers the full round-trip: compile canister + plugin from examples/icp-sync-plugin/ at test time (no committed binaries), deploy to a local managed network, run icp sync, and verify the canister state via a query call. Co-Authored-By: Claude Sonnet 4.6 --- crates/icp-cli/tests/sync_tests.rs | 134 +++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/crates/icp-cli/tests/sync_tests.rs b/crates/icp-cli/tests/sync_tests.rs index a6a90da68..24bc185a7 100644 --- a/crates/icp-cli/tests/sync_tests.rs +++ b/crates/icp-cli/tests/sync_tests.rs @@ -411,6 +411,140 @@ async fn sync_multiple_canisters() { .stderr(contains("DEBUG icp::progress: syncing canister-c").not()); } +/// Compiles the canister and plugin from `examples/icp-sync-plugin/` and returns +/// (canister_wasm_path, plugin_wasm_path). Cargo caches the build so subsequent +/// test runs are fast when sources haven't changed. +fn build_sync_plugin_example() -> (PathBuf, PathBuf) { + let example_dir = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../examples/icp-sync-plugin"); + // Use CARGO env var when available (set by cargo test), fall back to PATH lookup. + let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); + + let status = std::process::Command::new(&cargo) + .args([ + "build", + "--target", + "wasm32-unknown-unknown", + "--release", + "-p", + "canister", + ]) + .current_dir(&example_dir) + .status() + .expect("failed to spawn cargo build for canister"); + assert!( + status.success(), + "cargo build --target wasm32-unknown-unknown failed" + ); + + let status = std::process::Command::new(&cargo) + .args([ + "build", + "--target", + "wasm32-wasip2", + "--release", + "-p", + "plugin", + ]) + .current_dir(&example_dir) + .status() + .expect("failed to spawn cargo build for plugin"); + assert!( + status.success(), + "cargo build --target wasm32-wasip2 failed" + ); + + ( + example_dir.join("target/wasm32-unknown-unknown/release/canister.wasm"), + example_dir.join("target/wasm32-wasip2/release/plugin.wasm"), + ) +} + +#[tokio::test] +async fn sync_plugin_registers_seed_data() { + let ctx = TestContext::new(); + let project_dir = ctx.create_project_dir("icp"); + + let (canister_wasm, plugin_wasm) = build_sync_plugin_example(); + + // Create seed-data directory with fruit files + let seed_data = project_dir.join("seed-data"); + create_dir_all(&seed_data).expect("failed to create seed-data"); + write_string(&seed_data.join("fruit-01.txt"), "apple").expect("failed to write fruit-01.txt"); + write_string(&seed_data.join("fruit-02.txt"), "banana").expect("failed to write fruit-02.txt"); + write_string(&seed_data.join("fruit-03.txt"), "cherry").expect("failed to write fruit-03.txt"); + + // Manifest: pre-built canister wasm + plugin sync step pointing at the pre-built plugin wasm. + // dirs is relative to the project directory and preopened read-only inside the plugin's WASI sandbox. + let pm = formatdoc! {r#" + canisters: + - name: my-canister + build: + steps: + - type: script + command: cp '{canister_wasm}' "$ICP_WASM_OUTPUT_PATH" + sync: + steps: + - type: plugin + path: {plugin_wasm} + dirs: + - seed-data + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + // Start network + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + // Mint cycles and deploy (user identity becomes the canister controller) + clients::icp(&ctx, &project_dir, Some("random-environment".to_string())) + .mint_cycles(10 * TRILLION); + + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "--subnet", + common::SUBNET_ID, + "--environment", + "random-environment", + ]) + .assert() + .success(); + + // Run sync: plugin calls set_uploader (user is controller, so the direct call is permitted), + // then calls register for each fruit file directly with the user identity as the uploader. + ctx.icp() + .current_dir(&project_dir) + .args(["sync", "my-canister", "--environment", "random-environment"]) + .assert() + .success(); + + // Query the canister to verify all three fruits were registered + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "call", + "my-canister", + "show", + "()", + "--query", + "--environment", + "random-environment", + ]) + .assert() + .success() + .stdout( + contains("apple") + .and(contains("banana")) + .and(contains("cherry")), + ); +} + #[tokio::test] async fn sync_all_canisters_in_environment() { let ctx = TestContext::new(); From 561c75af3fd4a260af70fc82d5bf42f2b5dd48d8 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 27 Apr 2026 21:55:04 -0400 Subject: [PATCH 21/23] docs: update changelog for sync plugin system and icp sync --proxy Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3681babb..c74a7f07a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +* feat: Canister manifests now support a `plugin` sync step type. Plugins are WebAssembly components that run in a sandboxed environment and can drive arbitrary post-deployment logic against the canister being synced. See `crates/icp-sync-plugin/DESIGN.md` for details. +* feat: `icp sync` now accepts `--proxy` to route management canister calls through a proxy canister, consistent with other `icp` subcommands. * fix: `icp canister call` now serializes arguments built via the interactive Candid assist prompt against the method's declared signature, matching the behavior of arguments passed on the command line. Previously, narrower values (e.g. a variant case from a multi-case variant) were encoded with a type table inferred only from the value, which the target canister rejected with errors like "Variant index N larger than length 1". # v0.2.5 From 541b44be23e7048ad135f27722a43c5a193881ef Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 27 Apr 2026 22:23:44 -0400 Subject: [PATCH 22/23] fix: update stale test expectations and add wasm targets to toolchain Update build_adapter_display_failing_prebuilt_output test to match current error messages. Add wasm32-unknown-unknown and wasm32-wasip2 targets to rust-toolchain.toml so sync_tests pass in CI and locally. Co-Authored-By: Claude Sonnet 4.6 --- crates/icp-cli/tests/build_tests.rs | 4 ++-- rust-toolchain.toml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/icp-cli/tests/build_tests.rs b/crates/icp-cli/tests/build_tests.rs index bf1f7ad87..c70320da2 100644 --- a/crates/icp-cli/tests/build_tests.rs +++ b/crates/icp-cli/tests/build_tests.rs @@ -220,11 +220,11 @@ fn build_adapter_display_failing_prebuilt_output() { // Invoke build let expected_output = indoc! {r#" ERR ----- Failed to build canister 'my-canister' ----- - ERR 'failed to read prebuilt canister file' + ERR 'failed to read wasm file at '/nonexistent/path/to/wasm.wasm'' ERR [my-canister] Build output: ERR [my-canister] Building: step 2 of 2 (pre-built): ERR [my-canister] path: /nonexistent/path/to/wasm.wasm, sha: invalid: - ERR [my-canister] > Reading local file: /nonexistent/path/to/wasm.wasm + ERR [my-canister] > Reading wasm: /nonexistent/path/to/wasm.wasm "#}; ctx.icp() diff --git a/rust-toolchain.toml b/rust-toolchain.toml index cdeba7a2b..227c37ec3 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,4 @@ [toolchain] channel = "1.91.0" components = ["rustfmt", "clippy"] +targets = ["wasm32-unknown-unknown", "wasm32-wasip2"] From 97efbd9368e367d192ec9c321bd6087f18dd1c4c Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 27 Apr 2026 23:06:01 -0400 Subject: [PATCH 23/23] fix: use manifest path in wasm read log and error messages On Windows, joining an absolute Unix path (e.g. /foo) with a base dir prepends the current drive letter (e.g. C:/foo), causing error messages to differ across platforms. Use the original path from the manifest (s.path) in the log and error context instead of the OS-resolved path. Co-Authored-By: Claude Sonnet 4.6 --- crates/icp/src/canister/wasm.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/icp/src/canister/wasm.rs b/crates/icp/src/canister/wasm.rs index b43cfecf7..8237f6729 100644 --- a/crates/icp/src/canister/wasm.rs +++ b/crates/icp/src/canister/wasm.rs @@ -61,11 +61,13 @@ async fn fetch( SourceField::Local(s) => { let path = base_dir.join(&s.path); if let Some(tx) = stdio { - tx.send(format!("Reading wasm: {path}")) + tx.send(format!("Reading wasm: {}", s.path)) .await .context(LogSnafu)?; } - read(&path).context(ReadLocalSnafu { path })? + read(&path).context(ReadLocalSnafu { + path: s.path.clone(), + })? } SourceField::Remote(s) => { let url = Url::parse(&s.url).context(ParseUrlSnafu)?;