cyclo-manager 0.4.0.dev0__tar.gz → 1.0.0.dev1__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 (20) hide show
  1. {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0.dev1}/PKG-INFO +11 -12
  2. {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0.dev1}/README.md +10 -11
  3. {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0.dev1}/cyclo_host_agent/routers/repos.py +5 -2
  4. {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0.dev1}/cyclo_manager.egg-info/PKG-INFO +11 -12
  5. {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0.dev1}/cyclo_manager_cli/cli.py +80 -19
  6. {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0.dev1}/cyclo_manager_cli/config/config.yml +4 -1
  7. {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0.dev1}/pyproject.toml +1 -1
  8. {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0.dev1}/cyclo_host_agent/__init__.py +0 -0
  9. {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0.dev1}/cyclo_host_agent/main.py +0 -0
  10. {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0.dev1}/cyclo_host_agent/models.py +0 -0
  11. {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0.dev1}/cyclo_host_agent/routers/__init__.py +0 -0
  12. {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0.dev1}/cyclo_host_agent/routers/update.py +0 -0
  13. {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0.dev1}/cyclo_manager.egg-info/SOURCES.txt +0 -0
  14. {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0.dev1}/cyclo_manager.egg-info/dependency_links.txt +0 -0
  15. {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0.dev1}/cyclo_manager.egg-info/entry_points.txt +0 -0
  16. {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0.dev1}/cyclo_manager.egg-info/requires.txt +0 -0
  17. {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0.dev1}/cyclo_manager.egg-info/top_level.txt +0 -0
  18. {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0.dev1}/cyclo_manager_cli/__init__.py +0 -0
  19. {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0.dev1}/cyclo_manager_cli/docker/docker-compose.yml +0 -0
  20. {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0.dev1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cyclo-manager
3
- Version: 0.4.0.dev0
3
+ Version: 1.0.0.dev1
4
4
  Summary: cyclo_manager CLI: pip-installable launcher for cyclo_manager server and UI containers. Run 'cyclo_manager up' to start Docker stack.
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.10
@@ -52,9 +52,9 @@ For the full monorepo (API source, UI source, dev compose), see the [repository
52
52
 
53
53
  - **Docker** with **Compose v2** (`docker compose` — the legacy `docker-compose` binary alone is not enough)
54
54
  - **Python 3.10+**
55
- - **`sudo`** — required to install the host agent systemd unit, socket directory permissions, and sudoers rules
55
+ - **`sudo`** — required on normal-user hosts to install the host agent systemd unit, socket directory permissions, and sudoers rules
56
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.
57
+ - **Root shell handling** — root is rejected when a normal login user exists. On root-only devices, the host agent is installed as root.
58
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
59
 
60
60
  ---
@@ -120,7 +120,7 @@ The pip package installs a small **FastAPI** server that listens on a **Unix dom
120
120
  |------|------|
121
121
  | Socket | `/var/run/robotis/agent_sockets/host/host_agent.sock` |
122
122
  | systemd unit | `cyclo_host_agent.service` |
123
- | sudoers | `/etc/sudoers.d/cyclo_manager` (`cp`, `udevadm` for container setup scripts) |
123
+ | sudoers | `/etc/sudoers.d/cyclo_manager` (`cp`, `udevadm` for container setup scripts; skipped on root-only devices) |
124
124
 
125
125
  The API container reaches it at `/agents/host/host_agent.sock` (see bundled `config.yml`).
126
126
 
@@ -131,6 +131,8 @@ The API container reaches it at `/agents/host/host_agent.sock` (see bundled `con
131
131
 
132
132
  `cyclo_manager up` and `cyclo_manager update` both call `_ensure_host_agent()`, which is idempotent — safe to run after upgrades or user changes.
133
133
 
134
+ Repository scanning uses `CYCLO_HOST_AGENT_WORKSPACE` when set. Otherwise it uses the service user's home directory, except root-only devices with `/data/docker`, where `/data/docker` is used automatically.
135
+
134
136
  ---
135
137
 
136
138
  ## Compose services
@@ -160,7 +162,7 @@ The API reads it as `CONFIG_FILE=/app/config.yml` inside the container.
160
162
 
161
163
  | Key | Description |
162
164
  |-----|-------------|
163
- | **`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`. |
165
+ | **`supported_robot_containers`** | Robot Docker container names that can open the System page (e.g. `ai_worker`, `open_manipulator`). Each must be a key in `sockets` and cannot be `host_agent`. |
164
166
  | **`sockets`** | Map of logical name → agent **Unix socket path as seen inside the API container** (under `/agents/...`). Include robot/service containers and `host_agent`. |
165
167
 
166
168
  s6 **service names** are not listed in config; each in-container agent reports them at runtime.
@@ -168,20 +170,17 @@ s6 **service names** are not listed in config; each in-container agent reports t
168
170
  ### Example (bundled default)
169
171
 
170
172
  ```yaml
171
- robot_container: ai_worker
173
+ supported_robot_containers:
174
+ - ai_worker
175
+ - open_manipulator
172
176
 
173
177
  sockets:
174
178
  ai_worker: "/agents/ai_worker/s6_agent.sock"
179
+ open_manipulator: "/agents/open_manipulator/s6_agent.sock"
175
180
  cyclo_intelligence: "/agents/cyclo_intelligence/s6_agent.sock"
176
181
  host_agent: "/agents/host/host_agent.sock"
177
182
  ```
178
183
 
179
- ### Validation (API startup)
180
-
181
- - At least one socket entry besides `host_agent`
182
- - `robot_container` must exist in `sockets`
183
- - All socket paths must be non-empty strings
184
-
185
184
  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).
186
185
 
187
186
  ---
@@ -38,9 +38,9 @@ For the full monorepo (API source, UI source, dev compose), see the [repository
38
38
 
39
39
  - **Docker** with **Compose v2** (`docker compose` — the legacy `docker-compose` binary alone is not enough)
40
40
  - **Python 3.10+**
41
- - **`sudo`** — required to install the host agent systemd unit, socket directory permissions, and sudoers rules
41
+ - **`sudo`** — required on normal-user hosts to install the host agent systemd unit, socket directory permissions, and sudoers rules
42
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.
43
+ - **Root shell handling** — root is rejected when a normal login user exists. On root-only devices, the host agent is installed as root.
44
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
45
 
46
46
  ---
@@ -106,7 +106,7 @@ The pip package installs a small **FastAPI** server that listens on a **Unix dom
106
106
  |------|------|
107
107
  | Socket | `/var/run/robotis/agent_sockets/host/host_agent.sock` |
108
108
  | systemd unit | `cyclo_host_agent.service` |
109
- | sudoers | `/etc/sudoers.d/cyclo_manager` (`cp`, `udevadm` for container setup scripts) |
109
+ | sudoers | `/etc/sudoers.d/cyclo_manager` (`cp`, `udevadm` for container setup scripts; skipped on root-only devices) |
110
110
 
111
111
  The API container reaches it at `/agents/host/host_agent.sock` (see bundled `config.yml`).
112
112
 
@@ -117,6 +117,8 @@ The API container reaches it at `/agents/host/host_agent.sock` (see bundled `con
117
117
 
118
118
  `cyclo_manager up` and `cyclo_manager update` both call `_ensure_host_agent()`, which is idempotent — safe to run after upgrades or user changes.
119
119
 
120
+ Repository scanning uses `CYCLO_HOST_AGENT_WORKSPACE` when set. Otherwise it uses the service user's home directory, except root-only devices with `/data/docker`, where `/data/docker` is used automatically.
121
+
120
122
  ---
121
123
 
122
124
  ## Compose services
@@ -146,7 +148,7 @@ The API reads it as `CONFIG_FILE=/app/config.yml` inside the container.
146
148
 
147
149
  | Key | Description |
148
150
  |-----|-------------|
149
- | **`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`. |
151
+ | **`supported_robot_containers`** | Robot Docker container names that can open the System page (e.g. `ai_worker`, `open_manipulator`). Each must be a key in `sockets` and cannot be `host_agent`. |
150
152
  | **`sockets`** | Map of logical name → agent **Unix socket path as seen inside the API container** (under `/agents/...`). Include robot/service containers and `host_agent`. |
151
153
 
152
154
  s6 **service names** are not listed in config; each in-container agent reports them at runtime.
@@ -154,20 +156,17 @@ s6 **service names** are not listed in config; each in-container agent reports t
154
156
  ### Example (bundled default)
155
157
 
156
158
  ```yaml
157
- robot_container: ai_worker
159
+ supported_robot_containers:
160
+ - ai_worker
161
+ - open_manipulator
158
162
 
159
163
  sockets:
160
164
  ai_worker: "/agents/ai_worker/s6_agent.sock"
165
+ open_manipulator: "/agents/open_manipulator/s6_agent.sock"
161
166
  cyclo_intelligence: "/agents/cyclo_intelligence/s6_agent.sock"
162
167
  host_agent: "/agents/host/host_agent.sock"
163
168
  ```
164
169
 
165
- ### Validation (API startup)
166
-
167
- - At least one socket entry besides `host_agent`
168
- - `robot_container` must exist in `sockets`
169
- - All socket paths must be non-empty strings
170
-
171
170
  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).
172
171
 
173
172
  ---
@@ -43,14 +43,17 @@ router = APIRouter(prefix='/repos', tags=['repos'])
43
43
  GIT_TIMEOUT = 120.0
44
44
 
45
45
 
46
- def _resolve_home() -> Path:
46
+ def _resolve_workspace() -> Path:
47
+ configured_workspace = os.environ.get('CYCLO_HOST_AGENT_WORKSPACE')
48
+ if configured_workspace:
49
+ return Path(configured_workspace).expanduser()
47
50
  sudo_user = os.environ.get('SUDO_USER')
48
51
  if sudo_user:
49
52
  return Path(f'/home/{sudo_user}')
50
53
  return Path.home()
51
54
 
52
55
 
53
- WORKSPACE_PATH = _resolve_home()
56
+ WORKSPACE_PATH = _resolve_workspace()
54
57
  MANAGED_GITHUB_ORG = 'ROBOTIS-GIT'
55
58
  ALLOWED_UPDATE_BRANCHES = frozenset({'main', 'jazzy'})
56
59
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cyclo-manager
3
- Version: 0.4.0.dev0
3
+ Version: 1.0.0.dev1
4
4
  Summary: cyclo_manager CLI: pip-installable launcher for cyclo_manager server and UI containers. Run 'cyclo_manager up' to start Docker stack.
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.10
@@ -52,9 +52,9 @@ For the full monorepo (API source, UI source, dev compose), see the [repository
52
52
 
53
53
  - **Docker** with **Compose v2** (`docker compose` — the legacy `docker-compose` binary alone is not enough)
54
54
  - **Python 3.10+**
55
- - **`sudo`** — required to install the host agent systemd unit, socket directory permissions, and sudoers rules
55
+ - **`sudo`** — required on normal-user hosts to install the host agent systemd unit, socket directory permissions, and sudoers rules
56
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.
57
+ - **Root shell handling** — root is rejected when a normal login user exists. On root-only devices, the host agent is installed as root.
58
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
59
 
60
60
  ---
@@ -120,7 +120,7 @@ The pip package installs a small **FastAPI** server that listens on a **Unix dom
120
120
  |------|------|
121
121
  | Socket | `/var/run/robotis/agent_sockets/host/host_agent.sock` |
122
122
  | systemd unit | `cyclo_host_agent.service` |
123
- | sudoers | `/etc/sudoers.d/cyclo_manager` (`cp`, `udevadm` for container setup scripts) |
123
+ | sudoers | `/etc/sudoers.d/cyclo_manager` (`cp`, `udevadm` for container setup scripts; skipped on root-only devices) |
124
124
 
125
125
  The API container reaches it at `/agents/host/host_agent.sock` (see bundled `config.yml`).
126
126
 
@@ -131,6 +131,8 @@ The API container reaches it at `/agents/host/host_agent.sock` (see bundled `con
131
131
 
132
132
  `cyclo_manager up` and `cyclo_manager update` both call `_ensure_host_agent()`, which is idempotent — safe to run after upgrades or user changes.
133
133
 
134
+ Repository scanning uses `CYCLO_HOST_AGENT_WORKSPACE` when set. Otherwise it uses the service user's home directory, except root-only devices with `/data/docker`, where `/data/docker` is used automatically.
135
+
134
136
  ---
135
137
 
136
138
  ## Compose services
@@ -160,7 +162,7 @@ The API reads it as `CONFIG_FILE=/app/config.yml` inside the container.
160
162
 
161
163
  | Key | Description |
162
164
  |-----|-------------|
163
- | **`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`. |
165
+ | **`supported_robot_containers`** | Robot Docker container names that can open the System page (e.g. `ai_worker`, `open_manipulator`). Each must be a key in `sockets` and cannot be `host_agent`. |
164
166
  | **`sockets`** | Map of logical name → agent **Unix socket path as seen inside the API container** (under `/agents/...`). Include robot/service containers and `host_agent`. |
165
167
 
166
168
  s6 **service names** are not listed in config; each in-container agent reports them at runtime.
@@ -168,20 +170,17 @@ s6 **service names** are not listed in config; each in-container agent reports t
168
170
  ### Example (bundled default)
169
171
 
170
172
  ```yaml
171
- robot_container: ai_worker
173
+ supported_robot_containers:
174
+ - ai_worker
175
+ - open_manipulator
172
176
 
173
177
  sockets:
174
178
  ai_worker: "/agents/ai_worker/s6_agent.sock"
179
+ open_manipulator: "/agents/open_manipulator/s6_agent.sock"
175
180
  cyclo_intelligence: "/agents/cyclo_intelligence/s6_agent.sock"
176
181
  host_agent: "/agents/host/host_agent.sock"
177
182
  ```
178
183
 
179
- ### Validation (API startup)
180
-
181
- - At least one socket entry besides `host_agent`
182
- - `robot_container` must exist in `sockets`
183
- - All socket paths must be non-empty strings
184
-
185
184
  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).
186
185
 
187
186
  ---
@@ -26,6 +26,7 @@ Launches cyclo_manager server and UI via Docker.
26
26
  import argparse
27
27
  import os
28
28
  from pathlib import Path
29
+ import pwd
29
30
  import shutil
30
31
  import socket
31
32
  import subprocess
@@ -40,6 +41,7 @@ HOST_AGENT_SOCKET_DIR = '/var/run/robotis/agent_sockets/host'
40
41
  COMPOSE_SERVICES_UP = ('cyclo_manager', 'ui')
41
42
  # These get `docker compose create` only (stopped); start from UI or `docker start`.
42
43
  COMPOSE_SERVICES_CREATE_ONLY = ('rmw_zenoh', 'novnc-server')
44
+ DISABLED_LOGIN_SHELLS = {'', '/bin/false', '/sbin/nologin', '/usr/sbin/nologin'}
43
45
 
44
46
 
45
47
  def _docker_dir() -> Path:
@@ -55,6 +57,47 @@ def _packaged_config_path() -> Path:
55
57
  return _config_dir() / 'config.yml'
56
58
 
57
59
 
60
+ def _privileged_cmd(args: list[str]) -> list[str]:
61
+ """Return args prefixed with sudo when the current process is not already root."""
62
+ if os.geteuid() == 0:
63
+ return args
64
+ return ['sudo', *args]
65
+
66
+
67
+ def _user_home(user: str) -> Path:
68
+ """Return a user's home directory from passwd, with a conservative fallback."""
69
+ try:
70
+ return Path(pwd.getpwnam(user).pw_dir)
71
+ except KeyError:
72
+ if user == 'root':
73
+ return Path('/root')
74
+ return Path(f'/home/{user}')
75
+
76
+
77
+ def _has_normal_login_user() -> bool:
78
+ """Return True when the host has a non-root login user."""
79
+ for account in pwd.getpwall():
80
+ if account.pw_name == 'root':
81
+ continue
82
+ if account.pw_uid < 1000 or account.pw_uid >= 65534:
83
+ continue
84
+ if account.pw_shell in DISABLED_LOGIN_SHELLS:
85
+ continue
86
+ return True
87
+ return False
88
+
89
+
90
+ def _host_agent_workspace(user: str, user_home: Path) -> Path:
91
+ """Return the workspace path the host agent should scan for git repositories."""
92
+ configured_workspace = os.environ.get('CYCLO_HOST_AGENT_WORKSPACE')
93
+ if configured_workspace:
94
+ return Path(configured_workspace).expanduser()
95
+ data_docker = Path('/data/docker')
96
+ if user == 'root' and data_docker.is_dir():
97
+ return data_docker
98
+ return user_home
99
+
100
+
58
101
  def _resolve_service_user() -> tuple[str, Path] | None:
59
102
  """
60
103
  Return (username, home) for the host agent systemd service.
@@ -64,9 +107,15 @@ def _resolve_service_user() -> tuple[str, Path] | None:
64
107
  """
65
108
  sudo_user = os.environ.get('SUDO_USER')
66
109
  if sudo_user:
67
- return sudo_user, Path(f'/home/{sudo_user}')
68
- user = os.environ.get('USER') or os.getlogin()
110
+ return sudo_user, _user_home(sudo_user)
111
+ user = os.environ.get('USER') or pwd.getpwuid(os.getuid()).pw_name
69
112
  if user == 'root':
113
+ if not _has_normal_login_user():
114
+ print(
115
+ 'Info: root-only device detected; cyclo_host_agent will run as root.',
116
+ file=sys.stderr,
117
+ )
118
+ return 'root', Path('/root')
70
119
  print(
71
120
  'Error: cyclo_manager up must run as a normal user (e.g. robotis), not root.',
72
121
  file=sys.stderr,
@@ -85,19 +134,19 @@ def _setup_socket_dir(user: str) -> bool:
85
134
  print(f' [1/3] Creating socket directory {HOST_AGENT_SOCKET_DIR}...')
86
135
  try:
87
136
  subprocess.run(
88
- ['sudo', 'mkdir', '-p', HOST_AGENT_SOCKET_DIR],
137
+ _privileged_cmd(['mkdir', '-p', HOST_AGENT_SOCKET_DIR]),
89
138
  check=True,
90
139
  capture_output=True,
91
140
  )
92
141
  print(f' [2/3] Setting ownership to {user}:{user}...')
93
142
  subprocess.run(
94
- ['sudo', 'chown', f'{user}:{user}', HOST_AGENT_SOCKET_DIR],
143
+ _privileged_cmd(['chown', f'{user}:{user}', HOST_AGENT_SOCKET_DIR]),
95
144
  check=True,
96
145
  capture_output=True,
97
146
  )
98
147
  print(' [3/3] Removing stale socket file (if any)...')
99
148
  subprocess.run(
100
- ['sudo', 'rm', '-f', HOST_AGENT_SOCKET],
149
+ _privileged_cmd(['rm', '-f', HOST_AGENT_SOCKET]),
101
150
  check=True,
102
151
  capture_output=True,
103
152
  )
@@ -123,13 +172,13 @@ def _install_sudoers(user: str) -> bool:
123
172
  sudoers_file = '/etc/sudoers.d/cyclo_manager'
124
173
  try:
125
174
  subprocess.run(
126
- ['sudo', 'tee', sudoers_file],
175
+ _privileged_cmd(['tee', sudoers_file]),
127
176
  input=sudoers_content.encode(),
128
177
  capture_output=True,
129
178
  check=True,
130
179
  )
131
180
  subprocess.run(
132
- ['sudo', 'chmod', '440', sudoers_file],
181
+ _privileged_cmd(['chmod', '440', sudoers_file]),
133
182
  check=True,
134
183
  capture_output=True,
135
184
  )
@@ -151,6 +200,7 @@ def _ensure_host_agent() -> int:
151
200
  return 1
152
201
 
153
202
  user, user_home = account
203
+ workspace_path = _host_agent_workspace(user, user_home)
154
204
  agent_exe = shutil.which('cyclo_host_agent')
155
205
  if not agent_exe:
156
206
  print(
@@ -163,7 +213,7 @@ def _ensure_host_agent() -> int:
163
213
  if not _setup_socket_dir(user):
164
214
  return 1
165
215
 
166
- if not _install_sudoers(user):
216
+ if user != 'root' and not _install_sudoers(user):
167
217
  return 1
168
218
 
169
219
  service_content = f"""\
@@ -179,6 +229,7 @@ RuntimeDirectory=robotis/agent_sockets/host
179
229
  RuntimeDirectoryMode=0755
180
230
  ExecStart={agent_exe}
181
231
  Environment=HOME={user_home}
232
+ Environment=CYCLO_HOST_AGENT_WORKSPACE={workspace_path}
182
233
  Restart=always
183
234
  RestartSec=3
184
235
 
@@ -192,22 +243,26 @@ WantedBy=multi-user.target
192
243
  try:
193
244
  print(f' Writing unit file to {service_file}...')
194
245
  subprocess.run(
195
- ['sudo', 'tee', service_file],
246
+ _privileged_cmd(['tee', service_file]),
196
247
  input=service_content.encode(),
197
248
  capture_output=True,
198
249
  check=True,
199
250
  )
200
251
  print(' Running systemctl daemon-reload...')
201
- subprocess.run(['sudo', 'systemctl', 'daemon-reload'], check=True, capture_output=True)
252
+ subprocess.run(
253
+ _privileged_cmd(['systemctl', 'daemon-reload']),
254
+ check=True,
255
+ capture_output=True,
256
+ )
202
257
  print(f' Enabling {HOST_AGENT_SERVICE}...')
203
258
  subprocess.run(
204
- ['sudo', 'systemctl', 'enable', f'{HOST_AGENT_SERVICE}.service'],
259
+ _privileged_cmd(['systemctl', 'enable', f'{HOST_AGENT_SERVICE}.service']),
205
260
  check=True,
206
261
  capture_output=True,
207
262
  )
208
263
  print(f' Starting {HOST_AGENT_SERVICE}...')
209
264
  subprocess.run(
210
- ['sudo', 'systemctl', 'restart', f'{HOST_AGENT_SERVICE}.service'],
265
+ _privileged_cmd(['systemctl', 'restart', f'{HOST_AGENT_SERVICE}.service']),
211
266
  check=True,
212
267
  capture_output=True,
213
268
  )
@@ -217,7 +272,7 @@ WantedBy=multi-user.target
217
272
  print(f'Failed to install host agent service: {e}', file=sys.stderr)
218
273
  return 1
219
274
  except FileNotFoundError:
220
- print('sudo not found. Install the host agent service manually.', file=sys.stderr)
275
+ print('Required command not found. Install the host agent service manually.', file=sys.stderr)
221
276
  return 1
222
277
 
223
278
 
@@ -272,15 +327,21 @@ def _teardown_host_agent() -> None:
272
327
  """Stop and remove the host agent service, unit file, sudoers, and socket dir."""
273
328
  print(f'Tearing down {HOST_AGENT_SERVICE}...')
274
329
  steps = [
275
- (['sudo', 'systemctl', 'stop', f'{HOST_AGENT_SERVICE}.service'], 'stop service'),
276
- (['sudo', 'systemctl', 'disable', f'{HOST_AGENT_SERVICE}.service'], 'disable service'),
277
330
  (
278
- ['sudo', 'rm', '-f', f'/etc/systemd/system/{HOST_AGENT_SERVICE}.service'],
331
+ _privileged_cmd(['systemctl', 'stop', f'{HOST_AGENT_SERVICE}.service']),
332
+ 'stop service',
333
+ ),
334
+ (
335
+ _privileged_cmd(['systemctl', 'disable', f'{HOST_AGENT_SERVICE}.service']),
336
+ 'disable service',
337
+ ),
338
+ (
339
+ _privileged_cmd(['rm', '-f', f'/etc/systemd/system/{HOST_AGENT_SERVICE}.service']),
279
340
  'remove unit file',
280
341
  ),
281
- (['sudo', 'systemctl', 'daemon-reload'], 'daemon-reload'),
282
- (['sudo', 'rm', '-f', '/etc/sudoers.d/cyclo_manager'], 'remove sudoers'),
283
- (['sudo', 'rm', '-rf', HOST_AGENT_SOCKET_DIR], 'remove socket dir'),
342
+ (_privileged_cmd(['systemctl', 'daemon-reload']), 'daemon-reload'),
343
+ (_privileged_cmd(['rm', '-f', '/etc/sudoers.d/cyclo_manager']), 'remove sudoers'),
344
+ (_privileged_cmd(['rm', '-rf', HOST_AGENT_SOCKET_DIR]), 'remove socket dir'),
284
345
  ]
285
346
  for cmd, desc in steps:
286
347
  print(f' {desc}...')
@@ -1,8 +1,11 @@
1
1
  # cyclo_manager Configuration
2
2
 
3
- robot_container: ai_worker
3
+ supported_robot_containers:
4
+ - ai_worker
5
+ - open_manipulator
4
6
 
5
7
  sockets:
6
8
  ai_worker: "/agents/ai_worker/s6_agent.sock"
9
+ open_manipulator: "/agents/open_manipulator/s6_agent.sock"
7
10
  cyclo_intelligence: "/agents/cyclo_intelligence/s6_agent.sock"
8
11
  host_agent: "/agents/host/host_agent.sock"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cyclo-manager"
7
- version = "0.4.0.dev0"
7
+ version = "1.0.0.dev1"
8
8
  description = "cyclo_manager CLI: pip-installable launcher for cyclo_manager server and UI containers. Run 'cyclo_manager up' to start Docker stack."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"