cloudagent-daemon 0.5.0__tar.gz → 0.5.2__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.5.0 → cloudagent_daemon-0.5.2}/CHANGELOG.md +31 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/PKG-INFO +1 -1
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/pyproject.toml +1 -1
- cloudagent_daemon-0.5.2/src/cloudagent_daemon/__init__.py +1 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/claude_runner.py +135 -22
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/cli.py +11 -1
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/main_loop.py +24 -3
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/mcp_bridge.py +124 -17
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/outbound_queue.py +3 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/paths.py +3 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/protocol.py +10 -1
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/integration/test_mcp_bridge_round_trip.py +91 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/integration/test_spawn_mcp_bridge_e2e.py +26 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_claude_runner.py +87 -12
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_cli_smoke.py +1 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_mcp_bridge_agent_retrieval.py +14 -2
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_mcp_bridge_send_message.py +5 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_outbound_queue.py +11 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_paths.py +7 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/uv.lock +1 -1
- cloudagent_daemon-0.5.0/src/cloudagent_daemon/__init__.py +0 -1
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/.gitignore +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/LICENSE +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/README.md +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/scripts/release.sh +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/__main__.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/agent_download.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/agent_upload.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/agent_upload_denylist.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/auth_preflight.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/config.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/context_fetch.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/delegation_report.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/doctor.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/frame_dispatch.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/http_outbound.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/injection_state.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/jsonl_logging.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/login.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/metrics.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/metrics_server.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/qoder_credentials.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/readonly_bash_guard.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/runtime_config.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/runtime_detect.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/service.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/spawn_outbox.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/spawn_queue.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/sse_client.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/transport.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/unix_socket.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/workspace.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/src/cloudagent_daemon/ws_client.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/integration/__init__.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/integration/_fake_sse_server.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/integration/conftest.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/integration/test_e2e_deliver_to_spawn.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/integration/test_e2e_sse.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/integration/test_full_e2e.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/integration/test_one_shot_cli.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/integration/test_sse_handshake.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/integration/test_sse_watchdog.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/integration/test_ws_handshake.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/integration/test_ws_reconnect.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/conftest.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_agent_download.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_agent_upload.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_auth_preflight.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_claude_runner_codex_contract.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_claude_runner_routing_contract.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_claude_runner_w3.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_cli_multi_profile.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_config.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_context_fetch.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_delegation_report.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_doctor.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_frame_dispatch.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_http_outbound.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_injection_state.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_jsonl_logging.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_login.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_main_loop_assign.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_main_loop_preflight.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_metrics.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_metrics_server.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_multi_tool_wake.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_outbox_stuck_watchdog.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_packaging_mcp_pin.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_protocol.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_qoder_credentials.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_readonly_bash_guard.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_runtime_detect.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_service.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_service_multi_profile.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_spawn_outbox.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_spawn_queue.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_sse_active_watchdog.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_start_guard.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_status_stop.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_workspace.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_workspace_root_task113.py +0 -0
- {cloudagent_daemon-0.5.0 → cloudagent_daemon-0.5.2}/tests/unit/test_ws_client_insecure.py +0 -0
|
@@ -4,6 +4,37 @@ All notable changes to the local daemon. Format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/); versions are
|
|
5
5
|
[PEP 440](https://peps.python.org/pep-0440/). Release with `scripts/release.sh`.
|
|
6
6
|
|
|
7
|
+
## [0.5.1] — 2026-08-20
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Qoder engine now supports cross-owner `read_analyze` (only-read remote
|
|
11
|
+
command). Previously the daemon fail-closed refused `read_analyze` on qoder
|
|
12
|
+
because qodercli's tool-restriction was never verified end-to-end (#106).
|
|
13
|
+
This release ships a proven limited-tool surface:
|
|
14
|
+
- qodercli launched with `--setting-sources '' --permission-mode
|
|
15
|
+
bypass_permissions --tools Read Glob Grep --no-session-persistence
|
|
16
|
+
--strict-mcp-config` and a dedicated read_analyze MCP config; no `--resume`
|
|
17
|
+
of the owner lane.
|
|
18
|
+
- `mcp-bridge --tool-profile read_analyze` applies a POSITIVE allowlist at
|
|
19
|
+
MCP `tools/list` time: only 13 membership-gated query tools remain; every
|
|
20
|
+
write/mutation tool (`platform_send`, `room_post`, `send_message`,
|
|
21
|
+
upload/download, reminder/task mutation, react, login, chief tools) is
|
|
22
|
+
removed. Unknown profile → `ValueError` (fail-closed).
|
|
23
|
+
- Final analysis is delivered by the daemon-controlled fallback to the
|
|
24
|
+
backend-supplied origin conversation only; the model cannot choose a
|
|
25
|
+
destination.
|
|
26
|
+
Verified by real cross-owner E2E (human non-owner → qoder agent, induced
|
|
27
|
+
write/command/`platform_send` all blocked, read-only result returned to the
|
|
28
|
+
origin channel). Confidentiality scope is intentionally the target agent's
|
|
29
|
+
own read ACL (same-org cross-owner read/analysis is the product contract).
|
|
30
|
+
|
|
31
|
+
### Known debt
|
|
32
|
+
- mcp-bridge per-spawn token-file is still not wired end-to-end; bridge/daemon
|
|
33
|
+
IPC trust rests on the same-user 0600 unix socket, so a malicious native
|
|
34
|
+
process under the same OS user could forge an agent_id. Pre-existing and
|
|
35
|
+
shared by all engines/run-modes; tracked separately (#110), not introduced
|
|
36
|
+
by this release.
|
|
37
|
+
|
|
7
38
|
## [0.5.0] — 2026-08-19
|
|
8
39
|
|
|
9
40
|
First release since 0.4.1 (2026-07-22). Everything below shipped to internal
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cloudagent-daemon
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: Local daemon for cloudagent — bridges Claude Code, Codex, and Qoder CLI agents to the cloudagent backend.
|
|
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.5.
|
|
7
|
+
version = "0.5.2"
|
|
8
8
|
description = "Local daemon for cloudagent — bridges Claude Code, Codex, and Qoder CLI agents to the cloudagent backend."
|
|
9
9
|
authors = [{name = "chaoyin", email = "tudoucyx@gmail.com"}]
|
|
10
10
|
readme = "README.md"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.2"
|
|
@@ -391,6 +391,7 @@ class ClaudeRunner:
|
|
|
391
391
|
partial_publisher: PartialPublisher | None = None,
|
|
392
392
|
event_publisher: EventPublisher | None = None,
|
|
393
393
|
agent_to_session: "dict[str, str | None] | None" = None,
|
|
394
|
+
agent_to_delivery: "dict[str, str | None] | None" = None,
|
|
394
395
|
claude_permission_mode: str = "bypassPermissions",
|
|
395
396
|
runtime_binaries: dict[str, str] | None = None,
|
|
396
397
|
http: object | None = None,
|
|
@@ -440,6 +441,10 @@ class ClaudeRunner:
|
|
|
440
441
|
# unix socket ``get_current_session`` op instead of reading a file.
|
|
441
442
|
# None means feature disabled (legacy / test path).
|
|
442
443
|
self._agent_to_session: dict[str, str | None] | None = agent_to_session
|
|
444
|
+
# #112 — opaque backend delivery provenance for the currently running
|
|
445
|
+
# turn. The MCP bridge queries this map through daemon IPC and adds it
|
|
446
|
+
# to outbound calls without exposing a forgeable tool parameter.
|
|
447
|
+
self._agent_to_delivery: dict[str, str | None] | None = agent_to_delivery
|
|
443
448
|
|
|
444
449
|
# ------------------------------------------------------------------
|
|
445
450
|
# Public API
|
|
@@ -618,6 +623,14 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
|
|
|
618
623
|
# reading the file. Both paths coexist for backward compat.
|
|
619
624
|
if self._agent_to_session is not None:
|
|
620
625
|
self._agent_to_session[agent_id] = _effective_session_id
|
|
626
|
+
current_delivery_id = payload.get("delivery_id")
|
|
627
|
+
_effective_delivery_id = (
|
|
628
|
+
current_delivery_id
|
|
629
|
+
if isinstance(current_delivery_id, str) and current_delivery_id
|
|
630
|
+
else None
|
|
631
|
+
)
|
|
632
|
+
if self._agent_to_delivery is not None:
|
|
633
|
+
self._agent_to_delivery[agent_id] = _effective_delivery_id
|
|
621
634
|
|
|
622
635
|
# M7 W3 — localagent company-context injection. BEFORE spawning
|
|
623
636
|
# claude, if this deliver belongs to a task whose context has not
|
|
@@ -709,6 +722,8 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
|
|
|
709
722
|
# (c) IPC architecture upgrade — clear from memory map.
|
|
710
723
|
if self._agent_to_session is not None:
|
|
711
724
|
self._agent_to_session.pop(agent_id, None)
|
|
725
|
+
if self._agent_to_delivery is not None:
|
|
726
|
+
self._agent_to_delivery.pop(agent_id, None)
|
|
712
727
|
|
|
713
728
|
captured_session = attempt.captured_session_id
|
|
714
729
|
rc = attempt.exit_code
|
|
@@ -830,6 +845,11 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
|
|
|
830
845
|
"content": attempt.final_result_text,
|
|
831
846
|
"conversation_id": platform_conv_id,
|
|
832
847
|
"acknowledge_new_messages": True,
|
|
848
|
+
**(
|
|
849
|
+
{"delivery_id": _effective_delivery_id}
|
|
850
|
+
if _effective_delivery_id
|
|
851
|
+
else {}
|
|
852
|
+
),
|
|
833
853
|
},
|
|
834
854
|
timeout=15.0,
|
|
835
855
|
)
|
|
@@ -876,6 +896,8 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
|
|
|
876
896
|
payload, attempt.final_result_text
|
|
877
897
|
)
|
|
878
898
|
)
|
|
899
|
+
if _effective_delivery_id:
|
|
900
|
+
fallback_args["delivery_id"] = _effective_delivery_id
|
|
879
901
|
await self.outbound.send_tool_call(
|
|
880
902
|
agent_id=agent_id,
|
|
881
903
|
session_id=fallback_session,
|
|
@@ -1006,7 +1028,14 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
|
|
|
1006
1028
|
agent_id=agent_id,
|
|
1007
1029
|
session_id=fallback_session,
|
|
1008
1030
|
tool_name="send_message",
|
|
1009
|
-
args=
|
|
1031
|
+
args={
|
|
1032
|
+
**self._fallback_reply_args(payload, abort_body),
|
|
1033
|
+
**(
|
|
1034
|
+
{"delivery_id": _effective_delivery_id}
|
|
1035
|
+
if _effective_delivery_id
|
|
1036
|
+
else {}
|
|
1037
|
+
),
|
|
1038
|
+
},
|
|
1010
1039
|
timeout=15.0,
|
|
1011
1040
|
)
|
|
1012
1041
|
try:
|
|
@@ -1026,15 +1055,20 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
|
|
|
1026
1055
|
reason,
|
|
1027
1056
|
)
|
|
1028
1057
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
exit_code=rc,
|
|
1033
|
-
conversation_id=(
|
|
1058
|
+
final_status_kwargs: dict[str, Any] = {
|
|
1059
|
+
"exit_code": rc,
|
|
1060
|
+
"conversation_id": (
|
|
1034
1061
|
str(payload["conversation_id"])
|
|
1035
1062
|
if payload.get("conversation_id")
|
|
1036
1063
|
else None
|
|
1037
1064
|
),
|
|
1065
|
+
}
|
|
1066
|
+
if _effective_delivery_id:
|
|
1067
|
+
final_status_kwargs["delivery_id"] = _effective_delivery_id
|
|
1068
|
+
await self.outbound.send_agent_status(
|
|
1069
|
+
agent_id,
|
|
1070
|
+
final_status,
|
|
1071
|
+
**final_status_kwargs,
|
|
1038
1072
|
)
|
|
1039
1073
|
|
|
1040
1074
|
wake_secs = time.monotonic() - wake_start
|
|
@@ -1382,6 +1416,9 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
|
|
|
1382
1416
|
wake_prompt = self._codex_stdin_preamble(
|
|
1383
1417
|
agent_id, append_system_prompt,
|
|
1384
1418
|
read_analyze=run_mode == "read_analyze",
|
|
1419
|
+
daemon_managed_reply=(
|
|
1420
|
+
runtime_engine == "qoder" and run_mode == "read_analyze"
|
|
1421
|
+
),
|
|
1385
1422
|
) + wake_prompt
|
|
1386
1423
|
try:
|
|
1387
1424
|
assert proc.stdin is not None # for type checkers
|
|
@@ -1400,14 +1437,20 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
|
|
|
1400
1437
|
await self.outbound.send_ack(
|
|
1401
1438
|
seq=None, message_id=message_id, status="started"
|
|
1402
1439
|
)
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
"running",
|
|
1406
|
-
conversation_id=(
|
|
1440
|
+
running_status_kwargs: dict[str, Any] = {
|
|
1441
|
+
"conversation_id": (
|
|
1407
1442
|
str(payload["conversation_id"])
|
|
1408
1443
|
if payload.get("conversation_id")
|
|
1409
1444
|
else None
|
|
1410
|
-
)
|
|
1445
|
+
)
|
|
1446
|
+
}
|
|
1447
|
+
delivery_id = payload.get("delivery_id")
|
|
1448
|
+
if isinstance(delivery_id, str) and delivery_id:
|
|
1449
|
+
running_status_kwargs["delivery_id"] = delivery_id
|
|
1450
|
+
await self.outbound.send_agent_status(
|
|
1451
|
+
agent_id,
|
|
1452
|
+
"running",
|
|
1453
|
+
**running_status_kwargs,
|
|
1411
1454
|
)
|
|
1412
1455
|
|
|
1413
1456
|
# 1k74 — token-level streaming. Forward the deliver's session_id
|
|
@@ -1519,6 +1562,19 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
|
|
|
1519
1562
|
"mcp__cloudagent__platform_task_status",
|
|
1520
1563
|
)
|
|
1521
1564
|
|
|
1565
|
+
# #107 — qoder read_analyze is enforced with a positive allowlist, not a
|
|
1566
|
+
# denylist. qodercli 1.1.26 live probes showed that ``--tools`` removes
|
|
1567
|
+
# every other built-in capability from the init event even under
|
|
1568
|
+
# ``bypass_permissions``; the latter is still required so the injected
|
|
1569
|
+
# cloudagent MCP server can post the analysis result in headless mode.
|
|
1570
|
+
# Keep this deliberately small: WebFetch/WebSearch are excluded to avoid
|
|
1571
|
+
# an untrusted cross-owner prompt exfiltrating files it just read.
|
|
1572
|
+
QODER_READ_ANALYZE_BUILTIN_TOOLS: tuple[str, ...] = (
|
|
1573
|
+
"Read",
|
|
1574
|
+
"Glob",
|
|
1575
|
+
"Grep",
|
|
1576
|
+
)
|
|
1577
|
+
|
|
1522
1578
|
# 1i Bug F — operational contract for daemon-spawned wakes.
|
|
1523
1579
|
# Without this, claude in --print mode just dumps its answer to stdout
|
|
1524
1580
|
# and exits, never invoking send_message — so the user never sees a reply.
|
|
@@ -1875,10 +1931,16 @@ TASKS (频道任务板) — 频道里的分工协作走任务认领原语,纪
|
|
|
1875
1931
|
"提交给 owner 审核 —— owner 批准后再由 owner 指挥自己的 agent 实际落盘。"
|
|
1876
1932
|
"不要试图直接改文件;被拦不是故障,是权限设计。"
|
|
1877
1933
|
)
|
|
1934
|
+
QODER_READ_ANALYZE_REPLY_NOTE = (
|
|
1935
|
+
"【Qoder 只读回报例外】本模式刻意不向模型暴露 platform_send、"
|
|
1936
|
+
"room_post、send_message 或任何写类 MCP 工具。不要尝试调用这些工具;"
|
|
1937
|
+
"请把完整分析作为本轮最终文本直接输出。daemon 会把该最终文本经受控"
|
|
1938
|
+
"fallback 发回 backend 下发的原始 conversation_id,目标不可由你更改。"
|
|
1939
|
+
)
|
|
1878
1940
|
|
|
1879
1941
|
def _codex_stdin_preamble(
|
|
1880
1942
|
self, agent_id: str, append_system_prompt: str | None,
|
|
1881
|
-
*, read_analyze: bool = False,
|
|
1943
|
+
*, read_analyze: bool = False, daemon_managed_reply: bool = False,
|
|
1882
1944
|
) -> str:
|
|
1883
1945
|
"""The system contract as a stdin preamble — codex's substitute for
|
|
1884
1946
|
``--append-system-prompt`` (which only the claude argv path has).
|
|
@@ -1891,6 +1953,8 @@ TASKS (频道任务板) — 频道里的分工协作走任务认领原语,纪
|
|
|
1891
1953
|
parts = [self.SYSTEM_PROMPT_APPEND.format(agent_id=agent_id)]
|
|
1892
1954
|
if read_analyze:
|
|
1893
1955
|
parts.append(self.READ_ANALYZE_SYSTEM_NOTE)
|
|
1956
|
+
if daemon_managed_reply:
|
|
1957
|
+
parts.append(self.QODER_READ_ANALYZE_REPLY_NOTE)
|
|
1894
1958
|
if append_system_prompt and append_system_prompt.strip():
|
|
1895
1959
|
parts.append(append_system_prompt.strip())
|
|
1896
1960
|
joined = "\n\n".join(parts)
|
|
@@ -1952,6 +2016,38 @@ TASKS (频道任务板) — 频道里的分工协作走任务认领原语,纪
|
|
|
1952
2016
|
exc_info=True,
|
|
1953
2017
|
)
|
|
1954
2018
|
|
|
2019
|
+
@staticmethod
|
|
2020
|
+
def _qoder_read_analyze_mcp_config(
|
|
2021
|
+
ws: AgentWorkspaceLike, agent_id: str
|
|
2022
|
+
) -> Path:
|
|
2023
|
+
"""Write the per-wake, query-only cloudagent MCP config for qoder.
|
|
2024
|
+
|
|
2025
|
+
The regular workspace config exposes state-changing tools (send,
|
|
2026
|
+
upload, task mutations, reminders). ``--tools`` only restricts qoder
|
|
2027
|
+
built-ins, so read_analyze needs a separately profiled bridge whose
|
|
2028
|
+
``tools/list`` contains query-only operations. The daemon fallback,
|
|
2029
|
+
not the model, posts the final analysis back to the source channel.
|
|
2030
|
+
"""
|
|
2031
|
+
from .paths import build_mcp_bridge_args, daemon_executable
|
|
2032
|
+
|
|
2033
|
+
config_path = ws.path / ".meta" / "qoder-read-analyze-mcp-config.json"
|
|
2034
|
+
token_file = str(ws.path / ".meta" / "claude-token.txt")
|
|
2035
|
+
config = {
|
|
2036
|
+
"mcpServers": {
|
|
2037
|
+
"cloudagent": {
|
|
2038
|
+
"command": daemon_executable(),
|
|
2039
|
+
"args": build_mcp_bridge_args(
|
|
2040
|
+
agent_id,
|
|
2041
|
+
token_file,
|
|
2042
|
+
tool_profile="read_analyze",
|
|
2043
|
+
),
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2048
|
+
config_path.write_text(json.dumps(config, indent=2), encoding="utf-8")
|
|
2049
|
+
return config_path
|
|
2050
|
+
|
|
1955
2051
|
def _build_cmd(
|
|
1956
2052
|
self,
|
|
1957
2053
|
ws: AgentWorkspaceLike,
|
|
@@ -2061,20 +2157,14 @@ TASKS (频道任务板) — 频道里的分工协作走任务认领原语,纪
|
|
|
2061
2157
|
# stream-json/--append-system-prompt/--mcp-config/--resume 全对齐,
|
|
2062
2158
|
# 输出走既有 stream_json 消费器(联调时如发现事件形状偏差再适配)。
|
|
2063
2159
|
# 认证:QODER_PERSONAL_ACCESS_TOKEN 环境变量或 qodercli login。
|
|
2064
|
-
if read_analyze:
|
|
2065
|
-
# fail-closed:qoder 的只读硬化(--tools/--disallowed-tools 的
|
|
2066
|
-
# 内建工具名、--permission-mode 无头语义)尚未在真机验证 ——
|
|
2067
|
-
# 未验证的安全属性不许上线(#55 口径),宁可拒绝 spawn。
|
|
2068
|
-
raise RuntimeError(
|
|
2069
|
-
"qoder runtime does not support run_mode=read_analyze yet "
|
|
2070
|
-
"(read-only hardening flags unverified); refusing to spawn"
|
|
2071
|
-
)
|
|
2072
2160
|
binary = self.runtime_binaries.get("qoder") or "qodercli"
|
|
2073
2161
|
# #95 — 会话按 conversation_id 分轨(DM/各频道各自续聊),不再共用
|
|
2074
2162
|
# 全局 last_session;无 conversation_id 的非平台唤醒沿用全局轨。
|
|
2163
|
+
# #107 — read_analyze 与 claude/codex 一样绝不 resume owner 轨,
|
|
2164
|
+
# 防外部指挥上下文渗进后续 full 会话。
|
|
2075
2165
|
lane_session = (
|
|
2076
2166
|
self._qoder_lane_session(ws, conversation_id)
|
|
2077
|
-
if with_resume else None
|
|
2167
|
+
if with_resume and not read_analyze else None
|
|
2078
2168
|
)
|
|
2079
2169
|
cmd = [binary]
|
|
2080
2170
|
# r6 同款:模型 + 推理强度由 chairman 在编辑页选(qoder 档位
|
|
@@ -2085,13 +2175,18 @@ TASKS (频道任务板) — 频道里的分工协作走任务认领原语,纪
|
|
|
2085
2175
|
cmd += ["--reasoning-effort", reasoning_effort]
|
|
2086
2176
|
if lane_session:
|
|
2087
2177
|
cmd += ["--resume", lane_session]
|
|
2178
|
+
mcp_config_path = (
|
|
2179
|
+
self._qoder_read_analyze_mcp_config(ws, agent_id)
|
|
2180
|
+
if read_analyze
|
|
2181
|
+
else ws.mcp_config_path()
|
|
2182
|
+
)
|
|
2088
2183
|
# #95 — 运营契约 + 本轮 append(含频道级提示)**不走 argv
|
|
2089
2184
|
# --append-system-prompt**:qodercli --resume 不可靠采用新的 append,
|
|
2090
2185
|
# 改与 codex 同款折进 stdin 活跃前导(见 _spawn_attempt),每轮必到
|
|
2091
2186
|
# 模型眼前,不依赖 CLI 对 resume 的语义。
|
|
2092
2187
|
cmd += [
|
|
2093
2188
|
"--mcp-config",
|
|
2094
|
-
str(
|
|
2189
|
+
str(mcp_config_path),
|
|
2095
2190
|
"--print",
|
|
2096
2191
|
"--output-format",
|
|
2097
2192
|
"stream-json",
|
|
@@ -2100,6 +2195,24 @@ TASKS (频道任务板) — 频道里的分工协作走任务认领原语,纪
|
|
|
2100
2195
|
"--permission-mode",
|
|
2101
2196
|
"bypass_permissions",
|
|
2102
2197
|
]
|
|
2198
|
+
if read_analyze:
|
|
2199
|
+
# #107 — qodercli 1.1.26 双机真测:--tools 是 built-in
|
|
2200
|
+
# capability allowlist;在 bypass_permissions 下 init.tools
|
|
2201
|
+
# 仍精确为 Glob/Grep/Read,Bash/Write/Edit/NotebookEdit/Web*
|
|
2202
|
+
# 不可调用,诱导写文件的探针保持不存在。同时 bypass 让
|
|
2203
|
+
# cloudagent MCP 能在 --print 无头模式回报结果。
|
|
2204
|
+
#
|
|
2205
|
+
# 空 setting-sources 不加载 user/project/local hooks;strict
|
|
2206
|
+
# MCP 只保留 daemon 生成的 cloudagent 配置;禁用 session
|
|
2207
|
+
# persistence,配合上面的 no-resume 保证 owner 会话隔离。
|
|
2208
|
+
cmd += [
|
|
2209
|
+
"--setting-sources",
|
|
2210
|
+
"",
|
|
2211
|
+
"--no-session-persistence",
|
|
2212
|
+
"--strict-mcp-config",
|
|
2213
|
+
"--tools",
|
|
2214
|
+
*self.QODER_READ_ANALYZE_BUILTIN_TOOLS,
|
|
2215
|
+
]
|
|
2103
2216
|
return cmd
|
|
2104
2217
|
|
|
2105
2218
|
cmd = [self.runtime_binaries.get("claude_code") or self.claude_binary]
|
|
@@ -973,13 +973,23 @@ def mcp_bridge(
|
|
|
973
973
|
),
|
|
974
974
|
),
|
|
975
975
|
profile: str | None = typer.Option(None, "--profile", help=_PROFILE_HELP),
|
|
976
|
+
tool_profile: str = typer.Option(
|
|
977
|
+
"full",
|
|
978
|
+
"--tool-profile",
|
|
979
|
+
help="MCP capability profile (full or read_analyze).",
|
|
980
|
+
),
|
|
976
981
|
) -> None:
|
|
977
982
|
"""[internal] stdio MCP server invoked by claude subprocess."""
|
|
978
983
|
from .mcp_bridge import run_bridge
|
|
979
984
|
|
|
980
985
|
profile_name = _resolve_cli_profile(profile)
|
|
981
986
|
asyncio.run(
|
|
982
|
-
run_bridge(
|
|
987
|
+
run_bridge(
|
|
988
|
+
agent_id=agent_id,
|
|
989
|
+
token_file=token_file,
|
|
990
|
+
profile=profile_name,
|
|
991
|
+
tool_profile=tool_profile,
|
|
992
|
+
)
|
|
983
993
|
)
|
|
984
994
|
|
|
985
995
|
|
|
@@ -637,6 +637,7 @@ async def _run_sse_post(
|
|
|
637
637
|
# IPC op instead of reading a file; file fallback kept for backward
|
|
638
638
|
# compat (old daemons without this map still work via the file).
|
|
639
639
|
_agent_to_session: dict[str, str | None] = {}
|
|
640
|
+
_agent_to_delivery: dict[str, str | None] = {}
|
|
640
641
|
|
|
641
642
|
# M7 W3 — localagent context injection + delegation reporting.
|
|
642
643
|
# ``injection_state`` is the file-backed (agent,task) dedup store
|
|
@@ -657,6 +658,7 @@ async def _run_sse_post(
|
|
|
657
658
|
partial_publisher=_publish_partial,
|
|
658
659
|
event_publisher=_publish_event,
|
|
659
660
|
agent_to_session=_agent_to_session,
|
|
661
|
+
agent_to_delivery=_agent_to_delivery,
|
|
660
662
|
claude_permission_mode=cfg.claude_permission_mode,
|
|
661
663
|
runtime_binaries=runtime_binaries,
|
|
662
664
|
http=http,
|
|
@@ -752,11 +754,17 @@ async def _run_sse_post(
|
|
|
752
754
|
a = req.get("args") or {}
|
|
753
755
|
agent_id_q = a.get("agent_id") or ""
|
|
754
756
|
sid = _agent_to_session.get(agent_id_q)
|
|
757
|
+
delivery_id = _agent_to_delivery.get(agent_id_q)
|
|
755
758
|
logger.info(
|
|
756
|
-
"main_loop.ipc_handler: get_current_session agent=%s sid=%s",
|
|
757
|
-
agent_id_q, sid,
|
|
759
|
+
"main_loop.ipc_handler: get_current_session agent=%s sid=%s delivery=%s",
|
|
760
|
+
agent_id_q, sid, delivery_id,
|
|
758
761
|
)
|
|
759
|
-
return {
|
|
762
|
+
return {
|
|
763
|
+
"result": {
|
|
764
|
+
"session_id": sid,
|
|
765
|
+
"delivery_id": delivery_id,
|
|
766
|
+
}
|
|
767
|
+
}
|
|
760
768
|
|
|
761
769
|
if op != "tool_call":
|
|
762
770
|
logger.warning(
|
|
@@ -981,6 +989,8 @@ async def _run_legacy_ws(
|
|
|
981
989
|
delegation_outbox = DelegationOutbox(
|
|
982
990
|
profile_delegation_outbox_path(profile)
|
|
983
991
|
)
|
|
992
|
+
agent_to_session_ws: dict[str, str | None] = {}
|
|
993
|
+
agent_to_delivery_ws: dict[str, str | None] = {}
|
|
984
994
|
|
|
985
995
|
runner = ClaudeRunner(
|
|
986
996
|
outbound=outbound,
|
|
@@ -991,6 +1001,8 @@ async def _run_legacy_ws(
|
|
|
991
1001
|
http=ws_http,
|
|
992
1002
|
injection_state=injection_state,
|
|
993
1003
|
delegation_outbox=delegation_outbox,
|
|
1004
|
+
agent_to_session=agent_to_session_ws,
|
|
1005
|
+
agent_to_delivery=agent_to_delivery_ws,
|
|
994
1006
|
)
|
|
995
1007
|
|
|
996
1008
|
# #25 — pre-flight auth self-check (parity with the SSE path). Detached
|
|
@@ -1071,6 +1083,15 @@ async def _run_legacy_ws(
|
|
|
1071
1083
|
sock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1072
1084
|
|
|
1073
1085
|
async def ipc_handler(req: dict) -> dict:
|
|
1086
|
+
if req.get("op") == "get_current_session":
|
|
1087
|
+
a = req.get("args") or {}
|
|
1088
|
+
agent_id_q = a.get("agent_id") or ""
|
|
1089
|
+
return {
|
|
1090
|
+
"result": {
|
|
1091
|
+
"session_id": agent_to_session_ws.get(agent_id_q),
|
|
1092
|
+
"delivery_id": agent_to_delivery_ws.get(agent_id_q),
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1074
1095
|
if req.get("op") != "tool_call":
|
|
1075
1096
|
return {"error": {"code": "UNKNOWN_OP", "message": str(req.get("op"))}}
|
|
1076
1097
|
a = req.get("args") or {}
|