Casals
General-purpose canister lifecycle orchestrator for the Internet Computer — built for managed multi-tenant IC deployments with shared upgrade governance.
Casals is fully on-chain: the conductor is a canister that creates, upgrades, snapshots, and rolls back other canisters by calling the IC management canister directly. Sheets, WASM catalog, cycles policy, and audit history all live in Casals' stable state — there is no off-chain worker in the deploy path. The CLI and frontend are thin clients that submit update calls; execution and rollback logic run inside the conductor.
Any project can operate its own Casals conductor to manage a canister fleet. Realms GOS is the reference consumer — it deploys Casals per network and drives rollouts from consumer-side fleet config. Casals also powers provisioning on the gos.earth GOS-as-a-Service platform.
Casals lets a project create, upgrade, roll back, and retire its canisters under that coordinator — organized into sections, stands, and canisters. Governance is pluggable: each section delegates to one or more commanders (principals or external governance canisters). Casals provides the structure and executes approved actions; it never embeds voting logic inside the conductor.
Live demo — https://demo.ic-casals.tech
Design rationale — docs/philosophy (why orchestra / sections / stands / conductor / baton)
Model
| Term | Meaning |
|---|---|
| Section | A logical group of stands with a shared role (e.g. "Application", "Infra"). |
| Stand | A logical unit inside a section — typically one deployed application instance. |
| Canister | An actual canister. Stands contain one or more canisters. |
| Conductor | The Casals orchestrator canister — controller of managed canisters; runs all lifecycle calls on-chain. |
| Commander | A principal authorized by a section or stand to perform scoped lifecycle actions. |
| Baton / Multisig | Optional orchestration canisters (in packages/orchestration/) for managed upgrades and committee approval on target canisters. |
Key features
- Lifecycle — create, chunked install/upgrade, snapshot,
module_hashverification, all-or-nothing rollback across a stand. - Sheets — declare a whole orchestra in one JSON document;
deploy_sheetidempotently brings it to life. - Canister pool — reuses existing canisters before creating new ones (creation is expensive).
- Cycles management — native treasury, per-section/stand/canister policy, optional on-chain autopilot, or an off-chain monitor (
casals-monitor) that polls balances, runs auto top-ups, and serves the Cycles UI without burning conductor cycles on hourly samplers. - Authorized WASMs — ships with hello-world templates (Motoko, Rust, Basilisk, certified-assets frontend) plus orchestration templates (Baton, multisig); more added via governed list.
- Commanders & permissions — multiple commanders per section/stand; granular permission keys for create, upgrade, subnet whitelist, and shell access.
- Frontend — SvelteKit + Internet Identity (1-week delegation, no idle logout): Orchestra tree, Commanders, Orchestration consoles, sheet editor, cycles dashboard, WASM catalog, settings. Open the ☰ menu (top-left) for app navigation.
Off-chain cycle monitor
For production deployments, cycle observation and auto top-ups can run in casals-monitor instead of on-chain timers.
Use the hosted monitor (no account; your conductor's settings are the credential):
- In Settings → Cycle operations, choose Off-chain monitor, paste
https://service.ic-casals.techunder Hosted monitor service and click Use this service. Casals reads the service's principal fromGET /v1/serviceand fills in Monitor service URL (<base>/v1/<this conductor's canister id>) and Monitor principal. - Save. Casals stores
monitor_enabled/monitor_principal/monitor_service_urlon-chain, grants the monitor read access to managed canisters (Sync monitor access:status_visibility = allowed_viewers [monitor]), then callsPOST /v1/instanceson the service, which verifies those settings and starts polling. The Hosted monitor status card shows state (active/consent revoked/unreachable), last poll and cadence; Register / check status re-runs the registration. - To leave, switch back to On-chain (or change the principal) and save: the service stops auto top-ups at its next pass and disables the instance after 24 h; the next Sync monitor access drops the viewer grant. Nothing else is needed.
What the monitor can and cannot do. Its principal is an allowed viewer, never a controller: it can read canister_status, ask the conductor to top_up orchestra canisters (the conductor ignores the requested amount and deposits exactly what its own cycle policy says — zero when the canister is not below policy, never below treasury_reserve) and trigger convert_treasury_icp (ICP already on the treasury → cycles on the same treasury, at most once per 10 minutes). It cannot install, stop, destroy or reconfigure anything, and set_settings refuses to list it as an extra controller. New canisters get the viewer grant at provisioning, while Casals still controls them; canisters already handed to a baton are reported as skipped: not a controller by sync_controllers; casals up --sync-monitor grants those too (below). The conductor's own SPA asset canister is read by the monitor only if its viewer list is set by the multisig (CallCanister → update_settings).
A self-hosted casals-monitor works the same way; its host must be added to connect-src in frontend/static/.ic-assets.json5, or you fill the two fields by hand.
Or declare it in the sheet, so a fresh orchestra is watched from its first canister:
"environments": {
"production": {
"monitor": {
"principal": "wdtda-uafrj-6down-pxxwi-emhpc-uivwv-zdi3c-szrq7-hxodh-wlvas-iqe",
"url": "https://service.ic-casals.tech"
}
}
}
url is the service's base URL; the conductor appends /v1/<its canister id>. casals up hands the block to set_sheet, which switches the conductor to off-chain mode with these values before plan/apply creates anything, so every canister is provisioned with the viewer grant. When the block changes, or with casals up --sync-monitor, up also grants it on canisters the conductor no longer controls: it lends the conductor control of each one (as deployer, or through the multisig), runs sync_controllers, and puts the original controllers back. The canisters the deployer cannot reach are listed under monitor_access.unreachable. Registering with the service is still Register / check status in Settings (or POST /v1/instances).
This disables on-chain balance sampling and autopilot on the conductor (cycles_sampling: false, cycles_autopilot: false) while the monitor paymaster tops up from the same Casals treasury. Each signed-in user can save a Notification email under Settings → Your settings. The monitor sends operational notices (treasury cannot fund a top-up, monitor consent withdrawn, and later notices of the same kind) to those addresses. A legacy orchestra-wide address, if one was saved before this split, is included too.
For scripted wiring, see scripts/examples/wire_monitor.py (JSON config with monitor_url, monitor_principal, casals_backend, casals_frontend).
Toolchain
icp-clifor build & deploy (icp.yaml); dfx is not used.- Basilisk +
ic-basilisk-toolkitfor the backend. casals-store— the WASM store: a certified-assets canister (chunked batch upload, on-chain sha256, pinned directories) thatcasals upcreates and seeds; every install streams from it — and every frontend asset bundle (docs/BUNDLES.md). Upload from the CLI (casals up) or from the browser on/files.
Quick start
Needs Python 3.10+ and icp-cli. No checkout of this repo.
pip install ic-casals
casals init # writes casals.json, the minimal example
casals up casals.json --yes --local
casals init writes a sheet that installs the conductor, its store and its UI from the Casals GitHub release that matches the CLI (--release <tag> picks another). --local creates the local-dev identity, starts a local network and funds the identity. Outside an icp project, the network gets its own project under ~/.casals/replica.
In a checkout of this repo, the corpus sheets build the conductor from source instead (pip install . -r requirements.txt, plus make and Node/npm for the UI):
casals up tests/e2e/orchestras/minimal/casals.json --yes --local
Without --local, create the identity before the network starts: a local network funds only the identities that exist when it starts.
icp identity new local-dev --storage plaintext
icp network start -d
casals -e local --identity local-dev up tests/e2e/orchestras/minimal/casals.json --yes
casals up <sheet> is the day-one deploy path: it validates the sheet, builds and deploys the conductor and the casals-store store, uploads the referenced WASMs into it, and builds what the sheet declares (set_sheet → plan → apply). From then on the orchestra is operated imperatively — the UI, casals upgrade, create_stand, upgrade_to, … — with no on-chain reconciliation loop (issue #52).
Why the sheet stops being the truth after day one
Casals is deliberately not a reconciler. Terraform and Kubernetes converge the world onto a desired-state document, which works because the reconciler holds complete authority over what it manages, convergence is therefore always reachable, and any difference between file and world is drift to be erased. An orchestra under shared governance satisfies none of those:
- Authority is on-chain and shared; a file has none of it. A signer added by multisig proposal, a commander who redeemed an access code, a baton that ran an upgrade — all legitimate, none of them in your document. A diff between sheet and chain is genuinely ambiguous: it may mean "someone should approve this" or "the file is stale", and no control loop can tell which.
- Convergence needs other people. An item that requires N-of-M approval may wait days or never pass. A loop that treats that as failure blocks or nags forever; the right behaviour is to file a proposal and stop.
- The orchestra is itself an actor. Stands minted at runtime, a tenant's
instance growing a canister (
sync: manual, issue #51) — not drift, the system working. This holds even with a single controller, so it is not only a consequence of decentralized control.
So the chain is the source of truth. The conductor records each release into its
stored document, casals export regenerates a sheet from what is live, and
casals plan / casals oracle report divergence rather than erase it. After
hand-off the tooling may read, ship artifacts the conductor authorizes, and file
proposals — it must never assert the file over the chain. Keeping a sheet
hand-edited and re-running up on a governed orchestra is the one way to get
this wrong: it will plan to undo governance changes it knows nothing about.
The full argument is the genesis document slide in docs/philosophy.
Open http://casals_frontend.local.localhost:8000/ — log in with Internet Identity using a principal listed on Commanders (or a Casals controller).
Shipping this conductor
casals up is day one. A later change to this repo's backend or UI is scripts/deploy.sh: it rebuilds what you name and ships it with a delegated session identity. The production YubiKey signs that session once (docs/OPERATIONS.md); the deploy itself does not touch the key. With no --identity and no $CASALS_IDENTITY, the script uses the prod-session* delegation that expires last and skips an expired one.
scripts/deploy.sh # backend and frontend
scripts/deploy.sh frontend # Casals UI only
scripts/deploy.sh backend # conductor wasm only
scripts/deploy.sh --skip-build both # wasm and dist/ already built
scripts/deploy.sh --identity prod-session frontend
The UI is uploaded to the wasm store and copied onto the Casals frontend in batches. The backend is one upgrade of the conductor wasm. While mainnet calls are in flight, one line shows how much of that work is done and an ETA from the pace so far:
[ 42%] 27/65 sync casals-frontend (27/65) · ETA 4m 12s
A backend install is a single call, so its slice of the bar moves when the call returns. The footer’s first timestamp is the git commit time; deployed … is the wall clock of the frontend build that was shipped.
Do not add a new name to from util import in src/main.py. Basilisk can keep the previous util module across an upgrade and then trap post_upgrade on the missing export. Put the helper in main.py instead (see _normalize_notification_email).
CLI
Install the casals command:
pip install ic-casals
Run from your project directory (where icp.yaml lives):
casals status # version + object counts
casals tree # Section → Stand → Canister tree
casals events # audit log
casals wasms # authorized WASM catalog
casals bundle dist/ -o app-1.2.0.tgz # pack a frontend build into a hashed bundle (docs/BUNDLES.md)
casals init # write casals.json, the minimal example
casals up casals.json --yes --local # day one, local replica
casals upgrade sheet.json --wasm my-backend # release: move every canister running that family to the new build
casals upgrade sheet.json --content my-frontend # release: every frontend with that content serves the store's bundle
casals cycles # treasury + per-canister balances
casals pool # canister pool
casals export sheet.json # the sheet the conductor was built from + bindings
casals new [-y] # build, deploy, and seed (fresh canisters)
casals new ids.json [-y] # deploy with existing canister IDs
casals new -e ic --identity casals ids.json # mainnet upgrade from ID map
casals -e ic --identity casals status # mainnet, explicit identity
All output is JSON. Errors go to stderr as {"ok": false, "error": "..."} with exit code 1.
Without installing, the same commands are available via:
python3 scripts/casals.py status
make cli ARGS="status"
API
JSON-in / JSON-out text endpoints. Returns {"ok": true, …} or {"ok": false, "error": "…"}.
| Kind | Method | Purpose |
|---|---|---|
| query | get_tree |
full Section→Stand→Canister tree |
| query | get_sheet / list_pool |
stored day-one sheet + canister pool |
| query | get_cycle_history |
balance samples over time |
| query | list_permissions |
assignable commander permission keys |
| query | list_backend_controllers |
Casals canister IC controllers (for Commanders UI) |
| update | create_section / create_stand / create_canister |
structure |
| update | propose_upgrade / sync_content / deploy_content |
imperative release of a baton-governed member / a frontend bundle (one round / all rounds) |
| update | set_commander / set_permissions |
commander principals + permission grants |
| update | upgrade_to |
stand/canister upgrade with snapshot rollback |
| update | add_authorized_wasm / remove_authorized_wasm |
WASM catalog |
| update | top_up / reconcile / set_cycle_policy |
cycles management |
| update | sync_controllers |
sync monitor read access (status_visibility allowed viewer) on managed canisters |
Full endpoint list: AGENTS.md.
About the name
Named after Pablo Casals — cellist and conductor. This project coordinates canisters the way a conductor coordinates an orchestra.
Disclaimer
This software is not production-ready. Do not deploy to mainnet or use with real canisters, cycles, or governance authority you cannot afford to lose.
Casals is in early development (alpha). It may contain bugs, breaking changes, and unknown security vulnerabilities. It has not undergone an independent security audit. Use at your own risk.
- Not recommended for production deployments on the Internet Computer
- No guarantee of correctness, availability, or security
- APIs and behavior may change without notice
License
MIT — see LICENSE.
Metadata
Release files for ic-casals 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ic_casals-0.5.0.tar.gz | 108.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ic_casals-0.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 222.4 kB
Release files / ic_casals-0.5.0.tar.gz
| Download URL | ic_casals-0.5.0.tar.gz |
|---|---|
| Size | 108.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
89a97eb7bdeee07fdc68e14c09fbefa16937226a1847c1a4095e24a6e64b831d
|
|
BLAKE2b-256 checksum How to use checksums |
7d3c6258882e42337fc0c69675f3f1b95b76f90581c31fef2efab09f001c6ac8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / ic_casals-0.5.0-py3-none-any.whl
| Download URL | ic_casals-0.5.0-py3-none-any.whl |
|---|---|
| Size | 114.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
77d4bfc5ce0f63c7a1e27916efee26369d406fa1aaabee2b929006fd604f140a
|
|
BLAKE2b-256 checksum How to use checksums |
5c001fd5f5d95b8329b1a011394b46acbb05d67e4e0bed68912e74beeb80da90
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|