From 949ad2a989a6076f747fd6beb7050eac6338b06e Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Wed, 15 Apr 2026 16:01:54 -0400 Subject: [PATCH 01/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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/39] 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)?; From 35d3b44f576b5a702f25f65737d4cf11c439376f Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Tue, 28 Apr 2026 09:39:40 -0400 Subject: [PATCH 24/39] fix: address Copilot review comments on sync plugin PR Bugs: - Fix temp file leak: cleanup now runs on both success and error paths in plugin::sync, not only on success. - Remove needless clone: ¶ms.environment.clone() -> ¶ms.environment (params is already &Params). Security: - Validate `files` manifest entries in plugin::sync: reject absolute paths and '..' components before joining with the canister directory. - Validate `dirs` entries in run_plugin: same check before preopening directories into the WASI sandbox. Non-blocking: - Cap MemoryOutputPipe at 1 MiB per stream instead of usize::MAX. - Add --locked to build.rs fixture build to prevent network access. - Make build_sync_plugin_example() return Option and skip the e2e test gracefully when wasm32-wasip2 is not installed. Docs / stale text: - Replace "Extism sandbox" with "wasmtime WASI sandbox" in canister.rs doc comment; regenerate JSON schemas. - Update --proxy clap help text and changelog: it routes sync plugin calls to the target canister, not management canister calls; regenerate cli.md. - Update run_plugin signature in DESIGN.md (add proxy + identity_principal). - Clarify sync-plugin.wit: direct=false only proxies update calls; query calls always go directly to the target canister. Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 2 +- crates/icp-cli/src/commands/sync.rs | 2 +- crates/icp-cli/tests/sync_tests.rs | 24 ++++++++++++++---------- crates/icp-sync-plugin/DESIGN.md | 3 +++ crates/icp-sync-plugin/build.rs | 1 + crates/icp-sync-plugin/src/runtime.rs | 18 +++++++++++++++--- crates/icp-sync-plugin/sync-plugin.wit | 5 +++-- crates/icp/src/canister/sync/mod.rs | 2 +- crates/icp/src/canister/sync/plugin.rs | 21 +++++++++++++++++---- crates/icp/src/manifest/canister.rs | 2 +- docs/reference/cli.md | 2 +- docs/schemas/canister-yaml-schema.json | 2 +- docs/schemas/icp-yaml-schema.json | 2 +- 13 files changed, 60 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c74a7f07a..d7117caaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +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. +* feat: `icp sync` now accepts `--proxy` to route sync plugin calls to the target canister through a proxy canister. * 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 diff --git a/crates/icp-cli/src/commands/sync.rs b/crates/icp-cli/src/commands/sync.rs index aadeec0c8..5f3c9fdd5 100644 --- a/crates/icp-cli/src/commands/sync.rs +++ b/crates/icp-cli/src/commands/sync.rs @@ -16,7 +16,7 @@ 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. + /// Principal of a proxy canister to route sync plugin calls to the target canister through. #[arg(long)] pub(crate) proxy: Option, diff --git a/crates/icp-cli/tests/sync_tests.rs b/crates/icp-cli/tests/sync_tests.rs index 24bc185a7..55182fdb8 100644 --- a/crates/icp-cli/tests/sync_tests.rs +++ b/crates/icp-cli/tests/sync_tests.rs @@ -412,9 +412,10 @@ async fn sync_multiple_canisters() { } /// 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) { +/// `Some((canister_wasm_path, plugin_wasm_path))`, or `None` when the +/// `wasm32-wasip2` target is not installed (so callers can skip gracefully). +/// Cargo caches the build so subsequent test runs are fast when sources haven't changed. +fn build_sync_plugin_example() -> Option<(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. @@ -449,15 +450,16 @@ fn build_sync_plugin_example() -> (PathBuf, PathBuf) { .current_dir(&example_dir) .status() .expect("failed to spawn cargo build for plugin"); - assert!( - status.success(), - "cargo build --target wasm32-wasip2 failed" - ); - ( + if !status.success() { + eprintln!("Skipping plugin e2e test: wasm32-wasip2 target not installed"); + return None; + } + + Some(( example_dir.join("target/wasm32-unknown-unknown/release/canister.wasm"), example_dir.join("target/wasm32-wasip2/release/plugin.wasm"), - ) + )) } #[tokio::test] @@ -465,7 +467,9 @@ 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(); + let Some((canister_wasm, plugin_wasm)) = build_sync_plugin_example() else { + return; + }; // Create seed-data directory with fruit files let seed_data = project_dir.join("seed-data"); diff --git a/crates/icp-sync-plugin/DESIGN.md b/crates/icp-sync-plugin/DESIGN.md index 13f1cae4e..327bdd762 100644 --- a/crates/icp-sync-plugin/DESIGN.md +++ b/crates/icp-sync-plugin/DESIGN.md @@ -187,6 +187,8 @@ pub fn run_plugin( files: Vec<(String, String)>, target_canister_id: Principal, agent: Agent, + proxy: Option, + identity_principal: Principal, environment: String, stdio: Option>, ) -> Result<(), RunPluginError> @@ -207,6 +209,7 @@ wasmtime::component::bindgen!({ struct HostState { target_canister_id: Principal, agent: Arc, + proxy: Option, wasi_ctx: wasmtime_wasi::WasiCtx, wasi_table: wasmtime_wasi::ResourceTable, } diff --git a/crates/icp-sync-plugin/build.rs b/crates/icp-sync-plugin/build.rs index 5e19e195f..b6b5e8c7e 100644 --- a/crates/icp-sync-plugin/build.rs +++ b/crates/icp-sync-plugin/build.rs @@ -22,6 +22,7 @@ fn build_test_fixture() { "--target", "wasm32-wasip2", "--release", + "--locked", "--manifest-path", fixture_manifest.as_str(), "--target-dir", diff --git a/crates/icp-sync-plugin/src/runtime.rs b/crates/icp-sync-plugin/src/runtime.rs index 3dfc22870..25d06e914 100644 --- a/crates/icp-sync-plugin/src/runtime.rs +++ b/crates/icp-sync-plugin/src/runtime.rs @@ -1,7 +1,9 @@ // Host-side Component Model runtime for sync plugins. use std::sync::Arc; -use camino::Utf8PathBuf; +const MAX_PLUGIN_OUTPUT: usize = 1024 * 1024; // 1 MiB per stream + +use camino::{Utf8Component, Utf8PathBuf}; use candid::{Encode, Principal}; use ic_agent::Agent; use snafu::prelude::*; @@ -109,6 +111,11 @@ pub enum RunPluginError { path: Utf8PathBuf, }, + #[snafu(display( + "plugin dir '{dir}' is not a safe relative path (no absolute paths or '..' allowed)" + ))] + UnsafeDir { dir: String }, + #[snafu(display("failed to preopen directory '{dir}' for the plugin"))] PreopenDir { source: wasmtime::Error, @@ -161,6 +168,11 @@ pub fn run_plugin( // same relative path it used in the manifest. let mut wasi_builder = wasmtime_wasi::WasiCtxBuilder::new(); for dir in &dirs { + let p = Utf8PathBuf::from(dir); + ensure!( + !p.is_absolute() && !p.components().any(|c| c == Utf8Component::ParentDir), + UnsafeDirSnafu { dir } + ); let host_path = base_dir.join(dir); wasi_builder .preopened_dir( @@ -172,8 +184,8 @@ pub fn run_plugin( .context(PreopenDirSnafu { dir: host_path })?; } - let stdout_pipe = MemoryOutputPipe::new(usize::MAX); - let stderr_pipe = MemoryOutputPipe::new(usize::MAX); + let stdout_pipe = MemoryOutputPipe::new(MAX_PLUGIN_OUTPUT); + let stderr_pipe = MemoryOutputPipe::new(MAX_PLUGIN_OUTPUT); if stdio.is_some() { wasi_builder .stdout(stdout_pipe.clone()) diff --git a/crates/icp-sync-plugin/sync-plugin.wit b/crates/icp-sync-plugin/sync-plugin.wit index 1491b7f8c..46f2d71b1 100644 --- a/crates/icp-sync-plugin/sync-plugin.wit +++ b/crates/icp-sync-plugin/sync-plugin.wit @@ -45,8 +45,9 @@ interface types { 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. + /// (the default), update calls are routed through the proxy if one + /// is configured; query calls always go directly to the target + /// canister regardless of this flag. direct: bool, } } diff --git a/crates/icp/src/canister/sync/mod.rs b/crates/icp/src/canister/sync/mod.rs index 194ee6e94..10bb53fb6 100644 --- a/crates/icp/src/canister/sync/mod.rs +++ b/crates/icp/src/canister/sync/mod.rs @@ -65,7 +65,7 @@ impl Synchronize for Syncer { adapter, params, agent, - ¶ms.environment.clone(), + ¶ms.environment, params.proxy, stdio, pkg_cache, diff --git a/crates/icp/src/canister/sync/plugin.rs b/crates/icp/src/canister/sync/plugin.rs index 82c782ba6..fc2d791c3 100644 --- a/crates/icp/src/canister/sync/plugin.rs +++ b/crates/icp/src/canister/sync/plugin.rs @@ -16,6 +16,11 @@ use super::Params; #[derive(Debug, Snafu)] pub enum PluginError { + #[snafu(display( + "plugin file path '{name}' is not a safe relative path (no absolute paths or '..' allowed)" + ))] + UnsafeFilePath { name: String }, + #[snafu(display("failed to read plugin input file at '{path}'"))] ReadFile { source: crate::fs::IoError, @@ -95,6 +100,14 @@ pub(super) async fn sync( let mut files: Vec<(String, String)> = Vec::new(); for name in adapter.files.as_deref().unwrap_or(&[]) { + let p = Utf8PathBuf::from(name); + ensure!( + !p.is_absolute() + && !p + .components() + .any(|c| c == camino::Utf8Component::ParentDir), + UnsafeFilePathSnafu { name } + ); let abs = params.path.join(name); let content = read_to_string(abs.as_ref()).context(ReadFileSnafu { path: abs })?; files.push((name.clone(), content)); @@ -109,7 +122,7 @@ pub(super) async fn sync( let environment_owned = environment.to_owned(); let stdio_clone = stdio.clone(); - tokio::task::block_in_place(|| { + let result = tokio::task::block_in_place(|| { run_plugin( wasm_path.clone(), base_dir, @@ -123,12 +136,12 @@ pub(super) async fn sync( stdio_clone, ) }) - .context(RunSnafu)?; + .context(RunSnafu); - // Clean up temp file if we downloaded from a remote URL with no sha256. + // Clean up temp file regardless of plugin success/failure. if is_temp { let _ = std::fs::remove_file(wasm_path.as_std_path()); } - Ok(()) + result } diff --git a/crates/icp/src/manifest/canister.rs b/crates/icp/src/manifest/canister.rs index 48f786e13..4ee588b74 100644 --- a/crates/icp/src/manifest/canister.rs +++ b/crates/icp/src/manifest/canister.rs @@ -318,7 +318,7 @@ pub enum SyncStep { 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 + /// a wasmtime WASI sandbox. The plugin can call canister methods on exactly /// the canister being synced and read files from the declared `dirs`. Plugin(adapter::plugin::Adapter), } diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 63b9066d7..171122281 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -1545,7 +1545,7 @@ Synchronize canisters ###### **Options:** -* `--proxy ` — Principal of a proxy canister to route management canister calls through +* `--proxy ` — Principal of a proxy canister to route sync plugin calls to the target canister 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 diff --git a/docs/schemas/canister-yaml-schema.json b/docs/schemas/canister-yaml-schema.json index 8b15fc5a5..f2bd2aa7f 100644 --- a/docs/schemas/canister-yaml-schema.json +++ b/docs/schemas/canister-yaml-schema.json @@ -498,7 +498,7 @@ }, { "$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`.", + "description": "Represents a sync step executed by a WebAssembly plugin running inside\na wasmtime WASI sandbox. The plugin can call canister methods on exactly\nthe canister being synced and read files from the declared `dirs`.", "properties": { "type": { "const": "plugin", diff --git a/docs/schemas/icp-yaml-schema.json b/docs/schemas/icp-yaml-schema.json index afcbcd76d..2224234d1 100644 --- a/docs/schemas/icp-yaml-schema.json +++ b/docs/schemas/icp-yaml-schema.json @@ -994,7 +994,7 @@ }, { "$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`.", + "description": "Represents a sync step executed by a WebAssembly plugin running inside\na wasmtime WASI sandbox. The plugin can call canister methods on exactly\nthe canister being synced and read files from the declared `dirs`.", "properties": { "type": { "const": "plugin", From 406eb313649a57bc3f898efa2078be571b935252 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Tue, 28 Apr 2026 09:51:05 -0400 Subject: [PATCH 25/39] refactor: remove wasm32-wasip2 skip guards now that target is in toolchain wasm32-wasip2 is declared in rust-toolchain.toml so it is always available locally and in CI. Remove all the optional-skip paths: - build.rs: assert fixture build succeeds instead of silently skipping - runtime.rs unit tests: use env! instead of option_env! guards - sync_tests.rs: revert build_sync_plugin_example() to return (PathBuf, PathBuf) directly and assert the build succeeds Co-Authored-By: Claude Sonnet 4.6 --- crates/icp-cli/tests/sync_tests.rs | 23 ++++++++++------------- crates/icp-sync-plugin/build.rs | 20 ++++++++------------ crates/icp-sync-plugin/src/runtime.rs | 22 +++++----------------- 3 files changed, 23 insertions(+), 42 deletions(-) diff --git a/crates/icp-cli/tests/sync_tests.rs b/crates/icp-cli/tests/sync_tests.rs index 55182fdb8..59ab52038 100644 --- a/crates/icp-cli/tests/sync_tests.rs +++ b/crates/icp-cli/tests/sync_tests.rs @@ -412,10 +412,9 @@ async fn sync_multiple_canisters() { } /// Compiles the canister and plugin from `examples/icp-sync-plugin/` and returns -/// `Some((canister_wasm_path, plugin_wasm_path))`, or `None` when the -/// `wasm32-wasip2` target is not installed (so callers can skip gracefully). -/// Cargo caches the build so subsequent test runs are fast when sources haven't changed. -fn build_sync_plugin_example() -> Option<(PathBuf, PathBuf)> { +/// (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. @@ -451,15 +450,15 @@ fn build_sync_plugin_example() -> Option<(PathBuf, PathBuf)> { .status() .expect("failed to spawn cargo build for plugin"); - if !status.success() { - eprintln!("Skipping plugin e2e test: wasm32-wasip2 target not installed"); - return None; - } + assert!( + status.success(), + "cargo build --target wasm32-wasip2 failed" + ); - Some(( + ( example_dir.join("target/wasm32-unknown-unknown/release/canister.wasm"), example_dir.join("target/wasm32-wasip2/release/plugin.wasm"), - )) + ) } #[tokio::test] @@ -467,9 +466,7 @@ async fn sync_plugin_registers_seed_data() { let ctx = TestContext::new(); let project_dir = ctx.create_project_dir("icp"); - let Some((canister_wasm, plugin_wasm)) = build_sync_plugin_example() else { - return; - }; + let (canister_wasm, plugin_wasm) = build_sync_plugin_example(); // Create seed-data directory with fruit files let seed_data = project_dir.join("seed-data"); diff --git a/crates/icp-sync-plugin/build.rs b/crates/icp-sync-plugin/build.rs index b6b5e8c7e..f88d53749 100644 --- a/crates/icp-sync-plugin/build.rs +++ b/crates/icp-sync-plugin/build.rs @@ -28,16 +28,12 @@ fn build_test_fixture() { "--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"). - } - } + .status() + .expect("failed to spawn cargo build for test fixture"); + assert!( + status.success(), + "cargo build --target wasm32-wasip2 failed for test fixture" + ); + let wasm = fixture_target_dir.join("wasm32-wasip2/release/test_plugin.wasm"); + println!("cargo:rustc-env=TEST_PLUGIN_WASM={wasm}"); } diff --git a/crates/icp-sync-plugin/src/runtime.rs b/crates/icp-sync-plugin/src/runtime.rs index 25d06e914..42ce705d7 100644 --- a/crates/icp-sync-plugin/src/runtime.rs +++ b/crates/icp-sync-plugin/src/runtime.rs @@ -295,15 +295,12 @@ mod tests { } // ------------------------------------------------------------------------- - // Fixture-dependent tests — skipped when TEST_PLUGIN_WASM is not set + // Fixture-dependent tests // ------------------------------------------------------------------------- #[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 wasm_path = env!("TEST_PLUGIN_WASM"); let result = run_plugin( wasm_path.into(), ".".into(), @@ -321,10 +318,7 @@ mod tests { #[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 wasm_path = env!("TEST_PLUGIN_WASM"); let result = run_plugin( wasm_path.into(), ".".into(), @@ -342,10 +336,7 @@ mod tests { #[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 wasm_path = env!("TEST_PLUGIN_WASM"); let result = run_plugin( wasm_path.into(), ".".into(), @@ -366,10 +357,7 @@ mod tests { #[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 wasm_path = env!("TEST_PLUGIN_WASM"); let (tx, mut rx) = tokio::sync::mpsc::channel::(16); let result = run_plugin( wasm_path.into(), From abbc59dbefe480c0c9a2c116d41b6bcf594e5c4c Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Tue, 28 Apr 2026 10:05:29 -0400 Subject: [PATCH 26/39] chore: clippy fix --- crates/icp-sync-plugin/tests/fixtures/test-plugin/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) 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 index f1f5b0067..c92b8adb8 100644 --- a/crates/icp-sync-plugin/tests/fixtures/test-plugin/src/lib.rs +++ b/crates/icp-sync-plugin/tests/fixtures/test-plugin/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(clippy::too_many_arguments)] + wit_bindgen::generate!({ world: "sync-plugin", path: "../../../sync-plugin.wit", From 5fb311e27b58ce2bc995d2bbbb1c3048a5d4c66b Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Tue, 28 Apr 2026 10:55:32 -0400 Subject: [PATCH 27/39] fix: require sha256 for remote plugin sources Enforce at parse time that a plugin step using `url:` must supply `sha256:`. Previously the doc comment said it was required but nothing prevented omitting it, which silently allowed unverified downloads. Co-Authored-By: Claude Sonnet 4.6 --- crates/icp/src/manifest/adapter/plugin.rs | 58 +++++++++++++++++++++-- crates/icp/src/manifest/canister.rs | 16 +++++++ docs/schemas/canister-yaml-schema.json | 4 +- docs/schemas/icp-yaml-schema.json | 4 +- 4 files changed, 73 insertions(+), 9 deletions(-) diff --git a/crates/icp/src/manifest/adapter/plugin.rs b/crates/icp/src/manifest/adapter/plugin.rs index b0b87f291..915b03c1d 100644 --- a/crates/icp/src/manifest/adapter/plugin.rs +++ b/crates/icp/src/manifest/adapter/plugin.rs @@ -1,5 +1,5 @@ use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use super::prebuilt::SourceField; @@ -11,23 +11,30 @@ use super::prebuilt::SourceField; /// the contents of any files listed in `files` (read by the host and passed /// inline to the plugin). /// -/// Example: +/// Example (local path): /// ```yaml /// - type: plugin /// path: ./plugins/populate-data.wasm -/// sha256: e3b0c44298fc1c149afb... # optional but recommended +/// sha256: e3b0c44298fc1c149afb... # optional for path /// 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)] +/// +/// Example (remote URL — `sha256` is required): +/// ```yaml +/// - type: plugin +/// url: https://example.com/plugins/populate-data.wasm +/// sha256: e3b0c44298fc1c149afb... # required for url +/// ``` +#[derive(Clone, Debug, 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`. + /// Optional for `path`; required for `url`. pub sha256: Option, /// Directories (relative to canister directory) the plugin may read from. @@ -40,6 +47,32 @@ pub struct Adapter { pub files: Option>, } +impl<'de> Deserialize<'de> for Adapter { + fn deserialize>(d: D) -> Result { + #[derive(Deserialize)] + struct AdapterHelper { + #[serde(flatten)] + source: SourceField, + sha256: Option, + dirs: Option>, + files: Option>, + } + + let h = AdapterHelper::deserialize(d)?; + if matches!(h.source, SourceField::Remote(_)) && h.sha256.is_none() { + return Err(serde::de::Error::custom( + "plugin with `url` requires `sha256` for integrity verification", + )); + } + Ok(Self { + source: h.source, + sha256: h.sha256, + dirs: h.dirs, + files: h.files, + }) + } +} + #[cfg(test)] mod tests { use super::*; @@ -91,6 +124,21 @@ mod tests { ); } + #[test] + fn remote_url_without_sha256_is_rejected() { + let err = serde_yaml::from_str::( + r#" + url: https://example.com/plugins/migrate-v2.wasm + "#, + ) + .expect_err("expected error for remote url without sha256"); + assert!( + err.to_string() + .contains("plugin with `url` requires `sha256`"), + "unexpected error: {err}" + ); + } + #[test] fn remote_url_with_sha256() { assert_eq!( diff --git a/crates/icp/src/manifest/canister.rs b/crates/icp/src/manifest/canister.rs index 4ee588b74..ebafac6a2 100644 --- a/crates/icp/src/manifest/canister.rs +++ b/crates/icp/src/manifest/canister.rs @@ -820,6 +820,22 @@ mod tests { ); } + #[test] + #[should_panic(expected = "plugin with `url` requires `sha256`")] + fn sync_steps_plugin_remote_without_sha256_is_rejected() { + 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 + "#}); + } + #[test] fn sync_steps() { assert_eq!( diff --git a/docs/schemas/canister-yaml-schema.json b/docs/schemas/canister-yaml-schema.json index f2bd2aa7f..0dcda0934 100644 --- a/docs/schemas/canister-yaml-schema.json +++ b/docs/schemas/canister-yaml-schema.json @@ -100,7 +100,7 @@ "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 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```", + "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 (local path):\n```yaml\n- type: plugin\n path: ./plugins/populate-data.wasm\n sha256: e3b0c44298fc1c149afb... # optional for path\n dirs: # directories preopened read-only\n - assets/seed-data\n files: # files read by the host and passed inline\n - config.txt\n```\n\nExample (remote URL — `sha256` is required):\n```yaml\n- type: plugin\n url: https://example.com/plugins/populate-data.wasm\n sha256: e3b0c44298fc1c149afb... # required for url\n```", "properties": { "dirs": { "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.", @@ -123,7 +123,7 @@ ] }, "sha256": { - "description": "Optional sha256 checksum of the wasm file.\nRequired when `url` is used; optional (but recommended) for `path`.", + "description": "Optional sha256 checksum of the wasm file.\nOptional for `path`; required for `url`.", "type": [ "string", "null" diff --git a/docs/schemas/icp-yaml-schema.json b/docs/schemas/icp-yaml-schema.json index 2224234d1..0f0325c76 100644 --- a/docs/schemas/icp-yaml-schema.json +++ b/docs/schemas/icp-yaml-schema.json @@ -100,7 +100,7 @@ "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 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```", + "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 (local path):\n```yaml\n- type: plugin\n path: ./plugins/populate-data.wasm\n sha256: e3b0c44298fc1c149afb... # optional for path\n dirs: # directories preopened read-only\n - assets/seed-data\n files: # files read by the host and passed inline\n - config.txt\n```\n\nExample (remote URL — `sha256` is required):\n```yaml\n- type: plugin\n url: https://example.com/plugins/populate-data.wasm\n sha256: e3b0c44298fc1c149afb... # required for url\n```", "properties": { "dirs": { "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.", @@ -123,7 +123,7 @@ ] }, "sha256": { - "description": "Optional sha256 checksum of the wasm file.\nRequired when `url` is used; optional (but recommended) for `path`.", + "description": "Optional sha256 checksum of the wasm file.\nOptional for `path`; required for `url`.", "type": [ "string", "null" From cdedd3d097160584178cf1e8c81011e7391d8253 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Tue, 28 Apr 2026 10:57:30 -0400 Subject: [PATCH 28/39] fix: remove temp-file path for remote plugins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sha256 enforcement from the previous commit means remote plugin sources always have a checksum, so they always resolve through the content-addressed cache. The temp-file branch (which used wasm content bytes as a name, creating a race under concurrent sync) is now dead code — remove it along with the WriteTempWasm error variant and the post-run cleanup. Co-Authored-By: Claude Sonnet 4.6 --- crates/icp/src/canister/sync/plugin.rs | 76 +++++++++----------------- 1 file changed, 25 insertions(+), 51 deletions(-) diff --git a/crates/icp/src/canister/sync/plugin.rs b/crates/icp/src/canister/sync/plugin.rs index fc2d791c3..8bf7c413c 100644 --- a/crates/icp/src/canister/sync/plugin.rs +++ b/crates/icp/src/canister/sync/plugin.rs @@ -7,7 +7,7 @@ use tokio::sync::mpsc::Sender; use crate::{ canister::wasm, - fs::{read_to_string, write}, + fs::read_to_string, manifest::adapter::{plugin::Adapter, prebuilt::SourceField}, package::PackageCache, }; @@ -27,9 +27,6 @@ pub enum PluginError { path: Utf8PathBuf, }, - #[snafu(display("failed to write downloaded plugin wasm to temp file"))] - WriteTempWasm { source: crate::fs::IoError }, - #[snafu(transparent)] Wasm { source: wasm::WasmError }, @@ -54,43 +51,27 @@ pub(super) async fn sync( ) -> Result<(), PluginError> { // 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(LockCacheSnafu)?; - (path, false) - } - 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) - } - }, + // - Remote: resolve via cache (sha256 is required for remote, enforced at parse time), + // so the stable cache path is always available — no temp file needed. + let wasm_path = match &adapter.source { + SourceField::Local(s) => params.path.join(&s.path), + SourceField::Remote(_) => { + let sha = adapter + .sha256 + .as_deref() + .expect("remote plugin source requires sha256 — enforced at manifest parse time"); + wasm::resolve( + &adapter.source, + ¶ms.path, + Some(sha), + stdio.as_ref(), + pkg_cache, + ) + .await?; + wasm::cached_path(pkg_cache, sha) + .await + .context(LockCacheSnafu)? + } }; // 2. Collect inputs: `dirs` stays as manifest strings (runtime preopens them), @@ -122,9 +103,9 @@ pub(super) async fn sync( let environment_owned = environment.to_owned(); let stdio_clone = stdio.clone(); - let result = tokio::task::block_in_place(|| { + tokio::task::block_in_place(|| { run_plugin( - wasm_path.clone(), + wasm_path, base_dir, dirs, files, @@ -136,12 +117,5 @@ pub(super) async fn sync( stdio_clone, ) }) - .context(RunSnafu); - - // Clean up temp file regardless of plugin success/failure. - if is_temp { - let _ = std::fs::remove_file(wasm_path.as_std_path()); - } - - result + .context(RunSnafu) } From 51d267c7fa9f481590347ba8fdf8925818ccee0c Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Tue, 28 Apr 2026 10:59:44 -0400 Subject: [PATCH 29/39] test: run stdio test under a multi-thread Tokio runtime plugin_stdout_forwarded_through_stdio_channel calls run_plugin, which uses block_in_place internally. Wrapping the call in block_in_place and using a multi-thread runtime matches the production callsite and prevents a silent panic if canister_call (which needs Handle::current()) is ever exercised in this test. Co-Authored-By: Claude Sonnet 4.6 --- crates/icp-sync-plugin/src/runtime.rs | 30 ++++++++++++++------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/crates/icp-sync-plugin/src/runtime.rs b/crates/icp-sync-plugin/src/runtime.rs index 42ce705d7..c1bf50677 100644 --- a/crates/icp-sync-plugin/src/runtime.rs +++ b/crates/icp-sync-plugin/src/runtime.rs @@ -355,22 +355,24 @@ mod tests { )); } - #[test] - fn plugin_stdout_forwarded_through_stdio_channel() { + #[tokio::test(flavor = "multi_thread")] + async fn plugin_stdout_forwarded_through_stdio_channel() { let wasm_path = env!("TEST_PLUGIN_WASM"); 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), - ); + let result = tokio::task::block_in_place(|| { + 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}"); From 817521d1c7cf5812480e6d2402b3d2642831a370 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Tue, 28 Apr 2026 11:01:17 -0400 Subject: [PATCH 30/39] fix: make progress log sends best-effort A closed or full stdio channel should not abort a wasm fetch or build. Drop the Log error variants from WasmError and PrebuiltError and replace .context(LogSnafu)? with let _ = tx.send(...).await, matching the pattern already used for stdout forwarding in the plugin runtime. Co-Authored-By: Claude Sonnet 4.6 --- crates/icp/src/canister/build/prebuilt.rs | 9 +-------- crates/icp/src/canister/wasm.rs | 21 ++++----------------- 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/crates/icp/src/canister/build/prebuilt.rs b/crates/icp/src/canister/build/prebuilt.rs index b7bc2431d..d1bcb2809 100644 --- a/crates/icp/src/canister/build/prebuilt.rs +++ b/crates/icp/src/canister/build/prebuilt.rs @@ -9,11 +9,6 @@ use super::Params; #[derive(Debug, Snafu)] pub enum PrebuiltError { - #[snafu(display("failed to send log message"))] - Log { - source: tokio::sync::mpsc::error::SendError, - }, - #[snafu(transparent)] Wasm { source: wasm::WasmError }, @@ -37,9 +32,7 @@ pub(super) async fn build( .await?; if let Some(tx) = &stdio { - tx.send(format!("Writing WASM file: {}", params.output)) - .await - .context(LogSnafu)?; + let _ = tx.send(format!("Writing WASM file: {}", params.output)).await; } write(¶ms.output, &wasm_bytes).context(WriteFileSnafu)?; diff --git a/crates/icp/src/canister/wasm.rs b/crates/icp/src/canister/wasm.rs index 8237f6729..cade65155 100644 --- a/crates/icp/src/canister/wasm.rs +++ b/crates/icp/src/canister/wasm.rs @@ -34,11 +34,6 @@ pub enum WasmError { #[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 }, @@ -61,9 +56,7 @@ async fn fetch( SourceField::Local(s) => { let path = base_dir.join(&s.path); if let Some(tx) = stdio { - tx.send(format!("Reading wasm: {}", s.path)) - .await - .context(LogSnafu)?; + let _ = tx.send(format!("Reading wasm: {}", s.path)).await; } read(&path).context(ReadLocalSnafu { path: s.path.clone(), @@ -72,9 +65,7 @@ async fn fetch( 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 _ = tx.send(format!("Fetching wasm: {url}")).await; } let resp = Client::new() .execute(Request::new(Method::GET, url)) @@ -90,9 +81,7 @@ async fn fetch( if let Some(expected) = sha256 { if let Some(tx) = stdio { - tx.send("Verifying checksum".to_string()) - .await - .context(LogSnafu)?; + let _ = tx.send("Verifying checksum".to_string()).await; } let actual = hex::encode(Sha256::digest(&bytes)); ensure!( @@ -124,9 +113,7 @@ pub async fn resolve( .context(LockCacheSnafu)?; if let Some(cached) = maybe_cached? { if let Some(tx) = stdio { - tx.send("Using cached file".to_string()) - .await - .context(LogSnafu)?; + let _ = tx.send("Using cached file".to_string()).await; } return Ok(cached); } From f4980d9f988ecd1f75643b101700bf610ed862e5 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Tue, 28 Apr 2026 11:05:16 -0400 Subject: [PATCH 31/39] docs: document wasm cache path migration in CHANGELOG Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7117caaf..d3b0c7ea3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased +* fix: The local wasm cache has moved from `.icp/cache/canisters/` to `.icp/cache/wasms/`. Existing cached files will be re-downloaded automatically on the next run. * 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 sync plugin calls to the target canister through a proxy canister. * 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". From e47a6335fe8dd6e20f4f7bc08821206f5369362d Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Tue, 28 Apr 2026 11:13:40 -0400 Subject: [PATCH 32/39] fix: make call-type non-optional in sync-plugin WIT Removes the implicit update default from the host and requires plugins to always specify call-type explicitly, making intent clear at the call site. Co-Authored-By: Claude Sonnet 4.6 --- crates/icp-sync-plugin/src/runtime.rs | 3 +-- crates/icp-sync-plugin/sync-plugin.wit | 4 ++-- examples/icp-sync-plugin/plugin/src/lib.rs | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/icp-sync-plugin/src/runtime.rs b/crates/icp-sync-plugin/src/runtime.rs index c1bf50677..8726328f9 100644 --- a/crates/icp-sync-plugin/src/runtime.rs +++ b/crates/icp-sync-plugin/src/runtime.rs @@ -50,13 +50,12 @@ impl SyncPluginImports for HostState { 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 { + match req.call_type { CallType::Update => { if let Some(proxy_cid) = proxy { let proxy_args = ProxyArgs { diff --git a/crates/icp-sync-plugin/sync-plugin.wit b/crates/icp-sync-plugin/sync-plugin.wit index 46f2d71b1..feaf064ee 100644 --- a/crates/icp-sync-plugin/sync-plugin.wit +++ b/crates/icp-sync-plugin/sync-plugin.wit @@ -41,8 +41,8 @@ interface types { /// 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, + /// Whether to perform an `update` or `query` call. + call-type: call-type, /// When true, the call bypasses any proxy canister configured via /// `--proxy`, going directly to the target canister. When false /// (the default), update calls are routed through the proxy if one diff --git a/examples/icp-sync-plugin/plugin/src/lib.rs b/examples/icp-sync-plugin/plugin/src/lib.rs index e4a3fdc44..e607e3b93 100644 --- a/examples/icp-sync-plugin/plugin/src/lib.rs +++ b/examples/icp-sync-plugin/plugin/src/lib.rs @@ -26,7 +26,7 @@ impl Guest for Plugin { canister_call(&CanisterCallRequest { method: "set_uploader".to_string(), arg, - call_type: Some(icp::sync_plugin::types::CallType::Update), + call_type: icp::sync_plugin::types::CallType::Update, direct: false, })?; println!("set_uploader ({}): ok", input.identity_principal); @@ -67,7 +67,7 @@ fn register_dir(dir: &Path) -> Result { canister_call(&CanisterCallRequest { method: "register".to_string(), arg, - call_type: Some(icp::sync_plugin::types::CallType::Update), + call_type: icp::sync_plugin::types::CallType::Update, direct: true, })?; println!("{path_str}: ok"); From 756e0aa303c8da3ab4d98679bb97b365d2393b5d Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Tue, 28 Apr 2026 11:21:22 -0400 Subject: [PATCH 33/39] feat: expose cycles field in canister-call-request for proxy calls Add `cycles: u64` to the `canister-call-request` WIT record so plugins can attach cycles to proxied update calls, replacing the hardcoded zero. The field is documented as a no-op for direct calls and query calls. Update the example plugin and DESIGN.md snippet accordingly. Co-Authored-By: Claude Sonnet 4.6 --- crates/icp-sync-plugin/DESIGN.md | 4 +++- crates/icp-sync-plugin/src/runtime.rs | 2 +- crates/icp-sync-plugin/sync-plugin.wit | 5 +++++ examples/icp-sync-plugin/plugin/src/lib.rs | 2 ++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/crates/icp-sync-plugin/DESIGN.md b/crates/icp-sync-plugin/DESIGN.md index 327bdd762..4142cebe6 100644 --- a/crates/icp-sync-plugin/DESIGN.md +++ b/crates/icp-sync-plugin/DESIGN.md @@ -278,7 +278,9 @@ impl Guest for Plugin { canister_call(&CanisterCallRequest { method: "set_config".to_string(), arg, - call_type: Some(icp::sync_plugin::types::CallType::Update), + call_type: icp::sync_plugin::types::CallType::Update, + direct: false, + cycles: 0, })?; } diff --git a/crates/icp-sync-plugin/src/runtime.rs b/crates/icp-sync-plugin/src/runtime.rs index 8726328f9..dbce47293 100644 --- a/crates/icp-sync-plugin/src/runtime.rs +++ b/crates/icp-sync-plugin/src/runtime.rs @@ -62,7 +62,7 @@ impl SyncPluginImports for HostState { canister_id: cid, method: method.clone(), args: arg_bytes, - cycles: candid::Nat::from(0u64), + cycles: candid::Nat::from(req.cycles), }; let encoded = Encode!(&proxy_args) .map_err(|e| format!("proxy encode failed: {e}"))?; diff --git a/crates/icp-sync-plugin/sync-plugin.wit b/crates/icp-sync-plugin/sync-plugin.wit index feaf064ee..a64f2cab5 100644 --- a/crates/icp-sync-plugin/sync-plugin.wit +++ b/crates/icp-sync-plugin/sync-plugin.wit @@ -49,6 +49,11 @@ interface types { /// is configured; query calls always go directly to the target /// canister regardless of this flag. direct: bool, + /// Cycles to attach to a proxied update call. Only meaningful when + /// `direct` is `false`, a proxy canister is configured, and + /// `call-type` is `update`; silently ignored for direct calls and + /// for query calls. + cycles: u64, } } diff --git a/examples/icp-sync-plugin/plugin/src/lib.rs b/examples/icp-sync-plugin/plugin/src/lib.rs index e607e3b93..23cd5d455 100644 --- a/examples/icp-sync-plugin/plugin/src/lib.rs +++ b/examples/icp-sync-plugin/plugin/src/lib.rs @@ -28,6 +28,7 @@ impl Guest for Plugin { arg, call_type: icp::sync_plugin::types::CallType::Update, direct: false, + cycles: 0, })?; println!("set_uploader ({}): ok", input.identity_principal); @@ -69,6 +70,7 @@ fn register_dir(dir: &Path) -> Result { arg, call_type: icp::sync_plugin::types::CallType::Update, direct: true, + cycles: 0, })?; println!("{path_str}: ok"); count += 1; From beb8cc28d4697aaacc1ae11d1ee01a3f0da87221 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Tue, 28 Apr 2026 11:28:31 -0400 Subject: [PATCH 34/39] test: add E2E test for sync plugin proxy routing path Adds sync_plugin_routes_through_proxy, which deploys through the local proxy canister (making it a controller) and verifies that the plugin's set_uploader call is correctly routed through the proxy while register calls go directly. Also removes the redundant explicit sync call from sync_plugin_registers_seed_data since deploy already runs sync steps. Co-Authored-By: Claude Sonnet 4.6 --- crates/icp-cli/tests/sync_tests.rs | 78 ++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/crates/icp-cli/tests/sync_tests.rs b/crates/icp-cli/tests/sync_tests.rs index 59ab52038..69beff427 100644 --- a/crates/icp-cli/tests/sync_tests.rs +++ b/crates/icp-cli/tests/sync_tests.rs @@ -500,7 +500,9 @@ async fn sync_plugin_registers_seed_data() { 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) + // Mint cycles and deploy. deploy also runs the sync step: the 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. clients::icp(&ctx, &project_dir, Some("random-environment".to_string())) .mint_cycles(10 * TRILLION); @@ -516,11 +518,79 @@ async fn sync_plugin_registers_seed_data() { .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. + // Query the canister to verify all three fruits were registered ctx.icp() .current_dir(&project_dir) - .args(["sync", "my-canister", "--environment", "random-environment"]) + .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_plugin_routes_through_proxy() { + 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"); + + 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 (the proxy canister is automatically deployed) + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + let proxy_cid = ctx.get_proxy_cid(&project_dir, "random-network"); + + // Deploy through proxy so the proxy canister becomes a controller of my-canister. + // deploy also runs the sync step: the plugin routes set_uploader through the proxy + // (direct: false, proxy is controller), then calls register directly with the user identity. + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "--proxy", + &proxy_cid, + "--environment", + "random-environment", + ]) .assert() .success(); From 4f7fe8b3b2813154e49091c8b6614af3282df6ea Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Tue, 28 Apr 2026 11:32:31 -0400 Subject: [PATCH 35/39] fix: only cache remote wasm when sha256 is provided Unverified remote wasm bytes were previously written to the cache unconditionally, allowing integrity-unverified content to persist on disk and the cache key (a recomputed digest) to never match a future lookup (which requires a known sha256). Now the cache write is gated on sha256 being provided, consistent with the cache read path. Co-Authored-By: Claude Sonnet 4.6 --- crates/icp/src/canister/wasm.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/icp/src/canister/wasm.rs b/crates/icp/src/canister/wasm.rs index cade65155..0831ec059 100644 --- a/crates/icp/src/canister/wasm.rs +++ b/crates/icp/src/canister/wasm.rs @@ -121,10 +121,9 @@ pub async fn resolve( let bytes = fetch(source, base_dir, sha256, stdio).await?; - if matches!(source, SourceField::Remote(_)) { - let cksum = hex::encode(Sha256::digest(&bytes)); + if let (SourceField::Remote(_), Some(expected)) = (source, sha256) { pkg_cache - .with_write(async |w| cache_wasm(w, &cksum, &bytes).context(CacheFileSnafu)) + .with_write(async |w| cache_wasm(w, expected, &bytes).context(CacheFileSnafu)) .await .context(LockCacheSnafu)??; } From 6fda16f00ef3e653577b25bf2ed26e575352556a Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Tue, 28 Apr 2026 11:33:54 -0400 Subject: [PATCH 36/39] chore: fmt --- crates/icp/src/canister/build/prebuilt.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/icp/src/canister/build/prebuilt.rs b/crates/icp/src/canister/build/prebuilt.rs index d1bcb2809..5285a3598 100644 --- a/crates/icp/src/canister/build/prebuilt.rs +++ b/crates/icp/src/canister/build/prebuilt.rs @@ -32,7 +32,9 @@ pub(super) async fn build( .await?; if let Some(tx) = &stdio { - let _ = tx.send(format!("Writing WASM file: {}", params.output)).await; + let _ = tx + .send(format!("Writing WASM file: {}", params.output)) + .await; } write(¶ms.output, &wasm_bytes).context(WriteFileSnafu)?; From 58d83a114f541c8c2e8edf9737694139662c4ecb Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 30 Apr 2026 15:16:51 -0400 Subject: [PATCH 37/39] fix: add resource limits to sync plugin runtime Stack depth (512 KiB), compute time (60s via epoch interruption), and a memory-bounds comment are added per PR review. Canister call latency is excluded from the compute budget by crediting elapsed ticks back after each host call returns. Limits are documented in DESIGN.md. Co-Authored-By: Claude Sonnet 4.6 --- crates/icp-sync-plugin/DESIGN.md | 18 +++++++ crates/icp-sync-plugin/src/runtime.rs | 68 ++++++++++++++++++++++++++- 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/crates/icp-sync-plugin/DESIGN.md b/crates/icp-sync-plugin/DESIGN.md index 4142cebe6..deb2b7f18 100644 --- a/crates/icp-sync-plugin/DESIGN.md +++ b/crates/icp-sync-plugin/DESIGN.md @@ -146,6 +146,20 @@ The effective capability surface is: returns, stdout is forwarded to the CLI progress output first, then stderr. Invalid UTF-8 is replaced with U+FFFD. +### Resource limits + +| Resource | Limit | +|----------|-------| +| Wasm call-stack depth | 512 KiB | +| Pure compute time | 60 seconds | +| Linear memory | wasm32 address space (≤ 4 GiB) | +| stdout / stderr per stream | 1 MiB | + +**Compute time** counts only wasm instruction execution. Time spent waiting for +a `canister-call` to return over the network is excluded — the host grants that +time back to the budget when the call completes. A plugin that exceeds 60 seconds +of actual computation will be interrupted with an error. + ### What this means for plugin authors You can: @@ -153,12 +167,16 @@ You can: - 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. +- Make as many canister calls as needed; their network latency is not charged + against the compute time limit. 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. +- Use more than 512 KiB of wasm call stack or run for more than 60 seconds of + pure computation. --- diff --git a/crates/icp-sync-plugin/src/runtime.rs b/crates/icp-sync-plugin/src/runtime.rs index dbce47293..9c42e893c 100644 --- a/crates/icp-sync-plugin/src/runtime.rs +++ b/crates/icp-sync-plugin/src/runtime.rs @@ -1,7 +1,13 @@ // Host-side Component Model runtime for sync plugins. use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::time::{Duration, Instant}; const MAX_PLUGIN_OUTPUT: usize = 1024 * 1024; // 1 MiB per stream +// Maximum wasm call-stack depth (in bytes). +const MAX_WASM_STACK: usize = 512 * 1024; +// How many seconds of pure wasm compute a plugin may use (host-call latency is excluded). +const PLUGIN_COMPUTE_LIMIT_SECS: u64 = 60; use camino::{Utf8Component, Utf8PathBuf}; use candid::{Encode, Principal}; @@ -28,6 +34,11 @@ struct HostState { // filesystem locations the plugin can access. wasi_ctx: wasmtime_wasi::WasiCtx, wasi_table: wasmtime_wasi::ResourceTable, + // Accumulated epoch ticks to grant back after a host call returns, so that + // canister call latency doesn't consume the wasm compute budget. AtomicU64 + // (rather than Mutex) is required because the epoch_deadline_callback + // closure must be Send + 'static, which Arc> does not satisfy. + epoch_extension: Arc, } impl wasmtime_wasi::WasiView for HostState { @@ -54,7 +65,8 @@ impl SyncPluginImports for HostState { // 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 { + let start = Instant::now(); + let result = tokio::runtime::Handle::current().block_on(async move { match req.call_type { CallType::Update => { if let Some(proxy_cid) = proxy { @@ -92,10 +104,23 @@ impl SyncPluginImports for HostState { .await .map_err(|e| format!("canister call failed: {e}")), } - }) + }); + // Return the time spent in the host call to the compute budget so + // canister network latency doesn't count against the plugin's limit. + let elapsed_ticks = start.elapsed().as_secs() + 1; + self.epoch_extension + .fetch_add(elapsed_ticks, Ordering::Relaxed); + result } } +// Used as the error payload inside the epoch_deadline_callback closure, which +// must return wasmtime::Error (= anyhow::Error). Snafu derives std::error::Error +// so .into() converts it via anyhow's blanket From. +#[derive(Debug, Snafu)] +#[snafu(display("plugin exceeded the {PLUGIN_COMPUTE_LIMIT_SECS}s compute time limit"))] +struct ComputeTimeLimitExceeded; + #[derive(Debug, Snafu)] pub enum RunPluginError { #[snafu(display("failed to create wasmtime engine for plugin at {path}"))] @@ -154,10 +179,38 @@ pub fn run_plugin( let mut config = Config::new(); config.wasm_component_model(true); + config.max_wasm_stack(MAX_WASM_STACK); + // Linear memory is implicitly bounded by the wasm32 address space (4 GiB). + // If wasm64 support is ever added, set Config::memory_maximum() explicitly. + config.epoch_interruption(true); let engine = Engine::new(&config).context(CreateEngineSnafu { path: wasm_path.clone(), })?; + // Increment the engine epoch every second from a background thread. + // The store deadline is set below; the ticker stops when this guard is dropped. + // AtomicBool is sufficient here — it's a one-way stop signal between two threads. + let ticker_stop = Arc::new(AtomicBool::new(false)); + let _ticker_guard = { + let engine_ticker = engine.clone(); + let stop = ticker_stop.clone(); + let handle = std::thread::spawn(move || { + while !stop.load(Ordering::Relaxed) { + std::thread::sleep(Duration::from_secs(1)); + engine_ticker.increment_epoch(); + } + }); + let _ = handle; // detached; exits within 1 s once stop is set + // RAII guard: signals the ticker thread to stop when dropped. + struct TickerGuard(Arc); + impl Drop for TickerGuard { + fn drop(&mut self) { + self.0.store(true, Ordering::Relaxed); + } + } + TickerGuard(ticker_stop) + }; + let component = Component::from_file(&engine, wasm_path.as_std_path()).context(LoadComponentSnafu { path: wasm_path.clone(), @@ -191,12 +244,14 @@ pub fn run_plugin( .stderr(stderr_pipe.clone()); } + let epoch_extension = Arc::new(AtomicU64::new(0)); let host_state = HostState { target_canister_id, agent: Arc::new(agent), proxy, wasi_ctx: wasi_builder.build(), wasi_table: wasmtime_wasi::ResourceTable::new(), + epoch_extension: epoch_extension.clone(), }; let mut linker: Linker = Linker::new(&engine); @@ -210,6 +265,15 @@ pub fn run_plugin( )?; let mut store = Store::new(&engine, host_state); + store.set_epoch_deadline(PLUGIN_COMPUTE_LIMIT_SECS); + store.epoch_deadline_callback(move |_| { + let extra = epoch_extension.swap(0, Ordering::Relaxed); + if extra > 0 { + Ok(wasmtime::UpdateDeadline::Continue(extra)) + } else { + Err(ComputeTimeLimitExceeded.into()) + } + }); let plugin = SyncPlugin::instantiate(&mut store, &component, &linker).context(InstantiateSnafu { From 0b908af08da66bedf4045f7531e3b1be39f656a4 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 30 Apr 2026 15:56:03 -0400 Subject: [PATCH 38/39] refactor(wasm): unify wasm resolution to return a path for both prebuilt and plugin wasm::resolve() now returns a PathBuf instead of bytes, covering both use cases with consistent behavior: local sources verify sha256 if present and return the original path; remote sources with sha256 check the cache first, remote without sha256 always download and cache by the computed sha256. prebuilt uses fs::copy() (cross-filesystem safe, handles WSL) instead of writing bytes. plugin calls the same resolve() as prebuilt, removing the separate resolve_path()/cached_path() functions and the redundant LockCache error variant. Co-Authored-By: Claude Sonnet 4.6 --- crates/icp/src/canister/build/prebuilt.rs | 12 +- crates/icp/src/canister/sync/plugin.rs | 42 ++---- crates/icp/src/canister/wasm.rs | 154 ++++++++++++---------- crates/icp/src/fs/mod.rs | 14 ++ crates/icp/src/package.rs | 15 --- 5 files changed, 112 insertions(+), 125 deletions(-) diff --git a/crates/icp/src/canister/build/prebuilt.rs b/crates/icp/src/canister/build/prebuilt.rs index 5285a3598..774a102f9 100644 --- a/crates/icp/src/canister/build/prebuilt.rs +++ b/crates/icp/src/canister/build/prebuilt.rs @@ -1,9 +1,7 @@ use snafu::prelude::*; use tokio::sync::mpsc::Sender; -use crate::{ - canister::wasm, fs::write, manifest::adapter::prebuilt::Adapter, package::PackageCache, -}; +use crate::{canister::wasm, fs, manifest::adapter::prebuilt::Adapter, package::PackageCache}; use super::Params; @@ -12,8 +10,8 @@ pub enum PrebuiltError { #[snafu(transparent)] Wasm { source: wasm::WasmError }, - #[snafu(display("failed to write wasm output file"))] - WriteFile { source: crate::fs::IoError }, + #[snafu(display("failed to copy wasm to output file"))] + CopyFile { source: crate::fs::CopyError }, } pub(super) async fn build( @@ -22,7 +20,7 @@ pub(super) async fn build( stdio: Option>, pkg_cache: &PackageCache, ) -> Result<(), PrebuiltError> { - let wasm_bytes = wasm::resolve( + let src = wasm::resolve( &adapter.source, ¶ms.path, adapter.sha256.as_deref(), @@ -36,7 +34,7 @@ pub(super) async fn build( .send(format!("Writing WASM file: {}", params.output)) .await; } - write(¶ms.output, &wasm_bytes).context(WriteFileSnafu)?; + fs::copy(&src, ¶ms.output).context(CopyFileSnafu)?; Ok(()) } diff --git a/crates/icp/src/canister/sync/plugin.rs b/crates/icp/src/canister/sync/plugin.rs index 8bf7c413c..1c9da8ca3 100644 --- a/crates/icp/src/canister/sync/plugin.rs +++ b/crates/icp/src/canister/sync/plugin.rs @@ -6,10 +6,7 @@ use snafu::prelude::*; use tokio::sync::mpsc::Sender; use crate::{ - canister::wasm, - fs::read_to_string, - manifest::adapter::{plugin::Adapter, prebuilt::SourceField}, - package::PackageCache, + canister::wasm, fs::read_to_string, manifest::adapter::plugin::Adapter, package::PackageCache, }; use super::Params; @@ -33,9 +30,6 @@ pub enum PluginError { #[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 }, } @@ -50,29 +44,17 @@ pub(super) async fn sync( pkg_cache: &PackageCache, ) -> Result<(), PluginError> { // 1. Determine the on-disk path for the wasm. run_plugin needs a path, not raw bytes. - // - Local: use the manifest path directly. - // - Remote: resolve via cache (sha256 is required for remote, enforced at parse time), - // so the stable cache path is always available — no temp file needed. - let wasm_path = match &adapter.source { - SourceField::Local(s) => params.path.join(&s.path), - SourceField::Remote(_) => { - let sha = adapter - .sha256 - .as_deref() - .expect("remote plugin source requires sha256 — enforced at manifest parse time"); - wasm::resolve( - &adapter.source, - ¶ms.path, - Some(sha), - stdio.as_ref(), - pkg_cache, - ) - .await?; - wasm::cached_path(pkg_cache, sha) - .await - .context(LockCacheSnafu)? - } - }; + // - Local: sha256 is verified if present, then the original path is returned. + // - Remote: downloaded to cache (sha256 required, enforced at parse time) and the + // stable cache path is returned — no temp file needed. + let wasm_path = wasm::resolve( + &adapter.source, + ¶ms.path, + adapter.sha256.as_deref(), + stdio.as_ref(), + pkg_cache, + ) + .await?; // 2. Collect inputs: `dirs` stays as manifest strings (runtime preopens them), // `files` are read on the host and passed inline. diff --git a/crates/icp/src/canister/wasm.rs b/crates/icp/src/canister/wasm.rs index 0831ec059..2cf2b219d 100644 --- a/crates/icp/src/canister/wasm.rs +++ b/crates/icp/src/canister/wasm.rs @@ -8,7 +8,7 @@ use url::Url; use crate::{ fs::read, manifest::adapter::prebuilt::SourceField, - package::{PackageCache, cache_wasm, read_cached_wasm}, + package::{PackageCache, cache_wasm}, }; #[derive(Debug, Snafu)] @@ -34,9 +34,6 @@ pub enum WasmError { #[snafu(display("checksum mismatch, expected: {expected}, actual: {actual}"))] ChecksumMismatch { expected: String, actual: String }, - #[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 }, @@ -44,25 +41,66 @@ pub enum WasmError { 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( +/// Resolve a wasm source to a local filesystem path, optionally verifying the sha256 checksum. +/// +/// - Local: verifies sha256 if provided, returns the local path. +/// - Remote with sha256: checks the cache first; downloads, verifies, and caches on miss. +/// - Remote without sha256: always downloads, computes sha256, caches by the computed sha256. +pub async fn resolve( source: &SourceField, base_dir: &Utf8Path, sha256: Option<&str>, stdio: Option<&Sender>, -) -> Result, WasmError> { - let bytes = match source { + pkg_cache: &PackageCache, +) -> Result { + match source { SourceField::Local(s) => { let path = base_dir.join(&s.path); - if let Some(tx) = stdio { - let _ = tx.send(format!("Reading wasm: {}", s.path)).await; + if let Some(expected) = sha256 { + if let Some(tx) = stdio { + let _ = tx.send(format!("Reading wasm: {}", s.path)).await; + } + let bytes = read(&path).context(ReadLocalSnafu { + path: s.path.clone(), + })?; + if let Some(tx) = stdio { + let _ = tx.send("Verifying checksum".to_string()).await; + } + let actual = hex::encode(Sha256::digest(&bytes)); + ensure!( + actual == expected, + ChecksumMismatchSnafu { + expected: expected.to_owned(), + actual, + } + ); } - read(&path).context(ReadLocalSnafu { - path: s.path.clone(), - })? + Ok(path) } SourceField::Remote(s) => { + // Pre-download cache check is only possible when sha256 is known. + if let Some(expected) = sha256 { + let cached = pkg_cache + .with_read(async |r| { + let wasm_cache = r.wasm_sha(expected); + let path = wasm_cache.wasm(); + if path.exists() { + _ = crate::fs::write(&wasm_cache.atime(), b""); + Some(path) + } else { + None + } + }) + .await + .context(LockCacheSnafu)?; + if let Some(path) = cached { + if let Some(tx) = stdio { + let _ = tx.send("Using cached file".to_string()).await; + } + return Ok(path); + } + } + let url = Url::parse(&s.url).context(ParseUrlSnafu)?; if let Some(tx) = stdio { let _ = tx.send(format!("Fetching wasm: {url}")).await; @@ -75,66 +113,36 @@ async fn fetch( 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 { - let _ = tx.send("Verifying checksum".to_string()).await; - } - let actual = hex::encode(Sha256::digest(&bytes)); - ensure!( - actual == expected, - ChecksumMismatchSnafu { - expected: expected.to_owned(), - actual, - } - ); - } - - Ok(bytes) -} + let bytes = resp.bytes().await.context(HttpResponseSnafu)?.to_vec(); + + // Use provided sha256 as cache key (after verifying), or compute from bytes. + let cache_sha = match sha256 { + Some(expected) => { + if let Some(tx) = stdio { + let _ = tx.send("Verifying checksum".to_string()).await; + } + let actual = hex::encode(Sha256::digest(&bytes)); + ensure!( + actual == expected, + ChecksumMismatchSnafu { + expected: expected.to_owned(), + actual, + } + ); + actual + } + None => hex::encode(Sha256::digest(&bytes)), + }; + + pkg_cache + .with_write(async |w| cache_wasm(w, &cache_sha, &bytes).context(CacheFileSnafu)) + .await + .context(LockCacheSnafu)??; -/// 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 { - let _ = tx.send("Using cached file".to_string()).await; - } - return Ok(cached); + pkg_cache + .with_read(async |r| r.wasm_sha(&cache_sha).wasm()) + .await + .context(LockCacheSnafu) } } - - let bytes = fetch(source, base_dir, sha256, stdio).await?; - - if let (SourceField::Remote(_), Some(expected)) = (source, sha256) { - pkg_cache - .with_write(async |w| cache_wasm(w, expected, &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/fs/mod.rs b/crates/icp/src/fs/mod.rs index ea9e2ee21..6b16cfb2d 100644 --- a/crates/icp/src/fs/mod.rs +++ b/crates/icp/src/fs/mod.rs @@ -22,6 +22,14 @@ pub struct RenameError { to: PathBuf, } +#[derive(Debug, Snafu)] +#[snafu(display("Failed to copy `{from}` to `{to}`"))] +pub struct CopyError { + source: io::Error, + from: PathBuf, + to: PathBuf, +} + impl IoError { pub fn kind(&self) -> ErrorKind { self.source.kind() @@ -48,6 +56,12 @@ pub fn remove_file(path: &Path) -> Result<(), IoError> { std::fs::remove_file(path).context(IoSnafu { path }) } +pub fn copy(from: &Path, to: &Path) -> Result<(), CopyError> { + std::fs::copy(from, to) + .map(|_| ()) + .context(CopySnafu { from, to }) +} + pub fn rename(from: &Path, to: &Path) -> Result<(), RenameError> { std::fs::rename(from, to).context(RenameSnafu { from, to }) } diff --git a/crates/icp/src/package.rs b/crates/icp/src/package.rs index e031f681b..4b62e195c 100644 --- a/crates/icp/src/package.rs +++ b/crates/icp/src/package.rs @@ -78,21 +78,6 @@ impl RecipeCache { } } -pub fn read_cached_wasm( - cache: LRead<&PackageCachePaths>, - sha: &str, -) -> Result>, crate::fs::IoError> { - 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)?; - _ = crate::fs::write(&cache_path.atime(), b""); - Ok(Some(wasm)) - } else { - Ok(None) - } -} - pub fn cache_wasm( cache: LWrite<&PackageCachePaths>, sha: &str, From c357005132218b3dd0f0c780b3cd118544a8f319 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 30 Apr 2026 16:06:38 -0400 Subject: [PATCH 39/39] fix(sync-plugin): strip ANSI escape codes from plugin output Use console::strip_ansi_codes to prevent terminal injection via plugin stdout/stderr or return messages. Co-Authored-By: Claude Sonnet 4.6 --- Cargo.lock | 1 + Cargo.toml | 1 + crates/icp-sync-plugin/Cargo.toml | 1 + crates/icp-sync-plugin/src/runtime.rs | 4 ++-- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b46f6bfaf..c54a6198a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3907,6 +3907,7 @@ version = "0.2.5" dependencies = [ "camino", "candid", + "console 0.16.3", "hex", "ic-agent", "icp-canister-interfaces", diff --git a/Cargo.toml b/Cargo.toml index ac6682bbd..e33475edc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ candid_parser = "0.3.0" clap = { version = "4.5.3", features = ["derive", "env"] } clap-markdown = "0.1.5" cryptoki = "0.12.0" +console = "0.16.3" dialoguer = "0.12.0" directories = "6.0.0" dunce = "1.0.5" diff --git a/crates/icp-sync-plugin/Cargo.toml b/crates/icp-sync-plugin/Cargo.toml index 4cc62252b..480fdc197 100644 --- a/crates/icp-sync-plugin/Cargo.toml +++ b/crates/icp-sync-plugin/Cargo.toml @@ -9,6 +9,7 @@ publish.workspace = true [dependencies] camino.workspace = true candid.workspace = true +console.workspace = true hex.workspace = true ic-agent.workspace = true icp-canister-interfaces.workspace = true diff --git a/crates/icp-sync-plugin/src/runtime.rs b/crates/icp-sync-plugin/src/runtime.rs index 9c42e893c..0da842da9 100644 --- a/crates/icp-sync-plugin/src/runtime.rs +++ b/crates/icp-sync-plugin/src/runtime.rs @@ -299,7 +299,7 @@ pub fn run_plugin( 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 s = console::strip_ansi_codes(&String::from_utf8_lossy(&bytes)).into_owned(); let _ = tx.blocking_send(s); } } @@ -308,7 +308,7 @@ pub fn run_plugin( match result { Ok(Some(msg)) => { if let Some(tx) = &stdio { - let _ = tx.blocking_send(msg); + let _ = tx.blocking_send(console::strip_ansi_codes(&msg).into_owned()); } } Ok(None) => {}