cloudagent-daemon 0.2.2__tar.gz → 0.2.4__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.
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/PKG-INFO +1 -1
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/pyproject.toml +1 -1
- cloudagent_daemon-0.2.4/src/cloudagent_daemon/__init__.py +1 -0
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/src/cloudagent_daemon/cli.py +26 -6
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/src/cloudagent_daemon/config.py +11 -4
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/src/cloudagent_daemon/paths.py +6 -0
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/src/cloudagent_daemon/service.py +76 -9
- cloudagent_daemon-0.2.2/src/cloudagent_daemon/__init__.py +0 -1
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/.gitignore +0 -0
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/LICENSE +0 -0
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/README.md +0 -0
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/src/cloudagent_daemon/__main__.py +0 -0
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/src/cloudagent_daemon/claude_runner.py +0 -0
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/src/cloudagent_daemon/doctor.py +0 -0
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/src/cloudagent_daemon/frame_dispatch.py +0 -0
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/src/cloudagent_daemon/http_outbound.py +0 -0
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/src/cloudagent_daemon/jsonl_logging.py +0 -0
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/src/cloudagent_daemon/login.py +0 -0
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/src/cloudagent_daemon/main_loop.py +0 -0
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/src/cloudagent_daemon/mcp_bridge.py +0 -0
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/src/cloudagent_daemon/metrics.py +0 -0
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/src/cloudagent_daemon/metrics_server.py +0 -0
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/src/cloudagent_daemon/outbound_queue.py +0 -0
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/src/cloudagent_daemon/protocol.py +0 -0
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/src/cloudagent_daemon/runtime_detect.py +0 -0
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/src/cloudagent_daemon/spawn_outbox.py +0 -0
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/src/cloudagent_daemon/spawn_queue.py +0 -0
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/src/cloudagent_daemon/sse_client.py +0 -0
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/src/cloudagent_daemon/transport.py +0 -0
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/src/cloudagent_daemon/unix_socket.py +0 -0
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/src/cloudagent_daemon/workspace.py +0 -0
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/src/cloudagent_daemon/ws_client.py +0 -0
- {cloudagent_daemon-0.2.2 → cloudagent_daemon-0.2.4}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudagent-daemon
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
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.
|
|
7
|
+
version = "0.2.4"
|
|
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.4"
|
|
@@ -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
|
-
|
|
85
|
-
if v not in allowed:
|
|
92
|
+
if v not in VALID_PERMISSION_MODES:
|
|
86
93
|
raise ValueError(
|
|
87
|
-
|
|
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,14 +189,30 @@ def install_macos(
|
|
|
174
189
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
175
190
|
|
|
176
191
|
content = _launchd_plist_content(
|
|
177
|
-
server_url,
|
|
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)
|
|
181
201
|
|
|
182
202
|
uid = os.getuid()
|
|
183
|
-
#
|
|
184
|
-
# service
|
|
203
|
+
# Reinstall-safe (2026-05-19). A plain ``bootstrap`` is a no-op when
|
|
204
|
+
# the service is already loaded — launchctl keeps the OLD in-memory
|
|
205
|
+
# plist and never re-reads the file we just wrote. Without a prior
|
|
206
|
+
# ``bootout`` a re-run of ``install-service`` silently keeps the
|
|
207
|
+
# stale definition (e.g. a changed CLOUDAGENT_DAEMON_PERMISSION_MODE
|
|
208
|
+
# never takes effect). So: bootout first (unload any loaded
|
|
209
|
+
# instance), then bootstrap genuinely loads the fresh plist.
|
|
210
|
+
# ``bootout`` exits non-zero when nothing is loaded (first install)
|
|
211
|
+
# — ``check=False`` swallows that, making install == reinstall.
|
|
212
|
+
subprocess.run(
|
|
213
|
+
["launchctl", "bootout", f"gui/{uid}/{label}"],
|
|
214
|
+
check=False,
|
|
215
|
+
)
|
|
185
216
|
subprocess.run(
|
|
186
217
|
["launchctl", "bootstrap", f"gui/{uid}", str(plist_path)],
|
|
187
218
|
check=False,
|
|
@@ -223,6 +254,7 @@ def _systemd_unit_content(
|
|
|
223
254
|
insecure: bool,
|
|
224
255
|
workspace_root: str | None = None,
|
|
225
256
|
profile: str | None = None,
|
|
257
|
+
permission_mode: str = "default",
|
|
226
258
|
) -> str:
|
|
227
259
|
profile_name = resolve_profile(profile)
|
|
228
260
|
binary = _binary_path()
|
|
@@ -252,6 +284,13 @@ def _systemd_unit_content(
|
|
|
252
284
|
else ""
|
|
253
285
|
)
|
|
254
286
|
|
|
287
|
+
# 2026-05-19 — bake the permission mode into the unit when non-default.
|
|
288
|
+
permission_env_line = (
|
|
289
|
+
f"Environment={PERMISSION_MODE_ENV}={permission_mode}\n"
|
|
290
|
+
if permission_mode and permission_mode != "default"
|
|
291
|
+
else ""
|
|
292
|
+
)
|
|
293
|
+
|
|
255
294
|
return f"""[Unit]
|
|
256
295
|
Description=cloudagent local daemon
|
|
257
296
|
After=network-online.target
|
|
@@ -266,7 +305,7 @@ Environment=NO_PROXY=*
|
|
|
266
305
|
Environment=PATH={daemon_path}
|
|
267
306
|
# Force line-buffered stdout — see launchd plist comment for rationale.
|
|
268
307
|
Environment=PYTHONUNBUFFERED=1
|
|
269
|
-
{profile_env_line}
|
|
308
|
+
{profile_env_line}{permission_env_line}
|
|
270
309
|
[Install]
|
|
271
310
|
WantedBy=default.target
|
|
272
311
|
"""
|
|
@@ -278,6 +317,7 @@ def install_linux(
|
|
|
278
317
|
insecure: bool,
|
|
279
318
|
workspace_root: str | None = None,
|
|
280
319
|
profile: str | None = None,
|
|
320
|
+
permission_mode: str = "default",
|
|
281
321
|
) -> Path:
|
|
282
322
|
profile_name = resolve_profile(profile)
|
|
283
323
|
unit_name = profile_systemd_unit_name(profile_name)
|
|
@@ -285,12 +325,28 @@ def install_linux(
|
|
|
285
325
|
unit_path.parent.mkdir(parents=True, exist_ok=True)
|
|
286
326
|
unit_path.write_text(
|
|
287
327
|
_systemd_unit_content(
|
|
288
|
-
server_url,
|
|
328
|
+
server_url,
|
|
329
|
+
api_key,
|
|
330
|
+
insecure,
|
|
331
|
+
workspace_root,
|
|
332
|
+
profile=profile_name,
|
|
333
|
+
permission_mode=permission_mode,
|
|
289
334
|
)
|
|
290
335
|
)
|
|
291
336
|
subprocess.run(["systemctl", "--user", "daemon-reload"], check=False)
|
|
292
337
|
subprocess.run(
|
|
293
|
-
["systemctl", "--user", "enable",
|
|
338
|
+
["systemctl", "--user", "enable", unit_name],
|
|
339
|
+
check=False,
|
|
340
|
+
)
|
|
341
|
+
# Reinstall-safe (2026-05-19): ``restart`` (not ``enable --now`` /
|
|
342
|
+
# ``start``) so a re-run of ``install-service`` genuinely applies
|
|
343
|
+
# the freshly written unit file — ``start`` is a no-op on an
|
|
344
|
+
# already-running service, leaving the daemon on the stale config.
|
|
345
|
+
# ``daemon-reload`` above re-read the unit; ``restart`` makes the
|
|
346
|
+
# running process pick it up. ``restart`` also starts a stopped
|
|
347
|
+
# service, so this covers first-install too.
|
|
348
|
+
subprocess.run(
|
|
349
|
+
["systemctl", "--user", "restart", unit_name],
|
|
294
350
|
check=False,
|
|
295
351
|
)
|
|
296
352
|
return unit_path
|
|
@@ -320,14 +376,25 @@ def install_service(
|
|
|
320
376
|
insecure: bool = False,
|
|
321
377
|
workspace_root: str | None = None,
|
|
322
378
|
profile: str | None = None,
|
|
379
|
+
permission_mode: str = "default",
|
|
323
380
|
) -> Path:
|
|
324
381
|
plat = detect_platform()
|
|
325
382
|
if plat == "macos":
|
|
326
383
|
return install_macos(
|
|
327
|
-
server_url,
|
|
384
|
+
server_url,
|
|
385
|
+
api_key,
|
|
386
|
+
insecure,
|
|
387
|
+
workspace_root,
|
|
388
|
+
profile=profile,
|
|
389
|
+
permission_mode=permission_mode,
|
|
328
390
|
)
|
|
329
391
|
return install_linux(
|
|
330
|
-
server_url,
|
|
392
|
+
server_url,
|
|
393
|
+
api_key,
|
|
394
|
+
insecure,
|
|
395
|
+
workspace_root,
|
|
396
|
+
profile=profile,
|
|
397
|
+
permission_mode=permission_mode,
|
|
331
398
|
)
|
|
332
399
|
|
|
333
400
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|