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.
- ic_casals-0.2.0/LICENSE +21 -0
- ic_casals-0.2.0/MANIFEST.in +6 -0
- ic_casals-0.2.0/PKG-INFO +201 -0
- ic_casals-0.2.0/README.md +176 -0
- ic_casals-0.2.0/casals_cli/__init__.py +18 -0
- ic_casals-0.2.0/casals_cli/bindings.py +159 -0
- ic_casals-0.2.0/casals_cli/bundle.py +291 -0
- ic_casals-0.2.0/casals_cli/commands.py +480 -0
- ic_casals-0.2.0/casals_cli/conductor.py +264 -0
- ic_casals-0.2.0/casals_cli/frontend_bootstrap.py +218 -0
- ic_casals-0.2.0/casals_cli/ic.py +532 -0
- ic_casals-0.2.0/casals_cli/main.py +229 -0
- ic_casals-0.2.0/casals_cli/multisig.py +55 -0
- ic_casals-0.2.0/casals_cli/oracle.py +374 -0
- ic_casals-0.2.0/casals_cli/registry.py +365 -0
- ic_casals-0.2.0/casals_cli/replica.py +293 -0
- ic_casals-0.2.0/casals_cli/show.py +160 -0
- ic_casals-0.2.0/casals_cli/up.py +668 -0
- ic_casals-0.2.0/casals_cli/upgrade.py +276 -0
- ic_casals-0.2.0/casals_cli/util.py +64 -0
- ic_casals-0.2.0/casals_cli/wasm_store.py +357 -0
- ic_casals-0.2.0/pyproject.toml +49 -0
- ic_casals-0.2.0/setup.cfg +4 -0
- ic_casals-0.2.0/src/access_code.py +50 -0
- ic_casals-0.2.0/src/auth.py +89 -0
- ic_casals-0.2.0/src/ic_assets.py +197 -0
- ic_casals-0.2.0/src/ic_casals.egg-info/PKG-INFO +201 -0
- ic_casals-0.2.0/src/ic_casals.egg-info/SOURCES.txt +31 -0
- ic_casals-0.2.0/src/ic_casals.egg-info/dependency_links.txt +1 -0
- ic_casals-0.2.0/src/ic_casals.egg-info/entry_points.txt +2 -0
- ic_casals-0.2.0/src/ic_casals.egg-info/requires.txt +2 -0
- ic_casals-0.2.0/src/ic_casals.egg-info/top_level.txt +5 -0
- ic_casals-0.2.0/src/sheetv2.py +1413 -0
ic_casals-0.2.0/LICENSE
ADDED
|
@@ -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.
|
ic_casals-0.2.0/PKG-INFO
ADDED
|
@@ -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)}
|