cyclo-manager 0.2.0.dev11__tar.gz → 0.2.0.dev12__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 (23) hide show
  1. cyclo_manager-0.2.0.dev12/PKG-INFO +275 -0
  2. cyclo_manager-0.2.0.dev12/README.md +261 -0
  3. {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_host_agent/routers/repos.py +44 -1
  4. cyclo_manager-0.2.0.dev12/cyclo_manager.egg-info/PKG-INFO +275 -0
  5. {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_manager_cli/cli.py +4 -12
  6. {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/pyproject.toml +1 -1
  7. {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/setup.cfg +1 -1
  8. cyclo_manager-0.2.0.dev11/PKG-INFO +0 -141
  9. cyclo_manager-0.2.0.dev11/README.md +0 -127
  10. cyclo_manager-0.2.0.dev11/cyclo_manager.egg-info/PKG-INFO +0 -141
  11. {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_host_agent/__init__.py +0 -0
  12. {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_host_agent/main.py +0 -0
  13. {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_host_agent/models.py +0 -0
  14. {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_host_agent/routers/__init__.py +0 -0
  15. {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_host_agent/routers/update.py +0 -0
  16. {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_manager.egg-info/SOURCES.txt +0 -0
  17. {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_manager.egg-info/dependency_links.txt +0 -0
  18. {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_manager.egg-info/entry_points.txt +0 -0
  19. {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_manager.egg-info/requires.txt +0 -0
  20. {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_manager.egg-info/top_level.txt +0 -0
  21. {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_manager_cli/__init__.py +0 -0
  22. {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_manager_cli/config/config.yml +0 -0
  23. {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_manager_cli/docker/docker-compose.yml +0 -0
@@ -0,0 +1,275 @@
1
+ Metadata-Version: 2.4
2
+ Name: cyclo-manager
3
+ Version: 0.2.0.dev12
4
+ Summary: cyclo_manager CLI: pip-installable launcher for cyclo_manager server and UI containers. Run 'cyclo_manager up' to start Docker stack.
5
+ License-Expression: Apache-2.0
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: fastapi>=0.100.0
9
+ Requires-Dist: uvicorn[standard]>=0.23.0
10
+ Requires-Dist: psutil>=5.9.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest; extra == "dev"
13
+ Requires-Dist: ruff; extra == "dev"
14
+
15
+ # cyclo_manager CLI
16
+
17
+ **PyPI package:** `cyclo-manager`
18
+ **Console commands:** `cyclo_manager` (underscore), `cyclo-manager` (hyphen)
19
+ **Host agent:** `cyclo_host_agent` (installed by the package, run via systemd)
20
+
21
+ This package is the **pip-installable launcher** for the cyclo_manager stack on a robot host. It:
22
+
23
+ 1. Runs **`docker compose`** against a **bundled** [`cyclo_manager_cli/docker/docker-compose.yml`](cyclo_manager_cli/docker/docker-compose.yml) to start the **API** and **web UI** containers.
24
+ 2. Installs and maintains **`cyclo_host_agent`** as a **systemd** service for host-level operations (git repo updates, `cyclo_manager` package update from the UI).
25
+ 3. Can **`pip install -U`** itself and bring the Docker stack back up.
26
+
27
+ Implementation: [`cyclo_manager_cli/cli.py`](cyclo_manager_cli/cli.py), [`cyclo_host_agent/`](cyclo_host_agent/).
28
+
29
+ For the full monorepo (API source, UI source, dev compose), see the [repository root README](../README.md).
30
+
31
+ ---
32
+
33
+ ## Table of contents
34
+
35
+ - [Prerequisites](#prerequisites)
36
+ - [Install](#install)
37
+ - [Commands](#commands)
38
+ - [What `cyclo_manager up` does](#what-cyclo_manager-up-does)
39
+ - [Host agent (`cyclo_host_agent`)](#host-agent-cyclo_host_agent)
40
+ - [Compose services](#compose-services)
41
+ - [Configuration](#configuration)
42
+ - [Agent sockets on the host](#agent-sockets-on-the-host)
43
+ - [Environment variables](#environment-variables)
44
+ - [URLs (packaged stack)](#urls-packaged-stack)
45
+ - [Custom config and development](#custom-config-and-development)
46
+ - [Dependencies](#dependencies)
47
+ - [License](#license)
48
+
49
+ ---
50
+
51
+ ## Prerequisites
52
+
53
+ - **Docker** with **Compose v2** (`docker compose` — the legacy `docker-compose` binary alone is not enough)
54
+ - **Python 3.10+**
55
+ - **`sudo`** — required to install the host agent systemd unit, socket directory permissions, and sudoers rules
56
+ - For **`cyclo_manager update`:** **`pip`** or **`pip3`** on `PATH`
57
+ - **Do not run as root** — the host agent runs as the invoking user (or `SUDO_USER` when using `sudo cyclo_manager up`). Running as root is rejected.
58
+ - **Agent sockets** on the host under `/var/run/robotis/agent_sockets/` (robot containers and host agent). The bundled Compose file bind-mounts this tree into the API container as `/agents/`.
59
+
60
+ ---
61
+
62
+ ## Install
63
+
64
+ From **PyPI**:
65
+
66
+ ```bash
67
+ pip install cyclo-manager
68
+ ```
69
+
70
+ Pre-release versions:
71
+
72
+ ```bash
73
+ pip install --pre cyclo-manager==0.2.0.dev0
74
+ ```
75
+
76
+ If `cyclo_manager` is not on `PATH`, add the user script directory:
77
+
78
+ ```bash
79
+ export PATH="$HOME/.local/bin:$PATH"
80
+ cyclo_manager up
81
+ ```
82
+
83
+ If the entry point is missing:
84
+
85
+ ```bash
86
+ python3 -m cyclo_manager_cli.cli --help
87
+ python3 -m cyclo_manager_cli.cli up
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Commands
93
+
94
+ | Command | Behavior |
95
+ |--------|----------|
96
+ | `cyclo_manager up` | Install/refresh **cyclo_host_agent** (systemd unit, socket dir, sudoers), then `docker compose up -d` for **API + UI**, then `docker compose create --no-recreate` for **Zenoh** and **noVNC** (created but **not started**). Sets **`CYCLO_MANAGER_CONFIG_FILE`** to the bundled config. |
97
+ | `cyclo_manager up --pull` | `docker compose pull` first, then the same as `up`. |
98
+ | `cyclo_manager down` | **Tear down host agent** (stop/disable service, remove unit, sudoers, socket dir), then `docker compose down` for all services in the bundled compose file. |
99
+ | `cyclo_manager update` | `docker compose down` (containers only — **host agent stays installed**), `pip install -U cyclo-manager`, `docker compose up` + create optional services, then **refresh host agent** (unit, socket, sudoers). Optional `--pull` pulls images before the final `up`. |
100
+ | `cyclo_manager --help` | Subcommand overview |
101
+
102
+ **`down` vs `update`:** `down` removes the host agent entirely. `update` only stops Docker containers, upgrades the pip package, restarts the stack, and refreshes the host agent — it does **not** call `cyclo_manager down`.
103
+
104
+ ---
105
+
106
+ ## What `cyclo_manager up` does
107
+
108
+ Three phases:
109
+
110
+ ```text
111
+ [1/3] Setting up host agent → systemd unit, socket ownership, sudoers
112
+ [2/3] Starting containers → cyclo_manager (API) + ui
113
+ [3/3] Creating (not starting) → zenoh_daemon + novnc-server
114
+ ```
115
+
116
+ Open **http://127.0.0.1:3000** (UI) and **http://127.0.0.1:8081/docs** (API).
117
+
118
+ ---
119
+
120
+ ## Host agent (`cyclo_host_agent`)
121
+
122
+ The pip package installs a small **FastAPI** server that listens on a **Unix domain socket** on the host:
123
+
124
+ | Item | Path |
125
+ |------|------|
126
+ | Socket | `/var/run/robotis/agent_sockets/host/host_agent.sock` |
127
+ | systemd unit | `cyclo_host_agent.service` |
128
+ | sudoers | `/etc/sudoers.d/cyclo_manager` (`cp`, `udevadm` for container setup scripts) |
129
+
130
+ The API container reaches it at `/agents/host/host_agent.sock` (see bundled `config.yml`).
131
+
132
+ **Responsibilities:**
133
+
134
+ - List and update **`~/ROBOTIS-GIT/*`** git repositories (Version Management in the UI)
135
+ - Run **`cyclo_manager update`** when triggered from the UI (`POST /host/update`)
136
+
137
+ `cyclo_manager up` and `cyclo_manager update` both call `_ensure_host_agent()`, which is idempotent — safe to run after upgrades or user changes.
138
+
139
+ ---
140
+
141
+ ## Compose services
142
+
143
+ Defined in [`cyclo_manager_cli/docker/docker-compose.yml`](cyclo_manager_cli/docker/docker-compose.yml). All services use **`network_mode: host`**.
144
+
145
+ | Compose service | Container name | `cyclo_manager up` | Image (example) |
146
+ |-----------------|----------------|--------------------|-----------------|
147
+ | `cyclo_manager` | `cyclo_manager` | **Started** | `robotis/cyclo-manager:0.2.0` |
148
+ | `ui` | `cyclo_manager_ui` | **Started** | `robotis/cyclo-manager-ui:0.2.0` |
149
+ | `rmw_zenoh` | `zenoh_daemon` | **Created only** | `robotis/zenoh-daemon:latest` |
150
+ | `novnc-server` | `novnc-server` | **Created only** | `robotis/novnc-server:latest` |
151
+
152
+ Start optional containers from the UI or manually, e.g. `docker start zenoh_daemon`.
153
+
154
+ ---
155
+
156
+ ## Configuration
157
+
158
+ The CLI always mounts the **bundled** config into the API container. There is **no** `-c` / `--config` flag on the pip CLI.
159
+
160
+ **Bundled path:** [`cyclo_manager_cli/config/config.yml`](cyclo_manager_cli/config/config.yml)
161
+
162
+ The API reads it as `CONFIG_FILE=/app/config.yml` inside the container.
163
+
164
+ ### Schema
165
+
166
+ | Key | Description |
167
+ |-----|-------------|
168
+ | **`robot_container`** | Primary robot Docker container name (e.g. `ai_worker`). Used by the UI System page and service bringup. Must be a key in `sockets` and cannot be `host_agent`. |
169
+ | **`sockets`** | Map of logical name → agent **Unix socket path as seen inside the API container** (under `/agents/...`). Include robot/service containers and `host_agent`. |
170
+
171
+ s6 **service names** are not listed in config; each in-container agent reports them at runtime.
172
+
173
+ ### Example (bundled default)
174
+
175
+ ```yaml
176
+ robot_container: ai_worker
177
+
178
+ sockets:
179
+ ai_worker: "/agents/ai_worker/s6_agent.sock"
180
+ physical_ai_server: "/agents/physical_ai_server/s6_agent.sock"
181
+ host_agent: "/agents/host/host_agent.sock"
182
+ ```
183
+
184
+ ### Validation (API startup)
185
+
186
+ - At least one socket entry besides `host_agent`
187
+ - `robot_container` must exist in `sockets`
188
+ - All socket paths must be non-empty strings
189
+
190
+ To use a different config layout, mounts, or local source builds, use the repository’s **`docker-compose.dev.yml`** — see [Custom config and development](#custom-config-and-development).
191
+
192
+ ---
193
+
194
+ ## Agent sockets on the host
195
+
196
+ On the host, sockets typically live under:
197
+
198
+ ```text
199
+ /var/run/robotis/agent_sockets/
200
+ ├── ai_worker/s6_agent.sock
201
+ ├── physical_ai_server/s6_agent.sock
202
+ └── host/host_agent.sock ← created by cyclo_manager up
203
+ ```
204
+
205
+ The bundled compose file mounts the parent directory:
206
+
207
+ ```yaml
208
+ - /var/run/robotis/agent_sockets:/agents
209
+ ```
210
+
211
+ So `/agents/ai_worker/s6_agent.sock` in config corresponds to the host path above.
212
+
213
+ ---
214
+
215
+ ## Environment variables
216
+
217
+ | Variable | Set by | Purpose |
218
+ |----------|--------|---------|
219
+ | **`CYCLO_MANAGER_CONFIG_FILE`** | CLI (`up`, `down`, `update`) | Absolute path to bundled `config.yml` on the host; mounted into the API container |
220
+ | **`HOSTNAME`** | CLI (default: machine hostname) | Passed to API as `HOST_HOSTNAME` |
221
+ | **`CONFIG_FILE`** | Compose (`/app/config.yml`) | Path inside the API container |
222
+ | **`ROS_DOMAIN_ID`** | **Not** set by CLI | Set inside robot containers (e.g. `~/.bashrc`) so DDS matches your fleet |
223
+
224
+ ---
225
+
226
+ ## URLs (packaged stack)
227
+
228
+ With `network_mode: host` and default ports:
229
+
230
+ | What | URL |
231
+ |------|-----|
232
+ | Web UI | http://127.0.0.1:3000 |
233
+ | cyclo_manager API | http://127.0.0.1:8081 |
234
+ | OpenAPI (Swagger) | http://127.0.0.1:8081/docs |
235
+
236
+ ---
237
+
238
+ ## Custom config and development
239
+
240
+ The pip workflow is intended for **production robots** with pre-built images.
241
+
242
+ For development (local API/UI source, custom `config.yml`, hot reload):
243
+
244
+ ```bash
245
+ # From repository root
246
+ docker compose -f docker-compose.dev.yml up
247
+ ```
248
+
249
+ See the [root README](../README.md#development).
250
+
251
+ ---
252
+
253
+ ## Dependencies
254
+
255
+ Python dependencies declared in [`pyproject.toml`](pyproject.toml):
256
+
257
+ | Package | Used by |
258
+ |---------|---------|
259
+ | **fastapi** | `cyclo_host_agent` HTTP API |
260
+ | **uvicorn** | `cyclo_host_agent` server |
261
+ | **psutil** | Host agent / system utilities |
262
+
263
+ The **`cyclo_manager`** CLI itself uses only the Python standard library plus **`subprocess`** calls to **`docker compose`**, **`pip`**, **`systemctl`**, and **`sudo`**.
264
+
265
+ Runtime **outside** Python:
266
+
267
+ - **Docker** + **Compose v2**
268
+ - **systemd** (host agent service)
269
+ - Pre-built **Docker images** referenced in the bundled compose file
270
+
271
+ ---
272
+
273
+ ## License
274
+
275
+ SPDX: **Apache-2.0** (see [pyproject.toml](pyproject.toml) and the repository [LICENSE](../LICENSE)).
@@ -0,0 +1,261 @@
1
+ # cyclo_manager CLI
2
+
3
+ **PyPI package:** `cyclo-manager`
4
+ **Console commands:** `cyclo_manager` (underscore), `cyclo-manager` (hyphen)
5
+ **Host agent:** `cyclo_host_agent` (installed by the package, run via systemd)
6
+
7
+ This package is the **pip-installable launcher** for the cyclo_manager stack on a robot host. It:
8
+
9
+ 1. Runs **`docker compose`** against a **bundled** [`cyclo_manager_cli/docker/docker-compose.yml`](cyclo_manager_cli/docker/docker-compose.yml) to start the **API** and **web UI** containers.
10
+ 2. Installs and maintains **`cyclo_host_agent`** as a **systemd** service for host-level operations (git repo updates, `cyclo_manager` package update from the UI).
11
+ 3. Can **`pip install -U`** itself and bring the Docker stack back up.
12
+
13
+ Implementation: [`cyclo_manager_cli/cli.py`](cyclo_manager_cli/cli.py), [`cyclo_host_agent/`](cyclo_host_agent/).
14
+
15
+ For the full monorepo (API source, UI source, dev compose), see the [repository root README](../README.md).
16
+
17
+ ---
18
+
19
+ ## Table of contents
20
+
21
+ - [Prerequisites](#prerequisites)
22
+ - [Install](#install)
23
+ - [Commands](#commands)
24
+ - [What `cyclo_manager up` does](#what-cyclo_manager-up-does)
25
+ - [Host agent (`cyclo_host_agent`)](#host-agent-cyclo_host_agent)
26
+ - [Compose services](#compose-services)
27
+ - [Configuration](#configuration)
28
+ - [Agent sockets on the host](#agent-sockets-on-the-host)
29
+ - [Environment variables](#environment-variables)
30
+ - [URLs (packaged stack)](#urls-packaged-stack)
31
+ - [Custom config and development](#custom-config-and-development)
32
+ - [Dependencies](#dependencies)
33
+ - [License](#license)
34
+
35
+ ---
36
+
37
+ ## Prerequisites
38
+
39
+ - **Docker** with **Compose v2** (`docker compose` — the legacy `docker-compose` binary alone is not enough)
40
+ - **Python 3.10+**
41
+ - **`sudo`** — required to install the host agent systemd unit, socket directory permissions, and sudoers rules
42
+ - For **`cyclo_manager update`:** **`pip`** or **`pip3`** on `PATH`
43
+ - **Do not run as root** — the host agent runs as the invoking user (or `SUDO_USER` when using `sudo cyclo_manager up`). Running as root is rejected.
44
+ - **Agent sockets** on the host under `/var/run/robotis/agent_sockets/` (robot containers and host agent). The bundled Compose file bind-mounts this tree into the API container as `/agents/`.
45
+
46
+ ---
47
+
48
+ ## Install
49
+
50
+ From **PyPI**:
51
+
52
+ ```bash
53
+ pip install cyclo-manager
54
+ ```
55
+
56
+ Pre-release versions:
57
+
58
+ ```bash
59
+ pip install --pre cyclo-manager==0.2.0.dev0
60
+ ```
61
+
62
+ If `cyclo_manager` is not on `PATH`, add the user script directory:
63
+
64
+ ```bash
65
+ export PATH="$HOME/.local/bin:$PATH"
66
+ cyclo_manager up
67
+ ```
68
+
69
+ If the entry point is missing:
70
+
71
+ ```bash
72
+ python3 -m cyclo_manager_cli.cli --help
73
+ python3 -m cyclo_manager_cli.cli up
74
+ ```
75
+
76
+ ---
77
+
78
+ ## Commands
79
+
80
+ | Command | Behavior |
81
+ |--------|----------|
82
+ | `cyclo_manager up` | Install/refresh **cyclo_host_agent** (systemd unit, socket dir, sudoers), then `docker compose up -d` for **API + UI**, then `docker compose create --no-recreate` for **Zenoh** and **noVNC** (created but **not started**). Sets **`CYCLO_MANAGER_CONFIG_FILE`** to the bundled config. |
83
+ | `cyclo_manager up --pull` | `docker compose pull` first, then the same as `up`. |
84
+ | `cyclo_manager down` | **Tear down host agent** (stop/disable service, remove unit, sudoers, socket dir), then `docker compose down` for all services in the bundled compose file. |
85
+ | `cyclo_manager update` | `docker compose down` (containers only — **host agent stays installed**), `pip install -U cyclo-manager`, `docker compose up` + create optional services, then **refresh host agent** (unit, socket, sudoers). Optional `--pull` pulls images before the final `up`. |
86
+ | `cyclo_manager --help` | Subcommand overview |
87
+
88
+ **`down` vs `update`:** `down` removes the host agent entirely. `update` only stops Docker containers, upgrades the pip package, restarts the stack, and refreshes the host agent — it does **not** call `cyclo_manager down`.
89
+
90
+ ---
91
+
92
+ ## What `cyclo_manager up` does
93
+
94
+ Three phases:
95
+
96
+ ```text
97
+ [1/3] Setting up host agent → systemd unit, socket ownership, sudoers
98
+ [2/3] Starting containers → cyclo_manager (API) + ui
99
+ [3/3] Creating (not starting) → zenoh_daemon + novnc-server
100
+ ```
101
+
102
+ Open **http://127.0.0.1:3000** (UI) and **http://127.0.0.1:8081/docs** (API).
103
+
104
+ ---
105
+
106
+ ## Host agent (`cyclo_host_agent`)
107
+
108
+ The pip package installs a small **FastAPI** server that listens on a **Unix domain socket** on the host:
109
+
110
+ | Item | Path |
111
+ |------|------|
112
+ | Socket | `/var/run/robotis/agent_sockets/host/host_agent.sock` |
113
+ | systemd unit | `cyclo_host_agent.service` |
114
+ | sudoers | `/etc/sudoers.d/cyclo_manager` (`cp`, `udevadm` for container setup scripts) |
115
+
116
+ The API container reaches it at `/agents/host/host_agent.sock` (see bundled `config.yml`).
117
+
118
+ **Responsibilities:**
119
+
120
+ - List and update **`~/ROBOTIS-GIT/*`** git repositories (Version Management in the UI)
121
+ - Run **`cyclo_manager update`** when triggered from the UI (`POST /host/update`)
122
+
123
+ `cyclo_manager up` and `cyclo_manager update` both call `_ensure_host_agent()`, which is idempotent — safe to run after upgrades or user changes.
124
+
125
+ ---
126
+
127
+ ## Compose services
128
+
129
+ Defined in [`cyclo_manager_cli/docker/docker-compose.yml`](cyclo_manager_cli/docker/docker-compose.yml). All services use **`network_mode: host`**.
130
+
131
+ | Compose service | Container name | `cyclo_manager up` | Image (example) |
132
+ |-----------------|----------------|--------------------|-----------------|
133
+ | `cyclo_manager` | `cyclo_manager` | **Started** | `robotis/cyclo-manager:0.2.0` |
134
+ | `ui` | `cyclo_manager_ui` | **Started** | `robotis/cyclo-manager-ui:0.2.0` |
135
+ | `rmw_zenoh` | `zenoh_daemon` | **Created only** | `robotis/zenoh-daemon:latest` |
136
+ | `novnc-server` | `novnc-server` | **Created only** | `robotis/novnc-server:latest` |
137
+
138
+ Start optional containers from the UI or manually, e.g. `docker start zenoh_daemon`.
139
+
140
+ ---
141
+
142
+ ## Configuration
143
+
144
+ The CLI always mounts the **bundled** config into the API container. There is **no** `-c` / `--config` flag on the pip CLI.
145
+
146
+ **Bundled path:** [`cyclo_manager_cli/config/config.yml`](cyclo_manager_cli/config/config.yml)
147
+
148
+ The API reads it as `CONFIG_FILE=/app/config.yml` inside the container.
149
+
150
+ ### Schema
151
+
152
+ | Key | Description |
153
+ |-----|-------------|
154
+ | **`robot_container`** | Primary robot Docker container name (e.g. `ai_worker`). Used by the UI System page and service bringup. Must be a key in `sockets` and cannot be `host_agent`. |
155
+ | **`sockets`** | Map of logical name → agent **Unix socket path as seen inside the API container** (under `/agents/...`). Include robot/service containers and `host_agent`. |
156
+
157
+ s6 **service names** are not listed in config; each in-container agent reports them at runtime.
158
+
159
+ ### Example (bundled default)
160
+
161
+ ```yaml
162
+ robot_container: ai_worker
163
+
164
+ sockets:
165
+ ai_worker: "/agents/ai_worker/s6_agent.sock"
166
+ physical_ai_server: "/agents/physical_ai_server/s6_agent.sock"
167
+ host_agent: "/agents/host/host_agent.sock"
168
+ ```
169
+
170
+ ### Validation (API startup)
171
+
172
+ - At least one socket entry besides `host_agent`
173
+ - `robot_container` must exist in `sockets`
174
+ - All socket paths must be non-empty strings
175
+
176
+ To use a different config layout, mounts, or local source builds, use the repository’s **`docker-compose.dev.yml`** — see [Custom config and development](#custom-config-and-development).
177
+
178
+ ---
179
+
180
+ ## Agent sockets on the host
181
+
182
+ On the host, sockets typically live under:
183
+
184
+ ```text
185
+ /var/run/robotis/agent_sockets/
186
+ ├── ai_worker/s6_agent.sock
187
+ ├── physical_ai_server/s6_agent.sock
188
+ └── host/host_agent.sock ← created by cyclo_manager up
189
+ ```
190
+
191
+ The bundled compose file mounts the parent directory:
192
+
193
+ ```yaml
194
+ - /var/run/robotis/agent_sockets:/agents
195
+ ```
196
+
197
+ So `/agents/ai_worker/s6_agent.sock` in config corresponds to the host path above.
198
+
199
+ ---
200
+
201
+ ## Environment variables
202
+
203
+ | Variable | Set by | Purpose |
204
+ |----------|--------|---------|
205
+ | **`CYCLO_MANAGER_CONFIG_FILE`** | CLI (`up`, `down`, `update`) | Absolute path to bundled `config.yml` on the host; mounted into the API container |
206
+ | **`HOSTNAME`** | CLI (default: machine hostname) | Passed to API as `HOST_HOSTNAME` |
207
+ | **`CONFIG_FILE`** | Compose (`/app/config.yml`) | Path inside the API container |
208
+ | **`ROS_DOMAIN_ID`** | **Not** set by CLI | Set inside robot containers (e.g. `~/.bashrc`) so DDS matches your fleet |
209
+
210
+ ---
211
+
212
+ ## URLs (packaged stack)
213
+
214
+ With `network_mode: host` and default ports:
215
+
216
+ | What | URL |
217
+ |------|-----|
218
+ | Web UI | http://127.0.0.1:3000 |
219
+ | cyclo_manager API | http://127.0.0.1:8081 |
220
+ | OpenAPI (Swagger) | http://127.0.0.1:8081/docs |
221
+
222
+ ---
223
+
224
+ ## Custom config and development
225
+
226
+ The pip workflow is intended for **production robots** with pre-built images.
227
+
228
+ For development (local API/UI source, custom `config.yml`, hot reload):
229
+
230
+ ```bash
231
+ # From repository root
232
+ docker compose -f docker-compose.dev.yml up
233
+ ```
234
+
235
+ See the [root README](../README.md#development).
236
+
237
+ ---
238
+
239
+ ## Dependencies
240
+
241
+ Python dependencies declared in [`pyproject.toml`](pyproject.toml):
242
+
243
+ | Package | Used by |
244
+ |---------|---------|
245
+ | **fastapi** | `cyclo_host_agent` HTTP API |
246
+ | **uvicorn** | `cyclo_host_agent` server |
247
+ | **psutil** | Host agent / system utilities |
248
+
249
+ The **`cyclo_manager`** CLI itself uses only the Python standard library plus **`subprocess`** calls to **`docker compose`**, **`pip`**, **`systemctl`**, and **`sudo`**.
250
+
251
+ Runtime **outside** Python:
252
+
253
+ - **Docker** + **Compose v2**
254
+ - **systemd** (host agent service)
255
+ - Pre-built **Docker images** referenced in the bundled compose file
256
+
257
+ ---
258
+
259
+ ## License
260
+
261
+ SPDX: **Apache-2.0** (see [pyproject.toml](pyproject.toml) and the repository [LICENSE](../LICENSE)).
@@ -52,6 +52,7 @@ def _resolve_home() -> Path:
52
52
 
53
53
  WORKSPACE_PATH = _resolve_home()
54
54
  MANAGED_GITHUB_ORG = 'ROBOTIS-GIT'
55
+ ALLOWED_UPDATE_BRANCHES = frozenset({'main', 'jazzy'})
55
56
 
56
57
 
57
58
  # ── shared helpers ─────────────────────────────────────────────────────────────
@@ -84,6 +85,41 @@ def _fmt_cmd(cmd: str, output: str) -> str:
84
85
 
85
86
  # ── git helpers ────────────────────────────────────────────────────────────────
86
87
 
88
+ async def _stash_count(cwd: Path) -> int:
89
+ rc, out, _ = await _git(['stash', 'list'], cwd=cwd)
90
+ if rc != 0:
91
+ return 0
92
+ return sum(1 for line in out.splitlines() if line.strip())
93
+
94
+
95
+ async def _current_branch(repo_path: Path) -> Optional[str]:
96
+ rc, out, _ = await _git(['rev-parse', '--abbrev-ref', 'HEAD'], cwd=repo_path)
97
+ if rc != 0:
98
+ return None
99
+ branch = out.strip()
100
+ if not branch or branch == 'HEAD':
101
+ return None
102
+ return branch
103
+
104
+
105
+ async def _ensure_allowed_branch(repo_path: Path) -> None:
106
+ branch = await _current_branch(repo_path)
107
+ if branch in ALLOWED_UPDATE_BRANCHES:
108
+ return
109
+ allowed = ', '.join(sorted(ALLOWED_UPDATE_BRANCHES))
110
+ if branch:
111
+ detail = (
112
+ f"Updates are only allowed on branches: {allowed}. "
113
+ f"Current branch is '{branch}'."
114
+ )
115
+ else:
116
+ detail = (
117
+ f"Updates are only allowed on branches: {allowed}. "
118
+ 'Could not determine the current branch (detached HEAD?).'
119
+ )
120
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=detail)
121
+
122
+
87
123
  async def _git(args: list[str], cwd: Optional[Path] = None) -> tuple[int, str, str]:
88
124
  proc = await asyncio.create_subprocess_exec(
89
125
  'git', *args,
@@ -202,17 +238,23 @@ async def _update_stash(repo_path: Path) -> UpdateResponse:
202
238
  name = repo_path.name
203
239
  lines: list[str] = []
204
240
 
241
+ stash_before = await _stash_count(repo_path)
205
242
  rc, out, err = await _git(['stash', '-u'], cwd=repo_path)
206
243
  lines.append(_fmt_cmd('git stash -u', out + err))
207
244
  if rc != 0:
208
245
  return UpdateResponse(name=name, success=False, output='\n'.join(lines))
246
+ stashed = await _stash_count(repo_path) > stash_before
209
247
 
210
248
  rc, out, err = await _git(['pull'], cwd=repo_path)
211
249
  lines.append(_fmt_cmd('git pull', out + err))
212
250
  if rc != 0:
213
- await _git(['stash', 'pop'], cwd=repo_path)
251
+ if stashed:
252
+ await _git(['stash', 'pop'], cwd=repo_path)
214
253
  return UpdateResponse(name=name, success=False, output='\n'.join(lines))
215
254
 
255
+ if not stashed:
256
+ return UpdateResponse(name=name, success=True, output='\n'.join(lines))
257
+
216
258
  rc, out, err = await _git(['stash', 'pop'], cwd=repo_path)
217
259
  lines.append(_fmt_cmd('git stash pop', out + err))
218
260
 
@@ -347,6 +389,7 @@ async def start_repo_container(name: str) -> ContainerScriptResponse:
347
389
  @router.post('/{name}/update', response_model=UpdateResponse)
348
390
  async def update_repo(name: str, req: UpdateRequest) -> UpdateResponse:
349
391
  repo_path = _get_repo(name)
392
+ await _ensure_allowed_branch(repo_path)
350
393
  try:
351
394
  if req.strategy == 'reset':
352
395
  return await _update_reset(repo_path, req.preserve_files)