This crate is the sync plugin invoked by icp-cli for assets canister syncing.
icp-cli added a new plugin system for canister syncing (operations that always run after canister module installation/upgrade).
This crate is an implementation of it that is supposed to be used with the assets canister only.
It is a WASM component module that only access the functionalities exposed by the icp-cli plugin runtime.
It exports an exec() function which executes operations similar to the sync() function of the public ic-asset crate:
- Read access to one or more directories (assets to be uploaded)
- Sync them to a deployed assets canister
The runtime side of the plugin system lives in github.com/dfinity/icp-cli:
- The runtime crate:
crates/icp-sync-plugin - An example:
examples/icp-sync-plugin
All sync logic (directory scanning, MIME detection, content encoding, canister diffing, and per-endpoint call wrappers) lives in the assets-sync library crate. This plugin crate is a thin WASI/WIT wrapper: it implements the CanisterCall trait (WasiCall) on top of the host's canister-call import, then delegates to assets_sync::sync::sync().
The protocol-level pieces (Candid types, batch/chunk upload flow, content encoding) were ported from the ic-asset crate in github.com/dfinity/sdk (src/canisters/frontend/ic-asset). The transport layer was rewritten on top of the host's canister-call import.
cargo build -p plugin --target wasm32-wasip2 --releaseThe output WASM lands at ../target/wasm32-wasip2/release/plugin.wasm — the path that ../example/icp.yaml references.
The current implementation supports the V2 protocol of the assets canister (transactional batch API):
- Walks each directory passed via the manifest's
dirs:setting; dotfiles are skipped. - Detects the MIME type of each file and computes encodings:
gzipfor alltext/*,*/javascript, and*/htmltypes (only if the compressed output is smaller),identityfor everything. - Diffs against
list_assets(): skips encodings already in place (matched by sha256), unsets encodings that are stale, and deletes assets that have been removed or whosecontent_typechanged. - Opens a transaction (
create_batch), uploads each content chunk viacreate_chunk(one canister call per chunk, 1.9 MB max), then commits all operations atomically with a singlecommit_batchcall. - In normal mode all canister calls use
direct: true. In proxy mode (when aproxy_canister_idis provided by the host) the plugin first ensures the signing identity hasCommitpermission, routing agrant_permissioncall through the proxy (which is the canister's controller) if needed, then proceeds with direct calls.
The plugin calls api_version first and aborts if the canister advertises anything below 2.
-
.ic-assets.json5parsing — per-directory config forheaders,max_age,allow_raw_access,enable_aliasing, encoding overrides, and ignore globs.- Port
AssetSourceDirectoryConfigurationandAssetConfigRulefromic-asset/src/asset/config.rsintoassets-sync. The struct supports both.ic-assets.jsonand.ic-assets.json5via thejson5crate. - Extend
AssetSource(currently justpath+key) to carry anAssetConfigresolved at scan time. - In
scan.rs, load the config tree withAssetSourceDirectoryConfiguration::load(root)and callget_asset_config(path)for each file. Respect theignorefield to skip files, and skip the config files themselves (.ic-assets.json/.ic-assets.json5). - Thread the resolved
AssetConfigthroughprepare_assetand intobuild_operationsso thatCreateAssetArgumentsfields (max_age,headers,enable_aliasing,allow_raw_access) are populated from config instead of hardcodedNone.
- Port
-
Warn on unmatched config rules — track which
AssetConfigRuleglob patterns actually matched at least one asset during scanning, and warn about the unused ones so users are alerted to typos. Mirrorsic-asset'sAssetSourceDirectoryConfiguration::get_unused_configs()(seeic-asset/src/asset/config.rs). -
Security policy — adopt
ic-asset's CSP / standard security headers. Depends on.ic-assets.json5parsing above.- Port
SecurityPolicyenum (disabled/standard/hardened) and theConcreteSecurityPolicy/to_headers()logic fromic-asset/src/security_policy.rsintoassets-sync. - Wire it into
AssetConfig::combined_headers()so thatsecurity_policyentries in.ic-assets.json5expand into the correspondingContent-Security-Policy,Permissions-Policy,X-Frame-Options, etc. headers before the headers map is passed toCreateAssetArguments. - Emit the same warnings as
ic-asset/src/sync.rs(gather_asset_descriptors): warn when no security policy is set for any asset, warn whenstandardpolicy is in use (suggesting hardening), and error whenhardenedis declared but no custom headers are provided.
- Port
-
Asset properties update — emit
SetAssetPropertiesops for assets whose properties drifted. Types are already in place.- Add a
get_asset_propertiescall incanister.rs(the canister exposesget_asset_propertiestaking a list of keys; seeic-asset/src/canister_api/methods/asset_properties.rs). - Call it in
sync()afterlist_assets, collecting aHashMap<String, AssetProperties>. - Add an
update_propertiesstep at the end ofbuild_operations, mirroringic-asset/src/batch_upload/operations.rs::update_properties: for each asset that already exists on the canister, comparemax_age,headers,allow_raw_access, andis_aliasedagainst the project config and push aBatchOperationKind::SetAssetPropertiesonly when at least one field differs. SetAssetPropertiesArgumentsis already defined incanister.rsandBatchOperationKind::SetAssetPropertiesis already a variant, so no new types are needed.
- Add a
-
commit_batchchunking — split operations across multiplecommit_batchcalls to stay within the ~2 MB ICP ingress message limit, matchingic-assetbehaviour.- Replace the single
commit_batchcall insync()with acommit_in_stageshelper modelled onic-asset/src/sync.rs::commit_in_stages. - Implement
create_commit_batches(ops)that splits the operation list using two limits: 500 ops per batch (cert-tree work) and 1.5 MB of accumulated header data per batch (header maps dominate ingress size). Header size is the sum of key+value lengths across allCreateAssetandSetAssetPropertiesoperations in the batch. - Each intermediate batch is committed with
batch_id = 0; after all operation batches are committed, a final call with the realbatch_idand an empty operations list closes the batch.
- Replace the single
-
Multi-chunk upload via
create_chunks— send multiple small chunks per canister call instead of one, reducing round-trips for projects with many small files.- Add a
create_chunkswrapper incanister.rsthat calls the canister'screate_chunksmethod (takesbatch_idandVec<Vec<u8>>, returnsVec<Nat>chunk IDs). - In
sync(), replace the single-chunk loop with a batching uploader: accumulate chunks up toMAX_CHUNK_SIZE(1.9 MB) of total payload per call, then flush viacreate_chunks. The last chunk of each file can be sent inline aslast_chunkinSetAssetContentArgumentsrather than as a separate chunk ID, saving one round-trip per file.
- Add a
-
Brotli encoding — wired into the encoder enum and
encode()implementation but not selected by any policy.- Once
.ic-assets.json5parsing is in place, encoding overrides from config ("encodings": ["identity", "gzip", "br"]) will naturally flow throughencoders_for/prepare_asset, enabling Brotli for specific assets. - Remove the
#[allow(dead_code)]onEncoder::Brotliincontent.rsand addbras a recognised string in theContentEncoderdeserialisation (needed for the config parser). - Optionally add Brotli to the default encoder policy for compressible types, matching user expectations (the old
ic-assetdoes not include it by default, but it could be added here).
- Once