@agentsdock/server 1.0.7-beta.5
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.
- package/LICENSE +202 -0
- package/NOTICE +19 -0
- package/README.md +60 -0
- package/npm/cli.cjs +289 -0
- package/package.json +32 -0
- package/server/LICENSE +202 -0
- package/server/NOTICE +19 -0
- package/server/VERSION +1 -0
- package/server/activation_transaction.py +2458 -0
- package/server/agent_server.py +93226 -0
- package/server/agentsdock_chats.py +1237 -0
- package/server/agentsdock_emergency.py +338 -0
- package/server/agentsdock_jobs.py +640 -0
- package/server/agentsdock_mail.py +325 -0
- package/server/agentsdock_publish.py +404 -0
- package/server/agentsdock_team.py +793 -0
- package/server/agentsdock_team_hub/__init__.py +33 -0
- package/server/agentsdock_team_hub/auth.py +710 -0
- package/server/agentsdock_team_hub/cli.py +647 -0
- package/server/agentsdock_team_hub/database.py +310 -0
- package/server/agentsdock_team_hub/mail_hint_streams.py +176 -0
- package/server/agentsdock_team_hub/mail_hints.py +201 -0
- package/server/agentsdock_team_hub/migrations/0001_identity_auth.sql +690 -0
- package/server/agentsdock_team_hub/migrations/0002_teamspace_ledger.sql +1336 -0
- package/server/agentsdock_team_hub/migrations/0003_service_runtime.sql +361 -0
- package/server/agentsdock_team_hub/migrations/0004_managed_host_binding.sql +22 -0
- package/server/agentsdock_team_hub/migrations/0005_tailnet_bootstrap_delegations.sql +63 -0
- package/server/agentsdock_team_hub/migrations/0006_team_network_mailbox.sql +423 -0
- package/server/agentsdock_team_hub/migrations/0007_local_agent_mail.sql +63 -0
- package/server/agentsdock_team_hub/migrations/0008_managed_server_session.sql +73 -0
- package/server/agentsdock_team_hub/migrations/0009_team_messages.sql +324 -0
- package/server/agentsdock_team_hub/migrations/0010_team_attachment_orphan_reclamation.sql +52 -0
- package/server/agentsdock_team_hub/migrations/0011_human_admin_paging.sql +65 -0
- package/server/agentsdock_team_hub/migrations/0012_network_content_deletions.sql +69 -0
- package/server/agentsdock_team_hub/migrations/0013_team_message_revisions.sql +48 -0
- package/server/agentsdock_team_hub/migrations/0014_managed_network_owner.sql +50 -0
- package/server/agentsdock_team_hub/migrations/0015_team_message_inbox_dismissals.sql +3 -0
- package/server/agentsdock_team_hub/migrations/0016_team_message_all_servers.sql +4 -0
- package/server/agentsdock_team_hub/migrations/0017_skill_announcement_deletions.sql +16 -0
- package/server/agentsdock_team_hub/migrations/0018_team_mail_subjects.sql +8 -0
- package/server/agentsdock_team_hub/migrations/0019_team_mailbox_state.sql +5 -0
- package/server/agentsdock_team_hub/migrations/0020_team_mail_arrivals.sql +44 -0
- package/server/agentsdock_team_hub/migrations/0021_team_mail_threads.sql +4 -0
- package/server/agentsdock_team_hub/migrations/0022_team_bulletin_changes.sql +49 -0
- package/server/agentsdock_team_hub/migrations/0023_team_message_search.sql +93 -0
- package/server/agentsdock_team_hub/migrations/__init__.py +1 -0
- package/server/agentsdock_team_hub/notification_hints.py +306 -0
- package/server/agentsdock_team_hub/secure_peer.py +11215 -0
- package/server/agentsdock_team_hub/secure_peer_hub.py +1372 -0
- package/server/agentsdock_team_hub/security.py +318 -0
- package/server/agentsdock_team_hub/service.py +1737 -0
- package/server/agentsdock_team_hub/store.py +16276 -0
- package/server/chat_mailbox.py +448 -0
- package/server/claude_background_reconciliation.py +92 -0
- package/server/claude_goals.py +245 -0
- package/server/claude_history_provenance.py +137 -0
- package/server/claude_history_repair.py +1601 -0
- package/server/claude_sdk_client.py +3731 -0
- package/server/claude_side_question.py +212 -0
- package/server/codex_app_server.py +3030 -0
- package/server/codex_auth.py +93 -0
- package/server/codex_history_repair.py +1247 -0
- package/server/codex_provider.py +1120 -0
- package/server/codex_side_question.py +301 -0
- package/server/cursor_agent_client.py +654 -0
- package/server/cursor_process_guard.py +347 -0
- package/server/execution_activation.py +610 -0
- package/server/execution_control.py +233 -0
- package/server/execution_durability.py +184 -0
- package/server/execution_http.py +256 -0
- package/server/execution_install.py +792 -0
- package/server/execution_legacy_runner.py +406 -0
- package/server/execution_maintenance.py +137 -0
- package/server/execution_manage.py +615 -0
- package/server/execution_ownership.py +83 -0
- package/server/execution_preparation.py +362 -0
- package/server/execution_recovery.py +845 -0
- package/server/execution_recovery_status.py +195 -0
- package/server/execution_service.py +387 -0
- package/server/execution_transport.py +622 -0
- package/server/execution_uninstall.py +406 -0
- package/server/execution_update_status.py +49 -0
- package/server/install.sh +7074 -0
- package/server/interactive_chat_controls.py +153 -0
- package/server/interactive_chat_native.py +66 -0
- package/server/interactive_chat_projection.py +152 -0
- package/server/interactive_chat_runtime.py +132 -0
- package/server/interactive_chat_share_routes.py +534 -0
- package/server/interactive_chat_share_web.py +12 -0
- package/server/interactive_chat_shares.py +238 -0
- package/server/provider_commands.py +425 -0
- package/server/public_chat_share_routes.py +426 -0
- package/server/public_chat_shares.py +759 -0
- package/server/public_chat_transcript.py +357 -0
- package/server/pyproject.toml +29 -0
- package/server/release-public-key.pem +3 -0
- package/server/secure_peer_delivery.py +1236 -0
- package/server/secure_peer_runtime.py +7694 -0
- package/server/shared_chat_video_stream.py +262 -0
- package/server/shared_chat_videos.py +155 -0
- package/server/side_questions.py +594 -0
- package/server/team_hub_host.py +1354 -0
- package/server/team_mail_grants.py +136 -0
- package/server/team_mail_runtime.py +302 -0
- package/server/team_mail_websocket.py +211 -0
- package/server/title_generation.py +333 -0
- package/server/uninstall.sh +359 -0
- package/server/update_handoff.py +237 -0
- package/server/update_preparation.py +264 -0
- package/server/update_recovery.py +140 -0
- package/server/update_runner.py +1819 -0
- package/server/uv.lock +1101 -0
- package/server/workspace_git.py +545 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Claude Code's native /btw control on an already connected SDK client.
|
|
2
|
+
|
|
3
|
+
Claude Code 2.1.261 supports ``side_question`` and request-scoped
|
|
4
|
+
``control_cancel_request`` on its stream-json control channel. Python Agent SDK
|
|
5
|
+
0.2.130 does not wrap this control yet. This small compatibility adapter uses
|
|
6
|
+
the SDK's existing response router; it never consumes the main message stream,
|
|
7
|
+
submits a user turn, forks a session, or interrupts/disconnects the parent.
|
|
8
|
+
|
|
9
|
+
The caller must run on the client owner's event loop and hold its lifecycle
|
|
10
|
+
lease for this entire await. The native provider snapshots its current full
|
|
11
|
+
conversation (including tool results), denies tools, and skips transcript writes.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
from contextlib import suppress
|
|
17
|
+
import json
|
|
18
|
+
import re
|
|
19
|
+
import uuid
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from side_questions import (
|
|
23
|
+
MAX_OUTPUT_BYTES,
|
|
24
|
+
MAX_QUESTION_CHARS,
|
|
25
|
+
REQUEST_TIMEOUT_SECONDS,
|
|
26
|
+
SideQuestionError,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
CONTEXT_NOTE = (
|
|
31
|
+
"Uses Claude's native conversation context, including completed tool results. "
|
|
32
|
+
"The reply currently being written may not be included. "
|
|
33
|
+
"Side questions have no tools and do not enter the main conversation."
|
|
34
|
+
)
|
|
35
|
+
CANCEL_TIMEOUT_SECONDS = 5.0
|
|
36
|
+
_OWNED_REQUEST_ID = re.compile(r"agentsdock_side_[0-9a-f]{32}\Z")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def is_native_side_question_progress(message: Any) -> bool:
|
|
40
|
+
"""Keep only our native control progress out of the main-turn stream.
|
|
41
|
+
|
|
42
|
+
Recognize the reserved request namespace even after cancellation removes
|
|
43
|
+
its pending entry, since a late progress packet still belongs to the side
|
|
44
|
+
request. Other native controls and ordinary system messages remain visible.
|
|
45
|
+
"""
|
|
46
|
+
if isinstance(message, dict):
|
|
47
|
+
if message.get("type") != "system":
|
|
48
|
+
return False
|
|
49
|
+
subtype, data = message.get("subtype"), message
|
|
50
|
+
else:
|
|
51
|
+
subtype, data = getattr(message, "subtype", None), getattr(message, "data", None)
|
|
52
|
+
if subtype != "control_request_progress" or not isinstance(data, dict):
|
|
53
|
+
return False
|
|
54
|
+
request_id = data.get("request_id")
|
|
55
|
+
return isinstance(request_id, str) and _OWNED_REQUEST_ID.fullmatch(request_id) is not None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _native_query(client: Any) -> Any:
|
|
59
|
+
"""Fail closed if this SDK cannot route our separate native control."""
|
|
60
|
+
query = getattr(client, "_query", None)
|
|
61
|
+
if (
|
|
62
|
+
query is None
|
|
63
|
+
or not getattr(query, "is_streaming_mode", False)
|
|
64
|
+
or not getattr(query, "_initialized", False)
|
|
65
|
+
or getattr(query, "_closed", True)
|
|
66
|
+
or not isinstance(getattr(query, "pending_control_responses", None), dict)
|
|
67
|
+
or not isinstance(getattr(query, "pending_control_results", None), dict)
|
|
68
|
+
or not callable(getattr(getattr(query, "transport", None), "write", None))
|
|
69
|
+
):
|
|
70
|
+
raise SideQuestionError(503, "Claude native side questions require a connected compatible SDK session")
|
|
71
|
+
return query
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _request(question: str, history: list[dict] | None) -> dict:
|
|
75
|
+
if not isinstance(question, str) or not question.strip() or len(question) > MAX_QUESTION_CHARS:
|
|
76
|
+
raise SideQuestionError(400, "Side questions must contain 1–8000 characters")
|
|
77
|
+
try:
|
|
78
|
+
question.encode("utf-8")
|
|
79
|
+
except UnicodeEncodeError:
|
|
80
|
+
raise SideQuestionError(400, "Side questions must contain valid Unicode text") from None
|
|
81
|
+
history = [] if history is None else history
|
|
82
|
+
if not isinstance(history, list) or len(history) > 20:
|
|
83
|
+
raise SideQuestionError(400, "Native side history must contain at most 20 exchanges")
|
|
84
|
+
pairs = []
|
|
85
|
+
for item in history:
|
|
86
|
+
if not isinstance(item, dict) or set(item) != {"question", "response"}:
|
|
87
|
+
raise SideQuestionError(400, "Native side history must contain question/response pairs")
|
|
88
|
+
for text in item.values():
|
|
89
|
+
if not isinstance(text, str) or not text.strip():
|
|
90
|
+
raise SideQuestionError(400, "Native side-history exchanges must contain text")
|
|
91
|
+
try:
|
|
92
|
+
text.encode("utf-8")
|
|
93
|
+
except UnicodeEncodeError:
|
|
94
|
+
raise SideQuestionError(400, "Native side history must contain valid Unicode text") from None
|
|
95
|
+
pairs.append(dict(item))
|
|
96
|
+
request = {"subtype": "side_question", "question": question}
|
|
97
|
+
if pairs:
|
|
98
|
+
request["history"] = pairs
|
|
99
|
+
return request
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _answer(result: Any) -> dict:
|
|
103
|
+
# Provider errors can contain paths, credentials or raw upstream content.
|
|
104
|
+
# They are deliberately not returned to an HTTP caller.
|
|
105
|
+
if isinstance(result, Exception):
|
|
106
|
+
raise SideQuestionError(503, "Claude native side question failed or is unsupported by this CLI")
|
|
107
|
+
if not isinstance(result, dict) or result.get("subtype") != "success":
|
|
108
|
+
raise SideQuestionError(502, "Claude returned an invalid native side-question response")
|
|
109
|
+
payload = result.get("response")
|
|
110
|
+
if not isinstance(payload, dict):
|
|
111
|
+
raise SideQuestionError(502, "Claude returned an invalid native side-question response")
|
|
112
|
+
answer = payload.get("response")
|
|
113
|
+
if payload.get("synthetic") is not False:
|
|
114
|
+
raise SideQuestionError(502, "Claude could not answer this side question from its current context")
|
|
115
|
+
if not isinstance(answer, str) or not answer.strip():
|
|
116
|
+
raise SideQuestionError(502, "Claude returned no native side-question answer")
|
|
117
|
+
try:
|
|
118
|
+
encoded = answer.encode("utf-8")
|
|
119
|
+
except UnicodeEncodeError:
|
|
120
|
+
raise SideQuestionError(502, "Claude returned an invalid native side-question answer") from None
|
|
121
|
+
if len(encoded) > MAX_OUTPUT_BYTES:
|
|
122
|
+
raise SideQuestionError(502, "Claude returned an oversized native side-question answer")
|
|
123
|
+
return {"answer": answer, "context_note": CONTEXT_NOTE}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def ask_native_side_question(
|
|
127
|
+
client: Any,
|
|
128
|
+
question: str,
|
|
129
|
+
*,
|
|
130
|
+
history: list[dict] | None = None,
|
|
131
|
+
timeout_seconds: float = REQUEST_TIMEOUT_SECONDS,
|
|
132
|
+
) -> dict:
|
|
133
|
+
"""Ask native /btw without acquiring the parent's main-turn authority.
|
|
134
|
+
|
|
135
|
+
Cancellation sends only this control's opaque ID. The parent's existing SDK
|
|
136
|
+
reader delivers the response into the same event tables it uses for all
|
|
137
|
+
other controls. No second stream reader or main-turn command is created.
|
|
138
|
+
"""
|
|
139
|
+
request = _request(question, history)
|
|
140
|
+
query = _native_query(client)
|
|
141
|
+
if timeout_seconds <= 0:
|
|
142
|
+
raise ValueError("timeout_seconds must be positive")
|
|
143
|
+
request_id = f"agentsdock_side_{uuid.uuid4().hex}"
|
|
144
|
+
event = asyncio.Event()
|
|
145
|
+
query.pending_control_responses[request_id] = event
|
|
146
|
+
wire = json.dumps({"type": "control_request", "request_id": request_id, "request": request}) + "\n"
|
|
147
|
+
sending = asyncio.create_task(query.transport.write(wire), name=f"claude-side-send:{request_id}")
|
|
148
|
+
|
|
149
|
+
async def exchange() -> Any:
|
|
150
|
+
await asyncio.shield(sending)
|
|
151
|
+
await event.wait()
|
|
152
|
+
return query.pending_control_results.get(request_id)
|
|
153
|
+
|
|
154
|
+
receiving = asyncio.create_task(exchange(), name=f"claude-side-result:{request_id}")
|
|
155
|
+
|
|
156
|
+
async def cancel_exact_request() -> None:
|
|
157
|
+
# Join the original write before cancellation so even a slow transport
|
|
158
|
+
# cannot deliver our cancellation before the side question exists.
|
|
159
|
+
with suppress(Exception):
|
|
160
|
+
await asyncio.shield(sending)
|
|
161
|
+
if event.is_set():
|
|
162
|
+
return
|
|
163
|
+
await query.transport.write(json.dumps({
|
|
164
|
+
"type": "control_cancel_request", "request_id": request_id,
|
|
165
|
+
}) + "\n")
|
|
166
|
+
await event.wait()
|
|
167
|
+
|
|
168
|
+
async def cancel_and_settle() -> None:
|
|
169
|
+
cleanup = asyncio.create_task(cancel_exact_request(), name=f"claude-side-cancel:{request_id}")
|
|
170
|
+
deadline = asyncio.get_running_loop().time() + CANCEL_TIMEOUT_SECONDS
|
|
171
|
+
try:
|
|
172
|
+
while not cleanup.done():
|
|
173
|
+
remaining = deadline - asyncio.get_running_loop().time()
|
|
174
|
+
if remaining <= 0:
|
|
175
|
+
break
|
|
176
|
+
try:
|
|
177
|
+
await asyncio.wait_for(asyncio.shield(cleanup), remaining)
|
|
178
|
+
except asyncio.CancelledError:
|
|
179
|
+
# A duplicate disconnect must not interrupt exact-ID cleanup.
|
|
180
|
+
continue
|
|
181
|
+
except Exception:
|
|
182
|
+
break
|
|
183
|
+
finally:
|
|
184
|
+
if not cleanup.done():
|
|
185
|
+
cleanup.cancel()
|
|
186
|
+
with suppress(BaseException):
|
|
187
|
+
await cleanup
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
result = await asyncio.wait_for(asyncio.shield(receiving), timeout_seconds)
|
|
191
|
+
return _answer(result)
|
|
192
|
+
except asyncio.CancelledError:
|
|
193
|
+
await cancel_and_settle()
|
|
194
|
+
raise
|
|
195
|
+
except TimeoutError:
|
|
196
|
+
await cancel_and_settle()
|
|
197
|
+
raise SideQuestionError(504, "Claude native side question timed out") from None
|
|
198
|
+
except SideQuestionError:
|
|
199
|
+
raise
|
|
200
|
+
except Exception:
|
|
201
|
+
# A transport can report failure after delivering a frame. Attempt the
|
|
202
|
+
# exact native cancellation without changing the main connection.
|
|
203
|
+
await cancel_and_settle()
|
|
204
|
+
raise SideQuestionError(503, "Claude native side-question connection is unavailable") from None
|
|
205
|
+
finally:
|
|
206
|
+
query.pending_control_responses.pop(request_id, None)
|
|
207
|
+
query.pending_control_results.pop(request_id, None)
|
|
208
|
+
for task in (receiving, sending):
|
|
209
|
+
if not task.done():
|
|
210
|
+
task.cancel()
|
|
211
|
+
with suppress(BaseException):
|
|
212
|
+
await task
|