Repository navigation
Conversation
6 of 7 tasks
raymondk
force-pushed
the
deploy-2-pipeline
branch
from
November 3, 2026 19:43
6ccabf9 to
7131710
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Critical validation and input-bounding gaps can leave funded canisters unreported, trigger stack-overflow aborts, and allow bundle reads to exceed the promised 256 MiB memory limit.
3 open issues
What changed in this PR
Adds the deployment pipeline over the bundle host from #230, using the icp-project operations behind icp deploy. MCP tool exposure remains in #232.
Changes:
- Adds bundle loading, validation, ID seeding, deployment execution, and outcomes that preserve created canister IDs after failures.
- Implements signed canister calls and certified reads through
ic-agent. - Adds offline pipeline, input-rejection, and log-rendering tests; on-chain deployment remains untested.
| File | Description |
|---|---|
crates/imcp2-deploy/src/lib.rs |
Documents and exports the deployment API. |
crates/imcp2-deploy/src/deploy.rs |
Implements the pipeline, recovery mappings, logs, and offline tests. |
crates/imcp2-deploy/src/calls.rs |
Adapts signed calls and certified reads to CanisterCalls. |
Review effort: Balanced
raymondk
force-pushed
the
deploy-2-pipeline
branch
from
November 3, 2026 20:34
7131710 to
331b699
Compare
raymondk
force-pushed
the
deploy-2-pipeline
branch
from
November 3, 2026 23:42
331b699 to
cae8000
Compare
raymondk
marked this pull request as draft
November 4, 2026 00:16
raymondk
force-pushed
the
deploy-2-pipeline
branch
5 times, most recently
from
November 10, 2026 00:54
e331f38 to
fb78ce5
Compare
The second of three changes bringing `icp deploy` to the local MCP server. On top of the bundle host, this adds what runs a deploy: - `deploy_bundle(identity, request)`: read the bundle (a regular file by absolute path, or bytes), extract it, load the project, check the bundle whole before any phase runs, seed the id store from the caller's canister-id mapping, then `resolve_targets` and the library's `deploy` — the same entry point `icp deploy` uses, so creation, id wiring, settings, install/upgrade with the Candid check and the sync steps behave as the CLI does. The outcome carries the status, the canisters created, the resulting mapping (the id store read back, always merged with what was created, so a failed run still reports every canister it made) and the event stream rendered as a log. - `AgentCalls`: the library's `CanisterCalls` over `ic-agent`, modelled on icp-app's own implementation minus proxy routing, with certified reads via `read_state`. The agent is built by the first phase that reaches the network, as the CLI does. The tests run the real pipeline offline: a hand-built bundle is extracted, loaded, checked and stopped at the create phase with `no_create`, exactly where `icp deploy --no-create` stops. A checksum mismatch, a script step, a managed network or a testnet, a path the bundle does not hold, an unknown environment or canister, and a bundle path that is not a regular file are each refused as request errors before any phase. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… split comment Two review findings on the host (#230) are now also covered through the real pipeline: a manifest that spells a canister's directory as `/etc`, with the `canister.yaml` the loader looks for carried in the archive at that spelling, and a manifest that declares a connected network claiming the mainnet root key over a loopback endpoint. Both are refused before any phase. The comment above the manifest-path test had been split in two by an insertion; it is whole again. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two of the host bounds from #230 through the real pipeline: a gzip-compressed module that hashes correctly but expands past the module limit, and two canisters whose names differ only in letter case. Both refused before any phase. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… check off the workers Four more things a request could do to the deploy before any phase ran, each now refused at the door. The library resolves the install mode only when it installs, after the canisters exist, and panics on a spelling it does not know; a typo would create funded canisters and unwind without reporting them. The mode is checked against the four the library takes before anything is read. A bundle file was sized by its path and then read to its end, so a file replaced or grown in between was read whole. The open file is sized again and read to the limit plus one byte, which is enough to tell it outgrew the limit; the path is still checked first, so a pipe is never opened. The library parses a canister's Candid arguments at install and its module's `candid:service` interface at the upgrade check, with no depth guard and on the worker thread, where nesting past the stack aborts the process. The bundle's arguments and interfaces are held to the bound the call tools apply to caller-supplied Candid (`guard_candid_text`, now public in imcp2-core) in check_bundle, and the interface the deployed canister answers with is held to it in the calls adapter, so neither side of the comparison can be deeper than the parser can take. check_bundle reads, hashes and gunzips every module, up to a gigabyte of them, so like extraction it now runs off the runtime's workers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A canister directory outside the root is now something the loader does not find, and a variable file outside it is refused by the filesystem as outside, so the two tests read those messages rather than the host's old ones. A wasm outside the root is refused earlier still, by the untrusted rule's vet at the load: it asks the bundle's filesystem where the module really is, and nothing outside the root resolves through it, so the test reads that. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The untrusted rule now holds at the load, naming the step it refuses, so the test reads that refusal, and the preflight comment no longer counts a script step among what the check catches. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… workers What a certificate proves about a metadata section is decided in one function over the tree alone, so each branch is read from a fixture with no network: a section the tree holds, a canister proven to exist without it (no module, or a module without that section), a canister proven absent, a pruned section and a pruned controllers entry, which each prove nothing and are reported as such rather than guessed at. The project load reads the manifest and every file a canister names with the filesystem's own blocking reads, up to the metered budget, and ran on the runtime's worker that took the call; it now runs on a blocking thread as extraction and the bundle check already do, and the lazy loader keeps what it loaded for every ask after. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
raymondk
force-pushed
the
deploy-2-pipeline
branch
from
November 11, 2026 22:06
fb78ce5 to
1e9a238
Compare
raymondk
marked this pull request as ready for review
November 12, 2026 12:57
There was a problem hiding this comment.
🟡 Changes recommended
Bundle-supplied plugins execute in the server process without a Wasmtime memory limit, allowing a small untrusted plugin to exhaust server memory before its compute-time budget stops it.
4 open issues
2 resolved since last review
Review effort: Balanced
| let collector = tokio::spawn(collect_log(receiver)); | ||
|
|
||
| let mut report = DeployReport::default(); | ||
| let result = deploy(&host, &calls, ¶ms, &reporter, &mut report).await; |
Comment on lines
+184
to
+190
| let fs = { | ||
| let dir = dir.clone(); | ||
| tokio::task::spawn_blocking(move || host::extract_bundle(&bytes, &dir)) | ||
| .await | ||
| .map_err(|e| RequestError(format!("bundle extraction panicked: {e}")))? | ||
| .map_err(RequestError)? | ||
| }; |
Comment on lines
+331
to
+332
| for (name, id) in &outcome.created { | ||
| outcome.canister_ids.insert(name.clone(), *id); |
| if meta.len() > limit as u64 { | ||
| return Err(too_large(meta.len())); | ||
| } | ||
| let file = tokio::fs::File::open(path.as_std_path()).await.map_err(cannot)?; |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Summary
Second of three stacked PRs bringing
icp deployto the local MCP server. On the bundle host from #230 this adds what runs a deploy, still with no MCP surface (that is #232).deploy_bundle(identity, request): read the bundle (a regular file by absolute path, or bytes), extract it, load the project, check the bundle whole before any phase runs, seed the id store from the caller's canister-id mapping, thenresolve_targetsand the library'sdeploy. That is the entry pointicp deployuses, so creation, id wiring, settings, install/upgrade with the Candid check and the sync steps that upload a frontend's assets behave as the CLI does. The outcome carries the status, the canisters created, the resulting mapping (the id store read back, always merged with what was created, so a failed run still reports every canister it made) and the event stream rendered as a log.AgentCalls: the library'sCanisterCallsoveric-agent, modelled on icp-app's own implementation minus proxy routing, with certified reads viaread_state. The agent is built by the first phase that reaches the network, as the CLI does. The caller principal is read once at construction, becauseCanisterCalls::calleris infallible whileAgent::get_principalis not.Related issues
Changes
crates/imcp2-deploy/src/deploy.rs: the pipeline,DeployRequest/DeployOutcome, log rendering, request errors.crates/imcp2-deploy/src/calls.rs:AgentCalls.crates/imcp2-deploy/src/lib.rs: the two modules and the public exports.Testing
cargo build --locked --workspace --all-targetscargo test --locked --workspace --all-targets(27 in the crate, 13 new)cargo fmt --all -- --check/cargo clippy --locked --workspace --all-targets(clean)npm test --prefix monitoring/mcp-status(dashboard unchanged)The tests run the real pipeline offline: a hand-built bundle (manifest +
(module)wasm + its sha256) is extracted, loaded through icp-project, resolved to the impliciticenvironment, checked, and stopped at the create phase withno_create, exactly whereicp deploy --no-createstops. Refused as request errors before any phase: a checksum mismatch, a script build or sync step, a managed network and a testnet, a manifest path the bundle does not hold, an unknown environment or canister, a bundle path that is not a regular file, a canister directory spelled/etcwith itscanister.yamlcarried at that spelling, an environment on a declared network claiming the mainnet key over a loopback endpoint, a gzipped module that expands past the module limit, and two canister names differing only in case. Plus log rendering and error chains.Not covered: an actual on-chain deploy. Nothing creates a canister, installs code or runs a sync plugin, so
calls.rsis exercised only by compilation. The existing PocketIC fixture has no cycles ledger or engine registry, which the create phase needs; a follow-up could add a PocketIC test usingmode: installwithno_createagainst a pre-created canister.Checklist
🤖 Generated with Claude Code