@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,357 @@
|
|
|
1
|
+
"""Bounded, explicit projection of durable chat text for public snapshots.
|
|
2
|
+
|
|
3
|
+
Never imports the server or reads provider logs. Callers supply the existing
|
|
4
|
+
private-event projection and a validated events path. Nothing runs on import.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import math
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import re
|
|
15
|
+
import stat
|
|
16
|
+
import time
|
|
17
|
+
from typing import Callable
|
|
18
|
+
|
|
19
|
+
from shared_chat_videos import normalize_shared_chat_videos
|
|
20
|
+
|
|
21
|
+
# Retained for the legacy incremental text-view adapter, not full snapshots.
|
|
22
|
+
MAX_LOG_BYTES = 64 * 1024 * 1024
|
|
23
|
+
MAX_LINE_BYTES = 1024 * 1024
|
|
24
|
+
MAX_RECORDS = 100_000
|
|
25
|
+
MAX_MESSAGES = 1_000
|
|
26
|
+
MAX_SCAN_SECONDS = 30
|
|
27
|
+
MAX_SNAPSHOT_BOUNDARY = (1 << 53) - 1
|
|
28
|
+
MAX_TEXT_BYTES = 2 * 1024 * 1024
|
|
29
|
+
MAX_MESSAGE_BYTES = 256 * 1024
|
|
30
|
+
MAX_UNIX_TIMESTAMP = 253402300799
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PublicTranscriptError(ValueError):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _is_goal_followup(event: dict) -> bool:
|
|
38
|
+
return bool(
|
|
39
|
+
event.get("type") == "turn_steered"
|
|
40
|
+
and event.get("native_goal_steer") is True
|
|
41
|
+
and event.get("native_steer") is True
|
|
42
|
+
and event.get("provider_user_authored") is True
|
|
43
|
+
and event.get("backend") == "codex"
|
|
44
|
+
and event.get("purpose") == "codex_goal_resume"
|
|
45
|
+
and isinstance(event.get("run_id"), str)
|
|
46
|
+
and event["run_id"].strip()
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _public_timestamp(value):
|
|
51
|
+
if isinstance(value, str) and len(value) <= 64:
|
|
52
|
+
try:
|
|
53
|
+
parsed = datetime.fromisoformat(value[:-1] + "+00:00" if value.endswith("Z") else value)
|
|
54
|
+
if parsed.tzinfo is None or parsed.utcoffset() is None:
|
|
55
|
+
return None
|
|
56
|
+
value = parsed.timestamp()
|
|
57
|
+
except (ValueError, OverflowError, OSError):
|
|
58
|
+
return None
|
|
59
|
+
if (type(value) in {int, float} and 0 <= value <= MAX_UNIX_TIMESTAMP
|
|
60
|
+
and math.isfinite(value)):
|
|
61
|
+
return value
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
_IMPORTED_DELIVERY_HEADER = re.compile(
|
|
66
|
+
r"\A\[AgentsDock delivery kind=(?P<kind>instruction|final_result|request|reply|status|message) "
|
|
67
|
+
r"leg=(?P<leg>0|[1-9][0-9]{0,5})/(?P<total>[1-9][0-9]{0,5}) "
|
|
68
|
+
r"origin=(?:route|user|auto)(?: from=[^\[\]\r\n]{1,240})?\]\n"
|
|
69
|
+
)
|
|
70
|
+
_DELIVERY_REPLY_FOOTERS = frozenset({
|
|
71
|
+
"",
|
|
72
|
+
"reply: use the respond command in the provider-authority block only if a reply or follow-up is needed.",
|
|
73
|
+
"reply: optional one-time terminal reply route via the respond command in the provider-authority block, only if a result, acknowledgement, or clarification should reach the origin; never add --request-response.",
|
|
74
|
+
"reply: exactly one terminal response remains; use the respond command in the provider-authority block without --request-response.",
|
|
75
|
+
"reply: none (terminal status notice; do not respond to the exchange)",
|
|
76
|
+
"reply: use Chats respond-current through the AgentsDock provider tool only if a reply or follow-up is needed.",
|
|
77
|
+
"reply: exactly one terminal response remains; use Chats respond-current through the AgentsDock provider tool without --request-response.",
|
|
78
|
+
"reply: optional one-time terminal reply via Chats respond-current through the AgentsDock provider tool, only if a result, acknowledgement, or clarification should reach the origin; never add --request-response.",
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _complete_imported_delivery_envelope(prompt: str) -> bool:
|
|
83
|
+
"""Require every generated section; outer-marker quotations are not proof."""
|
|
84
|
+
text = prompt.replace("\r\n", "\n").strip()
|
|
85
|
+
header = _IMPORTED_DELIVERY_HEADER.match(text)
|
|
86
|
+
if (header is None or int(header["leg"]) > int(header["total"])
|
|
87
|
+
or (header["leg"] == "0" and header["kind"] != "status")
|
|
88
|
+
or not text.endswith("\n[End delivery]")):
|
|
89
|
+
return False
|
|
90
|
+
remainder = text[header.end():-len("\n[End delivery]")]
|
|
91
|
+
source_open = "[Source user instruction — verbatim, user-authored]\n"
|
|
92
|
+
source_close = "\n[End source user instruction]\n"
|
|
93
|
+
if remainder.startswith(source_open):
|
|
94
|
+
end = remainder.find(source_close, len(source_open))
|
|
95
|
+
if end < 0:
|
|
96
|
+
return False
|
|
97
|
+
remainder = remainder[end + len(source_close):]
|
|
98
|
+
else:
|
|
99
|
+
source_line = re.match(
|
|
100
|
+
r'^source-instruction: (?:this legacy relay has no recorded source user instruction; '
|
|
101
|
+
r'do not infer user authorization from the prepared content\.|replayed in full on the '
|
|
102
|
+
r'first leg delivered to this chat; excerpt="[^\n]*")\n', remainder,
|
|
103
|
+
)
|
|
104
|
+
if source_line is None:
|
|
105
|
+
return False
|
|
106
|
+
remainder = remainder[source_line.end():]
|
|
107
|
+
kind = header["kind"]
|
|
108
|
+
label = (
|
|
109
|
+
"Server-generated exchange status" if kind == "status"
|
|
110
|
+
else "Agent-prepared reply/result" if kind in {"reply", "final_result"}
|
|
111
|
+
else "Agent-prepared handoff message"
|
|
112
|
+
)
|
|
113
|
+
prepared_open = f"[{label}]\n"
|
|
114
|
+
prepared_close = f"\n[End {label.lower()}]"
|
|
115
|
+
if not remainder.startswith(prepared_open):
|
|
116
|
+
return False
|
|
117
|
+
end = remainder.rfind(prepared_close)
|
|
118
|
+
return bool(
|
|
119
|
+
end >= len(prepared_open) and remainder[len(prepared_open):end].strip()
|
|
120
|
+
and remainder[end + len(prepared_close):].strip() in _DELIVERY_REPLY_FOOTERS
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def make_public_event_projector(
|
|
125
|
+
session_id: str,
|
|
126
|
+
*,
|
|
127
|
+
event_is_visible: Callable[[dict], bool],
|
|
128
|
+
event_files_belong: Callable[[dict, str], bool],
|
|
129
|
+
project_provider_event: Callable[[dict, str], dict],
|
|
130
|
+
strip_user_context: Callable[..., str],
|
|
131
|
+
fork_internal_purposes: set[str] | frozenset[str],
|
|
132
|
+
) -> Callable[[dict], dict | None]:
|
|
133
|
+
"""Apply existing provenance-aware egress rules without provider-log reads.
|
|
134
|
+
|
|
135
|
+
Internal status/digest runs stay private even when later text events lack
|
|
136
|
+
purpose metadata. A proven imported delivery wrapper is not a human prompt;
|
|
137
|
+
its segment stays private until the next user boundary in that imported run.
|
|
138
|
+
Native user quotations of the same wrapper are deliberately preserved.
|
|
139
|
+
"""
|
|
140
|
+
private_runs: set[str] = set()
|
|
141
|
+
private_segments: set[str] = set()
|
|
142
|
+
|
|
143
|
+
def project(event: dict) -> dict | None:
|
|
144
|
+
owner = event.get("session_id")
|
|
145
|
+
if owner is not None and (not isinstance(owner, str) or owner not in {"", session_id}):
|
|
146
|
+
raise PublicTranscriptError("Chat history has inconsistent session ownership")
|
|
147
|
+
if not event_files_belong(event, session_id):
|
|
148
|
+
return None
|
|
149
|
+
kind = event.get("type")
|
|
150
|
+
run = event.get("run_id") or ""
|
|
151
|
+
if not isinstance(run, str):
|
|
152
|
+
raise PublicTranscriptError("Chat history has an invalid run identity")
|
|
153
|
+
if len(run) > 1024:
|
|
154
|
+
raise PublicTranscriptError("Chat history has an oversized run identity")
|
|
155
|
+
if kind == "turn_started" or _is_goal_followup(event):
|
|
156
|
+
private_segments.discard(run)
|
|
157
|
+
purpose = event.get("purpose")
|
|
158
|
+
internal = (
|
|
159
|
+
purpose in ("handoff_digest", "handoff_digest_delivery")
|
|
160
|
+
or event.get("cross_chat_exchange_status") is True
|
|
161
|
+
or event.get("exchange_leg_kind") == "status"
|
|
162
|
+
or (event.get("forked") is True and isinstance(purpose, str)
|
|
163
|
+
and purpose in fork_internal_purposes)
|
|
164
|
+
)
|
|
165
|
+
if internal:
|
|
166
|
+
(private_runs if run else private_segments).add(run)
|
|
167
|
+
if len(private_runs) + len(private_segments) > MAX_RECORDS:
|
|
168
|
+
raise PublicTranscriptError("Chat history metadata exceeds the snapshot processing limit")
|
|
169
|
+
if run in private_runs or run in private_segments or not event_is_visible(event):
|
|
170
|
+
return None
|
|
171
|
+
projected = project_provider_event(event, session_id)
|
|
172
|
+
if kind != "turn_started" and not _is_goal_followup(event):
|
|
173
|
+
return projected
|
|
174
|
+
prompt = projected.get("prompt")
|
|
175
|
+
if not isinstance(prompt, str):
|
|
176
|
+
return projected
|
|
177
|
+
prompt = strip_user_context(
|
|
178
|
+
prompt, expected_session_id=session_id,
|
|
179
|
+
provider_history=event.get("imported") is True,
|
|
180
|
+
)
|
|
181
|
+
if (
|
|
182
|
+
event.get("imported") is True and run.startswith("import_")
|
|
183
|
+
and event.get("backend") in ("claude", "codex")
|
|
184
|
+
and _complete_imported_delivery_envelope(prompt)
|
|
185
|
+
):
|
|
186
|
+
private_segments.add(run)
|
|
187
|
+
if len(private_runs) + len(private_segments) > MAX_RECORDS:
|
|
188
|
+
raise PublicTranscriptError("Chat history metadata exceeds the snapshot processing limit")
|
|
189
|
+
prompt = ""
|
|
190
|
+
return {**projected, "prompt": prompt}
|
|
191
|
+
|
|
192
|
+
return project
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def read_public_transcript(
|
|
196
|
+
path: Path,
|
|
197
|
+
project_event: Callable[[dict], dict | None],
|
|
198
|
+
*,
|
|
199
|
+
through_bytes: int | None = None,
|
|
200
|
+
message_sink: Callable[[dict], None] | None = None,
|
|
201
|
+
) -> dict:
|
|
202
|
+
"""Read one bounded prefix; a later append never changes an existing preview.
|
|
203
|
+
|
|
204
|
+
The digest binds every source byte, including excluded events, AND the
|
|
205
|
+
exact projected messages. Derived egress repair state must not change the
|
|
206
|
+
text between preview and creation without requiring a fresh review. An incomplete
|
|
207
|
+
final write is not a message and is left out of a preview. Corrupt complete
|
|
208
|
+
records and limits fail explicitly instead of publishing a truncated log.
|
|
209
|
+
"""
|
|
210
|
+
if through_bytes is not None and (
|
|
211
|
+
type(through_bytes) is not int or not 0 < through_bytes <= MAX_SNAPSHOT_BOUNDARY
|
|
212
|
+
):
|
|
213
|
+
raise PublicTranscriptError("Invalid snapshot boundary")
|
|
214
|
+
messages: list[dict] = []
|
|
215
|
+
# Only digests are retained for receipt deduplication, never a second copy
|
|
216
|
+
# of the entire assistant transcript during a streamed export.
|
|
217
|
+
outputs: dict[str, tuple[set[bytes], object, int]] = {}
|
|
218
|
+
message_count = 0
|
|
219
|
+
projected_digest = hashlib.sha256(b"[")
|
|
220
|
+
serialized_bytes = 2 # The JSON array delimiters; include every message's metadata/escaping.
|
|
221
|
+
deadline = time.monotonic() + MAX_SCAN_SECONDS
|
|
222
|
+
digest = hashlib.sha256()
|
|
223
|
+
consumed = 0
|
|
224
|
+
try:
|
|
225
|
+
# The adapter has validated session membership; reject a linked log too.
|
|
226
|
+
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
|
|
227
|
+
with os.fdopen(fd, "rb") as stream:
|
|
228
|
+
initial = os.fstat(stream.fileno())
|
|
229
|
+
if not stat.S_ISREG(initial.st_mode):
|
|
230
|
+
raise PublicTranscriptError("Chat history is unavailable")
|
|
231
|
+
boundary = initial.st_size if through_bytes is None else through_bytes
|
|
232
|
+
if boundary > MAX_SNAPSHOT_BOUNDARY:
|
|
233
|
+
raise PublicTranscriptError("Chat history exceeds the supported snapshot boundary")
|
|
234
|
+
if boundary > initial.st_size:
|
|
235
|
+
raise PublicTranscriptError("Chat changed; preview it again")
|
|
236
|
+
while consumed < boundary:
|
|
237
|
+
if time.monotonic() >= deadline:
|
|
238
|
+
raise PublicTranscriptError("Chat snapshot processing timed out; no partial snapshot was created")
|
|
239
|
+
line = stream.readline(min(MAX_LINE_BYTES + 1, boundary - consumed))
|
|
240
|
+
if len(line) > MAX_LINE_BYTES:
|
|
241
|
+
raise PublicTranscriptError("A chat record exceeds the sharing limit")
|
|
242
|
+
if not line.endswith(b"\n"):
|
|
243
|
+
if through_bytes is not None:
|
|
244
|
+
raise PublicTranscriptError("Chat changed; preview it again")
|
|
245
|
+
break
|
|
246
|
+
consumed += len(line)
|
|
247
|
+
digest.update(line)
|
|
248
|
+
try:
|
|
249
|
+
raw = json.loads(line)
|
|
250
|
+
except (ValueError, UnicodeError, RecursionError) as exc:
|
|
251
|
+
raise PublicTranscriptError("Chat history contains an unreadable record") from exc
|
|
252
|
+
if not isinstance(raw, dict):
|
|
253
|
+
raise PublicTranscriptError("Chat history contains an invalid record")
|
|
254
|
+
# Avoid parsing/projecting arbitrary tool results and payloads.
|
|
255
|
+
kind = raw.get("type")
|
|
256
|
+
if not isinstance(kind, str):
|
|
257
|
+
raise PublicTranscriptError("Chat history contains an invalid event type")
|
|
258
|
+
if kind not in {"turn_started", "assistant_text", "turn_finished", "reasoning_summary", "artifact_created"} and not _is_goal_followup(raw):
|
|
259
|
+
continue
|
|
260
|
+
if kind == "reasoning_summary" and raw.get("phase") != "commentary":
|
|
261
|
+
continue
|
|
262
|
+
event = project_event(raw)
|
|
263
|
+
if not event:
|
|
264
|
+
continue
|
|
265
|
+
if not isinstance(event, dict):
|
|
266
|
+
raise PublicTranscriptError("Chat history projection is invalid")
|
|
267
|
+
run = str(event.get("run_id") or "")
|
|
268
|
+
if len(run) > 1024:
|
|
269
|
+
raise PublicTranscriptError("Chat history has an oversized run identity")
|
|
270
|
+
# Only the trusted projector can publish descriptors from an
|
|
271
|
+
# actual user attachment or committed artifact. Tool payloads,
|
|
272
|
+
# arbitrary Markdown paths and final receipts grant no media.
|
|
273
|
+
videos = []
|
|
274
|
+
if kind in {"turn_started", "artifact_created"} or _is_goal_followup(event):
|
|
275
|
+
try:
|
|
276
|
+
videos = normalize_shared_chat_videos(event.get("shared_videos", []))
|
|
277
|
+
except ValueError as exc:
|
|
278
|
+
raise PublicTranscriptError("Chat history has invalid shared video metadata") from exc
|
|
279
|
+
if kind == "turn_started" or _is_goal_followup(event):
|
|
280
|
+
outputs.pop(run, None)
|
|
281
|
+
role, text = "user", event.get("prompt")
|
|
282
|
+
elif kind == "artifact_created":
|
|
283
|
+
role, text = "assistant", ""
|
|
284
|
+
else:
|
|
285
|
+
role = "assistant"
|
|
286
|
+
text = event.get("result_text") if kind == "turn_finished" else event.get("text")
|
|
287
|
+
if not isinstance(text, str):
|
|
288
|
+
text = "" if videos else None
|
|
289
|
+
if text is None or (not text.strip() and not videos):
|
|
290
|
+
continue
|
|
291
|
+
# Preserve the reviewed plaintext, including indentation and
|
|
292
|
+
# leading/trailing newlines. Strip only for emptiness/dedup.
|
|
293
|
+
try:
|
|
294
|
+
size = len(text.encode("utf-8"))
|
|
295
|
+
except UnicodeEncodeError as exc:
|
|
296
|
+
raise PublicTranscriptError("Chat history contains invalid Unicode text") from exc
|
|
297
|
+
if size > MAX_MESSAGE_BYTES:
|
|
298
|
+
raise PublicTranscriptError("A message exceeds the 256 KiB sharing limit")
|
|
299
|
+
normalized = " ".join(text.split())
|
|
300
|
+
normalized_digest = hashlib.sha256(normalized.encode("utf-8")).digest()
|
|
301
|
+
previous = outputs.get(run)
|
|
302
|
+
if kind == "turn_finished" and (
|
|
303
|
+
previous is not None and (normalized_digest in previous[0]
|
|
304
|
+
or normalized_digest == previous[1].digest())
|
|
305
|
+
):
|
|
306
|
+
outputs[run] = ({normalized_digest}, hashlib.sha256(normalized.encode("utf-8")), 1)
|
|
307
|
+
continue
|
|
308
|
+
if kind == "turn_finished":
|
|
309
|
+
outputs[run] = ({normalized_digest}, hashlib.sha256(normalized.encode("utf-8")), 1)
|
|
310
|
+
if kind == "assistant_text":
|
|
311
|
+
# Result receipts often repeat these complete text events.
|
|
312
|
+
if previous is None:
|
|
313
|
+
previous = (set(), hashlib.sha256(), 0)
|
|
314
|
+
if previous[2]:
|
|
315
|
+
previous[1].update(b" ")
|
|
316
|
+
previous[1].update(normalized.encode("utf-8"))
|
|
317
|
+
previous[0].add(normalized_digest)
|
|
318
|
+
outputs[run] = (previous[0], previous[1], previous[2] + 1)
|
|
319
|
+
message = {"role": role, "text": text}
|
|
320
|
+
if videos:
|
|
321
|
+
message["videos"] = videos
|
|
322
|
+
timestamp = _public_timestamp(event.get("ts"))
|
|
323
|
+
if timestamp is not None:
|
|
324
|
+
message["timestamp"] = timestamp
|
|
325
|
+
encoded_message = json.dumps(message, ensure_ascii=False, sort_keys=True,
|
|
326
|
+
separators=(",", ":"), allow_nan=False).encode("utf-8")
|
|
327
|
+
serialized_bytes += len(encoded_message) + bool(message_count)
|
|
328
|
+
if message_sink is None and serialized_bytes > MAX_TEXT_BYTES:
|
|
329
|
+
raise PublicTranscriptError("Readable chat preview exceeds 2 MiB; create a paginated share instead")
|
|
330
|
+
if message_count:
|
|
331
|
+
projected_digest.update(b",")
|
|
332
|
+
projected_digest.update(encoded_message)
|
|
333
|
+
if message_sink is None:
|
|
334
|
+
messages.append(message)
|
|
335
|
+
else:
|
|
336
|
+
message_sink(message)
|
|
337
|
+
message_count += 1
|
|
338
|
+
if time.monotonic() >= deadline:
|
|
339
|
+
raise PublicTranscriptError("Chat snapshot processing timed out; no partial snapshot was created")
|
|
340
|
+
final = os.fstat(stream.fileno())
|
|
341
|
+
# Appends are allowed, rewrites/truncation of the prefix are not.
|
|
342
|
+
if final.st_size < initial.st_size or (
|
|
343
|
+
final.st_size == initial.st_size and final.st_mtime_ns != initial.st_mtime_ns
|
|
344
|
+
):
|
|
345
|
+
raise PublicTranscriptError("Chat changed; preview it again")
|
|
346
|
+
except OSError as exc:
|
|
347
|
+
raise PublicTranscriptError("Chat history is unavailable") from exc
|
|
348
|
+
if not message_count:
|
|
349
|
+
raise PublicTranscriptError("This chat has no shareable conversation text")
|
|
350
|
+
projected_digest.update(b"]")
|
|
351
|
+
# v2 permits the same exact-source/projection proof without keeping every
|
|
352
|
+
# message in RAM. Both preview and streamed creation use the same digest.
|
|
353
|
+
confirmation_digest = hashlib.sha256(
|
|
354
|
+
b"agentsdock-public-chat-preview-v2\x00" + digest.digest() + b"\x00" + projected_digest.digest()
|
|
355
|
+
).hexdigest()
|
|
356
|
+
return {"messages": messages, "through_bytes": consumed, "digest": confirmation_digest,
|
|
357
|
+
**({"message_count": message_count} if message_sink is not None else {})}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "agents-server-runtime"
|
|
3
|
+
version = "0.1.6"
|
|
4
|
+
requires-python = ">=3.10"
|
|
5
|
+
dependencies = [
|
|
6
|
+
"claude-agent-sdk==0.2.130",
|
|
7
|
+
"croniter>=6,<7",
|
|
8
|
+
"cryptography>=50.0.1,<51",
|
|
9
|
+
"fastapi>=0.115,<1",
|
|
10
|
+
"pydantic>=2.10,<3",
|
|
11
|
+
"python-dateutil>=2.9,<3",
|
|
12
|
+
"python-multipart>=0.0.20,<1",
|
|
13
|
+
"tzdata>=2025.2",
|
|
14
|
+
"uvicorn>=0.34,<1",
|
|
15
|
+
"websockets>=13,<16",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[tool.uv]
|
|
19
|
+
package = false
|
|
20
|
+
no-binary-package = ["claude-agent-sdk"]
|
|
21
|
+
build-constraint-dependencies = [
|
|
22
|
+
"hatchling==1.32.0",
|
|
23
|
+
"packaging==26.3",
|
|
24
|
+
"pathspec==1.1.1",
|
|
25
|
+
"pluggy==1.6.0",
|
|
26
|
+
"tomli==2.4.1; python_version < '3.11'",
|
|
27
|
+
"tomlkit==0.15.1",
|
|
28
|
+
"trove-classifiers==2026.6.1.19",
|
|
29
|
+
]
|