cloudagent-daemon 0.2.2__tar.gz → 0.2.3__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. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/PKG-INFO +1 -1
  2. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/pyproject.toml +1 -1
  3. cloudagent_daemon-0.2.3/src/cloudagent_daemon/__init__.py +1 -0
  4. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/src/cloudagent_daemon/cli.py +26 -6
  5. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/src/cloudagent_daemon/config.py +11 -4
  6. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/src/cloudagent_daemon/paths.py +6 -0
  7. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/src/cloudagent_daemon/service.py +51 -6
  8. cloudagent_daemon-0.2.2/src/cloudagent_daemon/__init__.py +0 -1
  9. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/.gitignore +0 -0
  10. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/LICENSE +0 -0
  11. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/README.md +0 -0
  12. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/src/cloudagent_daemon/__main__.py +0 -0
  13. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/src/cloudagent_daemon/claude_runner.py +0 -0
  14. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/src/cloudagent_daemon/doctor.py +0 -0
  15. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/src/cloudagent_daemon/frame_dispatch.py +0 -0
  16. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/src/cloudagent_daemon/http_outbound.py +0 -0
  17. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/src/cloudagent_daemon/jsonl_logging.py +0 -0
  18. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/src/cloudagent_daemon/login.py +0 -0
  19. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/src/cloudagent_daemon/main_loop.py +0 -0
  20. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/src/cloudagent_daemon/mcp_bridge.py +0 -0
  21. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/src/cloudagent_daemon/metrics.py +0 -0
  22. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/src/cloudagent_daemon/metrics_server.py +0 -0
  23. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/src/cloudagent_daemon/outbound_queue.py +0 -0
  24. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/src/cloudagent_daemon/protocol.py +0 -0
  25. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/src/cloudagent_daemon/runtime_detect.py +0 -0
  26. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/src/cloudagent_daemon/spawn_outbox.py +0 -0
  27. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/src/cloudagent_daemon/spawn_queue.py +0 -0
  28. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/src/cloudagent_daemon/sse_client.py +0 -0
  29. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/src/cloudagent_daemon/transport.py +0 -0
  30. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/src/cloudagent_daemon/unix_socket.py +0 -0
  31. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/src/cloudagent_daemon/workspace.py +0 -0
  32. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/src/cloudagent_daemon/ws_client.py +0 -0
  33. {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.3}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudagent-daemon
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Local daemon for cloudagent — bridges Claude Code subprocesses to the cloudagent backend over WebSocket.
5
5
  Project-URL: Homepage, https://cloudagent.chat
6
6
  Project-URL: Repository, https://codeup.aliyun.com/69bd0a4d29ad98af4065ce71/cloudagent
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cloudagent-daemon"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "Local daemon for cloudagent — bridges Claude Code subprocesses to the cloudagent backend over WebSocket."
9
9
  authors = [{name = "chaoyin", email = "tudoucyx@gmail.com"}]
10
10
  readme = "README.md"
@@ -0,0 +1 @@
1
+ __version__ = "0.2.3"
@@ -74,11 +74,6 @@ def _resolve_cli_profile(profile: str | None) -> str:
74
74
  raise typer.Exit(2)
75
75
 
76
76
 
77
- #: Env var that overrides ``DaemonConfig.claude_permission_mode``. The
78
- #: only permission knob available to one-shot mode (no config.json).
79
- PERMISSION_MODE_ENV = "CLOUDAGENT_DAEMON_PERMISSION_MODE"
80
-
81
-
82
77
  def _apply_permission_mode_override(cfg): # type: ignore[no-untyped-def]
83
78
  """Return ``cfg`` with ``claude_permission_mode`` replaced by the
84
79
  ``CLOUDAGENT_DAEMON_PERMISSION_MODE`` env var when it is set.
@@ -88,6 +83,7 @@ def _apply_permission_mode_override(cfg): # type: ignore[no-untyped-def]
88
83
  When the env var is unset / empty, ``cfg`` is returned unchanged.
89
84
  """
