Skip to content

imcp2-deploy 2/3: deploy a bundle through icp-project, the pipeline and its signed calls - #231

Open
raymondk wants to merge 7 commits into
deploy-1-bundle-hostfrom
deploy-2-pipeline
Open

raymondk wants to merge 7 commits into
deploy-1-bundle-hostfrom
deploy-2-pipeline

Conversation

@raymondk

@raymondk raymondk commented Nov 3, 2026 •

Copy link
Copy Markdown

Summary

Second of three stacked PRs bringing icp deploy to 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, then resolve_targets and the library's deploy. That is the entry point icp deploy uses, 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'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 caller principal is read once at construction, because CanisterCalls::caller is infallible while Agent::get_principal is 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-targets
  • cargo 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 implicit ic environment, checked, and stopped at the create phase with no_create, exactly where icp deploy --no-create stops. 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 /etc with its canister.yaml carried 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.rs is 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 using mode: install with no_create against a pre-created canister.

Checklist

  • I have read the Contributing guidelines.
  • Docs (README / comments) updated for any user-visible change.
  • No secrets, credentials, or internal-only information are included.

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment thread crates/imcp2-deploy/src/deploy.rs Resolved
Comment thread crates/imcp2-deploy/src/deploy.rs Resolved
Comment thread crates/imcp2-deploy/src/deploy.rs Resolved
Copilot AI balanced review requested due to automatic review settings November 3, 2026 20:34

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Bundle reads can bypass the 256 MiB limit and exhaust server memory, while invalid install modes can spend cycles before panicking without returning canister IDs needed for recovery.

4 open issues

Review effort: Balanced

Comment thread crates/imcp2-deploy/src/deploy.rs Resolved
Copilot AI balanced review requested due to automatic review settings November 3, 2026 23:42

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Project loading can block Tokio workers with large synchronous filesystem reads, and certificate-result branches affecting upgrade compatibility remain untested.

2 open issues
4 resolved since last review

Review effort: Balanced

Comment thread crates/imcp2-deploy/src/calls.rs Resolved
Comment thread crates/imcp2-deploy/src/deploy.rs Resolved
@raymondk
raymondk marked this pull request as draft November 4, 2026 00:16
@raymondk
raymondk force-pushed the deploy-2-pipeline branch 5 times, most recently from e331f38 to fb78ce5 Compare November 10, 2026 00:54
raymondk and others added 7 commits November 11, 2026 21:17
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
raymondk marked this pull request as ready for review November 12, 2026 12:57
Copilot AI balanced review requested due to automatic review settings November 12, 2026 12:57

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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, &params, &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

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants