ic-casals 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. ic_casals-0.2.0/LICENSE +21 -0
  2. ic_casals-0.2.0/MANIFEST.in +6 -0
  3. ic_casals-0.2.0/PKG-INFO +201 -0
  4. ic_casals-0.2.0/README.md +176 -0
  5. ic_casals-0.2.0/casals_cli/__init__.py +18 -0
  6. ic_casals-0.2.0/casals_cli/bindings.py +159 -0
  7. ic_casals-0.2.0/casals_cli/bundle.py +291 -0
  8. ic_casals-0.2.0/casals_cli/commands.py +480 -0
  9. ic_casals-0.2.0/casals_cli/conductor.py +264 -0
  10. ic_casals-0.2.0/casals_cli/frontend_bootstrap.py +218 -0
  11. ic_casals-0.2.0/casals_cli/ic.py +532 -0
  12. ic_casals-0.2.0/casals_cli/main.py +229 -0
  13. ic_casals-0.2.0/casals_cli/multisig.py +55 -0
  14. ic_casals-0.2.0/casals_cli/oracle.py +374 -0
  15. ic_casals-0.2.0/casals_cli/registry.py +365 -0
  16. ic_casals-0.2.0/casals_cli/replica.py +293 -0
  17. ic_casals-0.2.0/casals_cli/show.py +160 -0
  18. ic_casals-0.2.0/casals_cli/up.py +668 -0
  19. ic_casals-0.2.0/casals_cli/upgrade.py +276 -0
  20. ic_casals-0.2.0/casals_cli/util.py +64 -0
  21. ic_casals-0.2.0/casals_cli/wasm_store.py +357 -0
  22. ic_casals-0.2.0/pyproject.toml +49 -0
  23. ic_casals-0.2.0/setup.cfg +4 -0
  24. ic_casals-0.2.0/src/access_code.py +50 -0
  25. ic_casals-0.2.0/src/auth.py +89 -0
  26. ic_casals-0.2.0/src/ic_assets.py +197 -0
  27. ic_casals-0.2.0/src/ic_casals.egg-info/PKG-INFO +201 -0
  28. ic_casals-0.2.0/src/ic_casals.egg-info/SOURCES.txt +31 -0
  29. ic_casals-0.2.0/src/ic_casals.egg-info/dependency_links.txt +1 -0
  30. ic_casals-0.2.0/src/ic_casals.egg-info/entry_points.txt +2 -0
  31. ic_casals-0.2.0/src/ic_casals.egg-info/requires.txt +2 -0
  32. ic_casals-0.2.0/src/ic_casals.egg-info/top_level.txt +5 -0
  33. ic_casals-0.2.0/src/sheetv2.py +1413 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Smart Social Contracts
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ include LICENSE README.md pyproject.toml
2
+ recursive-include casals_cli *.py
3
+ include src/sheetv2.py src/access_code.py src/auth.py src/ic_assets.py
4
+ prune tests
5
+ recursive-exclude * *.egg-info
6
+ global-exclude *.pyc
@@ -0,0 +1,201 @@
1
+ Metadata-Version: 2.4
2
+ Name: ic-casals
3
+ Version: 0.2.0
4
+ Summary: CLI for querying and commanding a deployed Casals canister orchestrator
5
+ Author-email: Smart Social Contracts <contact@smartsocialcontracts.org>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/smart-social-contracts/Casals
8
+ Project-URL: Repository, https://github.com/smart-social-contracts/Casals
9
+ Project-URL: Issues, https://github.com/smart-social-contracts/Casals/issues
10
+ Keywords: internet-computer,icp,casals,canister,orchestrator
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Build Tools
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: cbor2>=5.4.0
23
+ Requires-Dist: ic-py>=1.0.0
24
+ Dynamic: license-file
25
+
26
+ <p align="center">
27
+ <img src="logo.png" alt="Casals logo" width="160" />
28
+ </p>
29
+
30
+ # Casals
31
+
32
+ **General-purpose canister lifecycle orchestrator for the Internet Computer** — built for **managed multi-tenant IC deployments with shared upgrade governance**.
33
+
34
+ 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.
35
+
36
+ Any project can operate its own Casals conductor to manage a canister fleet. [Realms GOS](https://github.com/smart-social-contracts/realms) 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](https://github.com/smart-social-contracts/gos-as-a-service).
37
+
38
+ 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.
39
+
40
+ > **Live demo** — https://igz53-6qaaa-aaaao-bbapa-cai.icp0.io
41
+
42
+ > **Design rationale** — [docs/philosophy](docs/philosophy/README.md) (why orchestra / sections / stands / conductor / baton)
43
+
44
+ ---
45
+
46
+ ## Model
47
+
48
+ | Term | Meaning |
49
+ |---|---|
50
+ | **Section** | A logical group of stands with a shared role (e.g. "Application", "Infra"). |
51
+ | **Stand** | A logical unit inside a section — typically one deployed application instance. |
52
+ | **Canister** | An actual canister. Stands contain one or more canisters. |
53
+ | **Conductor** | The Casals orchestrator canister — controller of managed canisters; runs all lifecycle calls on-chain. |
54
+ | **Commander** | A principal authorized by a section or stand to perform scoped lifecycle actions. |
55
+ | **Baton / Multisig** | Optional orchestration canisters (in `packages/orchestration/`) for managed upgrades and committee approval on target canisters. |
56
+
57
+ ---
58
+
59
+ ## Key features
60
+
61
+ - **Lifecycle** — create, chunked install/upgrade, snapshot, `module_hash` verification, all-or-nothing rollback across a stand.
62
+ - **Sheets** — declare a whole orchestra in one JSON document; `deploy_sheet` idempotently brings it to life.
63
+ - **Canister pool** — reuses existing canisters before creating new ones (creation is expensive).
64
+ - **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.
65
+ - **Authorized WASMs** — ships with hello-world templates (Motoko, Rust, Basilisk, certified-assets frontend) plus orchestration templates (Baton, multisig); more added via governed list.
66
+ - **Commanders & permissions** — multiple commanders per section/stand; granular permission keys for create, upgrade, subnet whitelist, and shell access.
67
+ - **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.
68
+
69
+ ---
70
+
71
+ ## Off-chain cycle monitor
72
+
73
+ For production deployments, cycle observation and auto top-ups can run in **[casals-monitor](https://github.com/smart-social-contracts/casals-monitor)** instead of on-chain timers.
74
+
75
+ **Use the hosted monitor** (no account; your conductor's settings are the credential):
76
+
77
+ 1. In **Settings → Cycle operations**, choose **Off-chain monitor**, paste the service base URL (`https://casals.realmsgos.dev` or `https://service.ic-casals.tech`) under **Hosted monitor service** and click **Use this service**. Casals reads the service's principal from `GET /v1/service` and fills in **Monitor service URL** (`<base>/v1/<this conductor's canister id>`) and **Monitor principal**.
78
+ 2. **Save.** Casals stores `monitor_enabled` / `monitor_principal` / `monitor_service_url` on-chain, grants the monitor read access to managed canisters (**Sync controllers**), then calls `POST /v1/instances` on 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.
79
+ 3. 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. Nothing else is needed.
80
+
81
+ 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.
82
+
83
+ 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. Optional **Alert emails** in Settings notify operators when the treasury cannot fund a top-up or when the monitor sees consent withdrawn.
84
+
85
+ For scripted wiring, see `scripts/examples/wire_monitor.py` (JSON config with `monitor_url`, `monitor_principal`, `casals_backend`, `casals_frontend`).
86
+
87
+ ---
88
+
89
+ ## Toolchain
90
+
91
+ - **`icp-cli`** for build & deploy (`icp.yaml`); dfx is not used.
92
+ - **Basilisk** + `ic-basilisk-toolkit` for the backend.
93
+ - **`casals-wasms`** — the WASM store: a [certified-assets](https://github.com/smart-social-contracts/certified-assets) canister (chunked batch upload, on-chain sha256, pinned directories) that `casals up` creates 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`.
94
+
95
+ ---
96
+
97
+ ## Quick start
98
+
99
+ ```bash
100
+ pip install ic-basilisk-toolkit
101
+ icp network start -e local # terminal 1 — keep replica running
102
+
103
+ python3 -m casals_cli.main -e local up seed/sheets/demo.json --yes # bootstrap + build the demo orchestra
104
+ ```
105
+
106
+ `casals up <sheet>` is the day-one deploy path: it validates the sheet, builds and deploys the conductor and the `casals-wasms` 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).
107
+
108
+ Open **http://casals_frontend.local.localhost:8000/** — log in with Internet Identity using a principal listed on **Commanders** (or a Casals controller).
109
+
110
+ After code changes: re-run `casals up` (it rebuilds the conductor WASM when sources changed).
111
+
112
+ Mainnet:
113
+
114
+ ```bash
115
+ python3 -m casals_cli.main -e ic --identity casals up <sheet> --yes
116
+ ```
117
+
118
+ ---
119
+
120
+ ## CLI
121
+
122
+ Install the `casals` command:
123
+
124
+ ```bash
125
+ pip install ic-casals
126
+ ```
127
+
128
+ Run from your project directory (where `icp.yaml` lives):
129
+
130
+ ```bash
131
+ casals status # version + object counts
132
+ casals tree # Section → Stand → Canister tree
133
+ casals events # audit log
134
+ casals wasms # authorized WASM catalog
135
+ casals bundle dist/ -o app-1.2.0.tgz # pack a frontend build into a hashed bundle (docs/BUNDLES.md)
136
+ casals up sheet.json --yes # day one: build the orchestra the sheet declares
137
+ casals upgrade sheet.json --wasm my-backend # release: move every canister running that family to the pinned build
138
+ casals upgrade sheet.json --content my-frontend # release: every frontend with that content serves the pinned bundle
139
+ casals cycles # treasury + per-canister balances
140
+ casals pool # canister pool
141
+ casals export sheet.json # the sheet the conductor was built from + bindings
142
+ casals new [-y] # build, deploy, and seed (fresh canisters)
143
+ casals new ids.json [-y] # deploy with existing canister IDs
144
+ casals new -e ic --identity casals ids.json # mainnet upgrade from ID map
145
+
146
+ casals -e ic --identity casals status # mainnet, explicit identity
147
+ ```
148
+
149
+ All output is JSON. Errors go to stderr as `{"ok": false, "error": "..."}` with exit code 1.
150
+
151
+ Without installing, the same commands are available via:
152
+
153
+ ```bash
154
+ python3 scripts/casals.py status
155
+ make cli ARGS="status"
156
+ ```
157
+
158
+ ---
159
+
160
+ ## API
161
+
162
+ JSON-in / JSON-out text endpoints. Returns `{"ok": true, …}` or `{"ok": false, "error": "…"}`.
163
+
164
+ | Kind | Method | Purpose |
165
+ |---|---|---|
166
+ | query | `get_tree` | full Section→Stand→Canister tree |
167
+ | query | `get_sheet` / `list_pool` | stored day-one sheet + canister pool |
168
+ | query | `get_cycle_history` | balance samples over time |
169
+ | query | `list_permissions` | assignable commander permission keys |
170
+ | query | `list_backend_controllers` | Casals canister IC controllers (for Commanders UI) |
171
+ | update | `create_section` / `create_stand` / `create_canister` | structure |
172
+ | update | `propose_upgrade` / `sync_content` | imperative release of a baton-governed member / a frontend bundle |
173
+ | update | `set_commander` / `set_permissions` | commander principals + permission grants |
174
+ | update | `upgrade_to` | stand/canister upgrade with snapshot rollback |
175
+ | update | `add_authorized_wasm` / `remove_authorized_wasm` | WASM catalog |
176
+ | update | `top_up` / `reconcile` / `set_cycle_policy` | cycles management |
177
+ | update | `sync_controllers` | add monitor co-controller on managed canisters |
178
+
179
+ Full endpoint list: [AGENTS.md](AGENTS.md).
180
+
181
+ ---
182
+
183
+ ## About the name
184
+
185
+ *Named after [Pablo Casals](https://en.wikipedia.org/wiki/Pablo_Casals) — cellist and conductor. This project coordinates canisters the way a conductor coordinates an orchestra.*
186
+
187
+ ---
188
+
189
+ ## Disclaimer
190
+
191
+ **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.
192
+
193
+ 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.**
194
+
195
+ - Not recommended for production deployments on the Internet Computer
196
+ - No guarantee of correctness, availability, or security
197
+ - APIs and behavior may change without notice
198
+
199
+ ## License
200
+
201
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,176 @@
1
+ <p align="center">
2
+ <img src="logo.png" alt="Casals logo" width="160" />
3
+ </p>
4
+
5
+ # Casals
6
+
7
+ **General-purpose canister lifecycle orchestrator for the Internet Computer** — built for **managed multi-tenant IC deployments with shared upgrade governance**.
8
+
9
+ 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.
10
+
11
+ Any project can operate its own Casals conductor to manage a canister fleet. [Realms GOS](https://github.com/smart-social-contracts/realms) 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](https://github.com/smart-social-contracts/gos-as-a-service).
12
+
13
+ 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.
14
+
15
+ > **Live demo** — https://igz53-6qaaa-aaaao-bbapa-cai.icp0.io
16
+
17
+ > **Design rationale** — [docs/philosophy](docs/philosophy/README.md) (why orchestra / sections / stands / conductor / baton)
18
+
19
+ ---
20
+
21
+ ## Model
22
+
23
+ | Term | Meaning |
24
+ |---|---|
25
+ | **Section** | A logical group of stands with a shared role (e.g. "Application", "Infra"). |
26
+ | **Stand** | A logical unit inside a section — typically one deployed application instance. |
27
+ | **Canister** | An actual canister. Stands contain one or more canisters. |
28
+ | **Conductor** | The Casals orchestrator canister — controller of managed canisters; runs all lifecycle calls on-chain. |
29
+ | **Commander** | A principal authorized by a section or stand to perform scoped lifecycle actions. |
30
+ | **Baton / Multisig** | Optional orchestration canisters (in `packages/orchestration/`) for managed upgrades and committee approval on target canisters. |
31
+
32
+ ---
33
+
34
+ ## Key features
35
+
36
+ - **Lifecycle** — create, chunked install/upgrade, snapshot, `module_hash` verification, all-or-nothing rollback across a stand.
37
+ - **Sheets** — declare a whole orchestra in one JSON document; `deploy_sheet` idempotently brings it to life.
38
+ - **Canister pool** — reuses existing canisters before creating new ones (creation is expensive).
39
+ - **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.
40
+ - **Authorized WASMs** — ships with hello-world templates (Motoko, Rust, Basilisk, certified-assets frontend) plus orchestration templates (Baton, multisig); more added via governed list.
41
+ - **Commanders & permissions** — multiple commanders per section/stand; granular permission keys for create, upgrade, subnet whitelist, and shell access.
42
+ - **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.
43
+
44
+ ---
45
+
46
+ ## Off-chain cycle monitor
47
+
48
+ For production deployments, cycle observation and auto top-ups can run in **[casals-monitor](https://github.com/smart-social-contracts/casals-monitor)** instead of on-chain timers.
49
+
50
+ **Use the hosted monitor** (no account; your conductor's settings are the credential):
51
+
52
+ 1. In **Settings → Cycle operations**, choose **Off-chain monitor**, paste the service base URL (`https://casals.realmsgos.dev` or `https://service.ic-casals.tech`) under **Hosted monitor service** and click **Use this service**. Casals reads the service's principal from `GET /v1/service` and fills in **Monitor service URL** (`<base>/v1/<this conductor's canister id>`) and **Monitor principal**.
53
+ 2. **Save.** Casals stores `monitor_enabled` / `monitor_principal` / `monitor_service_url` on-chain, grants the monitor read access to managed canisters (**Sync controllers**), then calls `POST /v1/instances` on 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.
54
+ 3. 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. Nothing else is needed.
55
+
56
+ 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.
57
+
58
+ 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. Optional **Alert emails** in Settings notify operators when the treasury cannot fund a top-up or when the monitor sees consent withdrawn.
59
+
60
+ For scripted wiring, see `scripts/examples/wire_monitor.py` (JSON config with `monitor_url`, `monitor_principal`, `casals_backend`, `casals_frontend`).
61
+
62
+ ---
63
+
64
+ ## Toolchain
65
+
66
+ - **`icp-cli`** for build & deploy (`icp.yaml`); dfx is not used.
67
+ - **Basilisk** + `ic-basilisk-toolkit` for the backend.
68
+ - **`casals-wasms`** — the WASM store: a [certified-assets](https://github.com/smart-social-contracts/certified-assets) canister (chunked batch upload, on-chain sha256, pinned directories) that `casals up` creates 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`.
69
+
70
+ ---
71
+
72
+ ## Quick start
73
+
74
+ ```bash
75
+ pip install ic-basilisk-toolkit
76
+ icp network start -e local # terminal 1 — keep replica running
77
+
78
+ python3 -m casals_cli.main -e local up seed/sheets/demo.json --yes # bootstrap + build the demo orchestra
79
+ ```
80
+
81
+ `casals up <sheet>` is the day-one deploy path: it validates the sheet, builds and deploys the conductor and the `casals-wasms` 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).
82
+
83
+ Open **http://casals_frontend.local.localhost:8000/** — log in with Internet Identity using a principal listed on **Commanders** (or a Casals controller).
84
+
85
+ After code changes: re-run `casals up` (it rebuilds the conductor WASM when sources changed).
86
+
87
+ Mainnet:
88
+
89
+ ```bash
90
+ python3 -m casals_cli.main -e ic --identity casals up <sheet> --yes
91
+ ```
92
+
93
+ ---
94
+
95
+ ## CLI
96
+
97
+ Install the `casals` command:
98
+
99
+ ```bash
100
+ pip install ic-casals
101
+ ```
102
+
103
+ Run from your project directory (where `icp.yaml` lives):
104
+
105
+ ```bash
106
+ casals status # version + object counts
107
+ casals tree # Section → Stand → Canister tree
108
+ casals events # audit log
109
+ casals wasms # authorized WASM catalog
110
+ casals bundle dist/ -o app-1.2.0.tgz # pack a frontend build into a hashed bundle (docs/BUNDLES.md)
111
+ casals up sheet.json --yes # day one: build the orchestra the sheet declares
112
+ casals upgrade sheet.json --wasm my-backend # release: move every canister running that family to the pinned build
113
+ casals upgrade sheet.json --content my-frontend # release: every frontend with that content serves the pinned bundle
114
+ casals cycles # treasury + per-canister balances
115
+ casals pool # canister pool
116
+ casals export sheet.json # the sheet the conductor was built from + bindings
117
+ casals new [-y] # build, deploy, and seed (fresh canisters)
118
+ casals new ids.json [-y] # deploy with existing canister IDs
119
+ casals new -e ic --identity casals ids.json # mainnet upgrade from ID map
120
+
121
+ casals -e ic --identity casals status # mainnet, explicit identity
122
+ ```
123
+
124
+ All output is JSON. Errors go to stderr as `{"ok": false, "error": "..."}` with exit code 1.
125
+
126
+ Without installing, the same commands are available via:
127
+
128
+ ```bash
129
+ python3 scripts/casals.py status
130
+ make cli ARGS="status"
131
+ ```
132
+
133
+ ---
134
+
135
+ ## API
136
+
137
+ JSON-in / JSON-out text endpoints. Returns `{"ok": true, …}` or `{"ok": false, "error": "…"}`.
138
+
139
+ | Kind | Method | Purpose |
140
+ |---|---|---|
141
+ | query | `get_tree` | full Section→Stand→Canister tree |
142
+ | query | `get_sheet` / `list_pool` | stored day-one sheet + canister pool |
143
+ | query | `get_cycle_history` | balance samples over time |
144
+ | query | `list_permissions` | assignable commander permission keys |
145
+ | query | `list_backend_controllers` | Casals canister IC controllers (for Commanders UI) |
146
+ | update | `create_section` / `create_stand` / `create_canister` | structure |
147
+ | update | `propose_upgrade` / `sync_content` | imperative release of a baton-governed member / a frontend bundle |
148
+ | update | `set_commander` / `set_permissions` | commander principals + permission grants |
149
+ | update | `upgrade_to` | stand/canister upgrade with snapshot rollback |
150
+ | update | `add_authorized_wasm` / `remove_authorized_wasm` | WASM catalog |
151
+ | update | `top_up` / `reconcile` / `set_cycle_policy` | cycles management |
152
+ | update | `sync_controllers` | add monitor co-controller on managed canisters |
153
+
154
+ Full endpoint list: [AGENTS.md](AGENTS.md).
155
+
156
+ ---
157
+
158
+ ## About the name
159
+
160
+ *Named after [Pablo Casals](https://en.wikipedia.org/wiki/Pablo_Casals) — cellist and conductor. This project coordinates canisters the way a conductor coordinates an orchestra.*
161
+
162
+ ---
163
+
164
+ ## Disclaimer
165
+
166
+ **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.
167
+
168
+ 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.**
169
+
170
+ - Not recommended for production deployments on the Internet Computer
171
+ - No guarantee of correctness, availability, or security
172
+ - APIs and behavior may change without notice
173
+
174
+ ## License
175
+
176
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,18 @@
1
+ """Casals CLI v2 — declarative orchestra bootstrap and reconciliation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+
8
+ # Keep in lockstep with [project].version in pyproject.toml (publish-pypi.yml checks).
9
+ __version__ = "0.2.0"
10
+
11
+ _ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
12
+ _SRC = os.path.join(_ROOT, "src")
13
+ if _SRC not in sys.path:
14
+ sys.path.insert(0, _SRC)
15
+ if _ROOT not in sys.path:
16
+ sys.path.insert(0, _ROOT)
17
+
18
+ __all__ = ["main", "__version__"]
@@ -0,0 +1,159 @@
1
+ """Runtime bindings: sheet name → canister id per environment."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from dataclasses import dataclass, field
8
+ from datetime import datetime, timezone
9
+ from typing import Any
10
+
11
+ from sheetv2 import CONDUCTOR_NAMES
12
+
13
+
14
+
15
+ def bindings_dir() -> str:
16
+ """`$CASALS_HOME` (tests, CI) or `~/.casals`."""
17
+ return os.environ.get("CASALS_HOME") or os.path.expanduser("~/.casals")
18
+
19
+
20
+ @dataclass
21
+ class Bindings:
22
+ sheet_name: str
23
+ env: str
24
+ network_url: str
25
+ deployer: str
26
+ conductor: dict[str, str] = field(default_factory=dict)
27
+ created_at: str = ""
28
+ backend_id: str = ""
29
+ icp_project_dir: str = ""
30
+ conductor_module_hashes: dict[str, str] = field(default_factory=dict)
31
+ asset_dist_hashes: dict[str, str] = field(default_factory=dict)
32
+
33
+ @property
34
+ def casals_backend_id(self) -> str:
35
+ return self.backend_id or self.conductor.get(CONDUCTOR_NAMES["backend"], "")
36
+
37
+ def path(self) -> str:
38
+ safe = self.sheet_name.replace("/", "_")
39
+ return os.path.join(bindings_dir(), f"{safe}.{self.env}.json")
40
+
41
+ def to_dict(self) -> dict[str, Any]:
42
+ return {
43
+ "sheet_name": self.sheet_name,
44
+ "env": self.env,
45
+ "network_url": self.network_url,
46
+ "deployer": self.deployer,
47
+ "conductor": dict(self.conductor),
48
+ "backend_id": self.backend_id or self.casals_backend_id,
49
+ "created_at": self.created_at or datetime.now(timezone.utc).isoformat(),
50
+ "icp_project_dir": self.icp_project_dir,
51
+ "conductor_module_hashes": dict(self.conductor_module_hashes),
52
+ "asset_dist_hashes": dict(self.asset_dist_hashes),
53
+ }
54
+
55
+ @classmethod
56
+ def from_dict(cls, data: dict) -> "Bindings":
57
+ conductor = data.get("conductor") or {}
58
+ backend = (data.get("backend_id") or conductor.get(CONDUCTOR_NAMES["backend"]) or "").strip()
59
+ return cls(
60
+ sheet_name=str(data.get("sheet_name") or ""),
61
+ env=str(data.get("env") or ""),
62
+ network_url=str(data.get("network_url") or ""),
63
+ deployer=str(data.get("deployer") or ""),
64
+ conductor={k: str(v) for k, v in conductor.items()},
65
+ created_at=str(data.get("created_at") or ""),
66
+ backend_id=backend,
67
+ icp_project_dir=str(data.get("icp_project_dir") or ""),
68
+ conductor_module_hashes={
69
+ k: str(v) for k, v in (data.get("conductor_module_hashes") or {}).items()
70
+ },
71
+ asset_dist_hashes={
72
+ k: str(v) for k, v in (data.get("asset_dist_hashes") or {}).items()
73
+ },
74
+ )
75
+
76
+ def save(self) -> None:
77
+ os.makedirs(bindings_dir(), exist_ok=True)
78
+ if not self.created_at:
79
+ self.created_at = datetime.now(timezone.utc).isoformat()
80
+ with open(self.path(), "w", encoding="utf-8") as f:
81
+ json.dump(self.to_dict(), f, indent=2)
82
+ f.write("\n")
83
+
84
+ def remove(self) -> None:
85
+ path = self.path()
86
+ if os.path.exists(path):
87
+ os.remove(path)
88
+
89
+
90
+ def load_bindings(sheet_name: str, env: str) -> Bindings | None:
91
+ b = Bindings(sheet_name=sheet_name, env=env, network_url="", deployer="")
92
+ path = b.path()
93
+ if not os.path.isfile(path):
94
+ return None
95
+ with open(path, encoding="utf-8") as f:
96
+ return Bindings.from_dict(json.load(f))
97
+
98
+
99
+ def resolve_backend_id(bindings: Bindings | None, conductor_override: str | None) -> str:
100
+ if conductor_override:
101
+ return conductor_override.strip()
102
+ if bindings and bindings.casals_backend_id:
103
+ return bindings.casals_backend_id
104
+ return ""
105
+
106
+
107
+ def live_bindings(ic, sheet_name: str, env: str, conductor_override: str | None = None) -> tuple[str, dict[str, str]]:
108
+ """(backend id, name → canister id) for an orchestra: local bindings file + the
109
+ conductor's `get_bindings` (stand canisters are only known to the conductor)."""
110
+ local = load_bindings(sheet_name, env)
111
+ backend = resolve_backend_id(local, conductor_override)
112
+ if not backend:
113
+ raise RuntimeError("no conductor; run casals up or pass --conductor")
114
+ ids = dict(local.conductor) if local else {}
115
+ res = ic.query(backend, "get_bindings")
116
+ if isinstance(res, dict) and res.get("bindings"):
117
+ ids.update(res["bindings"])
118
+ ids[CONDUCTOR_NAMES["backend"]] = backend
119
+ return backend, ids
120
+
121
+
122
+ def find_bindings_for_env(env: str, conductor: str | None = None) -> Bindings | None:
123
+ """Locate a bindings file for ``env``, optionally matching ``conductor``."""
124
+ if not os.path.isdir(bindings_dir()):
125
+ return None
126
+ matches: list[Bindings] = []
127
+ suffix = f".{env}.json"
128
+ for fname in os.listdir(bindings_dir()):
129
+ if not fname.endswith(suffix):
130
+ continue
131
+ path = os.path.join(bindings_dir(), fname)
132
+ try:
133
+ with open(path, encoding="utf-8") as f:
134
+ b = Bindings.from_dict(json.load(f))
135
+ except (OSError, json.JSONDecodeError, TypeError):
136
+ continue
137
+ if conductor:
138
+ cid = conductor.strip()
139
+ if b.backend_id != cid and b.casals_backend_id != cid:
140
+ continue
141
+ matches.append(b)
142
+ if not matches:
143
+ return None
144
+ if len(matches) == 1:
145
+ return matches[0]
146
+ if conductor:
147
+ return matches[0]
148
+ raise RuntimeError(
149
+ f"multiple bindings for env {env!r}; pass --conductor or run from a named sheet"
150
+ )
151
+
152
+
153
+ def live_stands(tree: dict) -> dict[str, dict]:
154
+ """stand name → {section, members, built} from the conductor's `get_tree`
155
+ (`built` absent = an older conductor: treat the stand as finished)."""
156
+ return {st.get("name", ""): {"section": sec.get("name", ""), "members": st.get("members") or [],
157
+ "built": bool(st.get("built", True))}
158
+ for sec in (tree.get("sections") or []) if isinstance(sec, dict)
159
+ for st in sec.get("stands") or [] if isinstance(st, dict)}