90
85
  from .config import DaemonConfig
86
+ from .paths import PERMISSION_MODE_ENV
91
87
 
92
88
  mode = os.environ.get(PERMISSION_MODE_ENV)
93
89
  if not mode:
@@ -645,6 +641,19 @@ def install_service_cmd(
645
641
  ),
646
642
  ),
647
643
  profile: str | None = typer.Option(None, "--profile", help=_PROFILE_HELP),
644
+ permission_mode: str = typer.Option(
645
+ "default",
646
+ "--permission-mode",
647
+ help=(
648
+ "Claude CLI permission mode for daemon-spawned staff agents. "
649
+ "'default' — only the cloudagent MCP toolset runs unattended "
650
+ "(staff cannot touch the filesystem). 'acceptEdits' — file "
651
+ "edits auto-accepted within the agent's working directory "
652
+ "(Bash still gates). 'bypassPermissions' — every tool runs "
653
+ "unattended (needed for staff that run git/build via Bash). "
654
+ "Baked into the service unit as CLOUDAGENT_DAEMON_PERMISSION_MODE."
655
+ ),
656
+ ),
648
657
  ) -> None:
649
658
  """Install daemon as a launchd (macOS) or systemd (Linux) user service.
650
659
 
@@ -655,7 +664,7 @@ def install_service_cmd(
655
664
  macOS (named profile fr): tail -F ~/.cloudagent/daemon/profiles/fr/logs/launchd.{out,err}.log
656
665
  Linux: journalctl --user -u cloudagent-daemon[-<profile>] -f
657
666
  """
658
- from .config import DaemonState
667
+ from .config import VALID_PERMISSION_MODES, DaemonState
659
668
  from .paths import (
660
669
  DEFAULT_PROFILE,
661
670
  profile_state_path,
@@ -666,6 +675,14 @@ def install_service_cmd(
666
675
 
667
676
  profile_name = _resolve_cli_profile(profile)
668
677
 
678
+ if permission_mode not in VALID_PERMISSION_MODES:
679
+ typer.echo(
680
+ f"Error: --permission-mode must be one of "
681
+ f"{sorted(VALID_PERMISSION_MODES)}, got {permission_mode!r}",
682
+ err=True,
683
+ )
684
+ raise typer.Exit(2)
685
+
669
686
  # Task #115 (2026-05-12) — same resolve sequence the ``start``
670
687
  # command uses (tilde-expand → resolve → mkdir). We also persist
671
688
  # to state.json so a later ``cloudagent-daemon start`` (no flag)
@@ -717,6 +734,7 @@ def install_service_cmd(
717
734
  insecure=insecure,
718
735
  workspace_root=resolved_workspace_root,
719
736
  profile=profile_name,
737
+ permission_mode=permission_mode,
720
738
  )
721
739
  except RuntimeError as exc:
722
740
  typer.echo(f"Error: {exc}", err=True)
@@ -724,6 +742,8 @@ def install_service_cmd(
724
742
  typer.echo(f"✓ Installed {plat} service unit: {path}")
725
743
  if profile_name != DEFAULT_PROFILE:
726
744
  typer.echo(f" profile: {profile_name}")
745
+ if permission_mode != "default":
746
+ typer.echo(f" permission mode: {permission_mode}")
727
747
  typer.echo("Daemon will auto-restart on crash and start at login.")
728
748
 
729
749
 
@@ -25,6 +25,14 @@ from .paths import (
25
25
 
26
26
  _LRU_CAP = 1024
27
27
 
28
+ # Accepted values for ``DaemonConfig.claude_permission_mode`` /
29
+ # ``--permission-mode`` / ``CLOUDAGENT_DAEMON_PERMISSION_MODE``. Single
30
+ # source of truth — the field_validator and cli's install-service both
31
+ # check against this.
32
+ VALID_PERMISSION_MODES = frozenset(
33
+ {"default", "acceptEdits", "bypassPermissions"}
34
+ )
35
+
28
36
 
29
37
  def _atomic_write_json(path: Path, data: dict) -> None:
30
38
  """Write *data* as JSON to *path* atomically with fsync and mode 0o600."""
@@ -81,11 +89,10 @@ class DaemonConfig(BaseModel):
81
89
  @field_validator("claude_permission_mode")
82
90
  @classmethod
83
91
  def _validate_permission_mode(cls, v: str) -> str:
84
- allowed = {"default", "acceptEdits", "bypassPermissions"}
85
- if v not in allowed:
92
+ if v not in VALID_PERMISSION_MODES:
86
93
  raise ValueError(
87
- f"claude_permission_mode must be one of {sorted(allowed)}, "
88
- f"got {v!r}"
94
+ "claude_permission_mode must be one of "
95
+ f"{sorted(VALID_PERMISSION_MODES)}, got {v!r}"
89
96
  )
90
97
  return v
91
98
 
@@ -46,6 +46,12 @@ _PROFILE_NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,30}$")
46
46
  # ``agent unregister`` etc.
47
47
  ENV_VAR = "CLOUDAGENT_DAEMON_PROFILE"
48
48
 
49
+ # Env var that overrides DaemonConfig.claude_permission_mode. Surfaced
50
+ # into the daemon process by ``install-service`` (baked into the
51
+ # launchd plist / systemd unit) and read by ``cli.start``. The only
52
+ # permission knob available to one-shot mode (no config.json).
53
+ PERMISSION_MODE_ENV = "CLOUDAGENT_DAEMON_PERMISSION_MODE"
54
+
49
55
 
50
56
  def validate_profile_name(name: str) -> str:
51
57
  """Return *name* if it is safe to use; raise ``ValueError`` otherwise.
@@ -22,6 +22,7 @@ from pathlib import Path
22
22
  from .paths import (
23
23
  DEFAULT_PROFILE,
24
24
  ENV_VAR,
25
+ PERMISSION_MODE_ENV,
25
26
  profile_launchd_label,
26
27
  profile_log_dir,
27
28
  profile_systemd_unit_name,
@@ -76,6 +77,7 @@ def _launchd_plist_content(
76
77
  insecure: bool,
77
78
  workspace_root: str | None = None,
78
79
  profile: str | None = None,
80
+ permission_mode: str = "default",
79
81
  ) -> str:
80
82
  """Render the plist XML.
81
83
 
@@ -119,6 +121,18 @@ def _launchd_plist_content(
119
121
  f" <string>{profile_name}</string>\n"
120
122
  )
121
123
 
124
+ # 2026-05-19 — when a non-default permission mode is requested,
125
+ # bake CLOUDAGENT_DAEMON_PERMISSION_MODE into the plist so the
126
+ # daemon process inherits it. "default" omits the key entirely so
127
+ # the rendered plist stays byte-for-byte identical to the legacy
128
+ # output (zero-migration guarantee).
129
+ permission_env = ""
130
+ if permission_mode and permission_mode != "default":
131
+ permission_env = (
132
+ f" <key>{PERMISSION_MODE_ENV}</key>\n"
133
+ f" <string>{permission_mode}</string>\n"
134
+ )
135
+
122
136
  return f"""<?xml version="1.0" encoding="UTF-8"?>
123
137
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTD/PropertyList-1.0.dtd">
124
138
  <plist version="1.0">
@@ -152,7 +166,7 @@ def _launchd_plist_content(
152
166
  instead of sitting in an 8 KB buffer until the process exits. -->
153
167
  <key>PYTHONUNBUFFERED</key>
154
168
  <string>1</string>
155
- {profile_env} </dict>
169
+ {profile_env}{permission_env} </dict>
156
170
  </dict>
157
171
  </plist>
158
172
  """
@@ -164,6 +178,7 @@ def install_macos(
164
178
  insecure: bool,
165
179
  workspace_root: str | None = None,
166
180
  profile: str | None = None,
181
+ permission_mode: str = "default",
167
182
  ) -> Path:
168
183
  """Install + load + start the launchd user agent for *profile*."""
169
184
  profile_name = resolve_profile(profile)
@@ -174,7 +189,12 @@ def install_macos(
174
189
  log_dir.mkdir(parents=True, exist_ok=True)
175
190
 
176
191
  content = _launchd_plist_content(
177
- server_url, api_key, insecure, workspace_root, profile=profile_name
192
+ server_url,
193
+ api_key,
194
+ insecure,
195
+ workspace_root,
196
+ profile=profile_name,
197
+ permission_mode=permission_mode,
178
198
  )
179
199
  plist_path.write_text(content)
180
200
  plist_path.chmod(0o600)
@@ -223,6 +243,7 @@ def _systemd_unit_content(
223
243
  insecure: bool,
224
244
  workspace_root: str | None = None,
225
245
  profile: str | None = None,
246
+ permission_mode: str = "default",
226
247
  ) -> str:
227
248
  profile_name = resolve_profile(profile)
228
249
  binary = _binary_path()
@@ -252,6 +273,13 @@ def _systemd_unit_content(
252
273
  else ""
253
274
  )
254
275
 
276
+ # 2026-05-19 — bake the permission mode into the unit when non-default.
277
+ permission_env_line = (
278
+ f"Environment={PERMISSION_MODE_ENV}={permission_mode}\n"
279
+ if permission_mode and permission_mode != "default"
280
+ else ""
281
+ )
282
+
255
283
  return f"""[Unit]
256
284
  Description=cloudagent local daemon
257
285
  After=network-online.target
@@ -266,7 +294,7 @@ Environment=NO_PROXY=*
266
294
  Environment=PATH={daemon_path}
267
295
  # Force line-buffered stdout — see launchd plist comment for rationale.
268
296
  Environment=PYTHONUNBUFFERED=1
269
- {profile_env_line}
297
+ {profile_env_line}{permission_env_line}
270
298
  [Install]
271
299
  WantedBy=default.target
272
300
  """
@@ -278,6 +306,7 @@ def install_linux(
278
306
  insecure: bool,
279
307
  workspace_root: str | None = None,
280
308
  profile: str | None = None,
309
+ permission_mode: str = "default",
281
310
  ) -> Path:
282
311
  profile_name = resolve_profile(profile)
283
312
  unit_name = profile_systemd_unit_name(profile_name)
@@ -285,7 +314,12 @@ def install_linux(
285
314
  unit_path.parent.mkdir(parents=True, exist_ok=True)
286
315
  unit_path.write_text(
287
316
  _systemd_unit_content(
288
- server_url, api_key, insecure, workspace_root, profile=profile_name
317
+ server_url,
318
+ api_key,
319
+ insecure,
320
+ workspace_root,
321
+ profile=profile_name,
322
+ permission_mode=permission_mode,
289
323
  )
290
324
  )
291
325
  subprocess.run(["systemctl", "--user", "daemon-reload"], check=False)
@@ -320,14 +354,25 @@ def install_service(
320
354
  insecure: bool = False,
321
355
  workspace_root: str | None = None,
322
356
  profile: str | None = None,
357
+ permission_mode: str = "default",
323
358
  ) -> Path:
324
359
  plat = detect_platform()
325
360
  if plat == "macos":
326
361
  return install_macos(
327
- server_url, api_key, insecure, workspace_root, profile=profile
362
+ server_url,
363
+ api_key,
364
+ insecure,
365
+ workspace_root,
366
+ profile=profile,
367
+ permission_mode=permission_mode,
328
368
  )
329
369
  return install_linux(
330
- server_url, api_key, insecure, workspace_root, profile=profile
370
+ server_url,
371
+ api_key,
372
+ insecure,
373
+ workspace_root,
374
+ profile=profile,
375
+ permission_mode=permission_mode,
331
376
  )
332
377
 
333
378
 
@@ -1 +0,0 @@
1
- __version__ = "0.2.2"