@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.
Files changed (113) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +19 -0
  3. package/README.md +60 -0
  4. package/npm/cli.cjs +289 -0
  5. package/package.json +32 -0
  6. package/server/LICENSE +202 -0
  7. package/server/NOTICE +19 -0
  8. package/server/VERSION +1 -0
  9. package/server/activation_transaction.py +2458 -0
  10. package/server/agent_server.py +93226 -0
  11. package/server/agentsdock_chats.py +1237 -0
  12. package/server/agentsdock_emergency.py +338 -0
  13. package/server/agentsdock_jobs.py +640 -0
  14. package/server/agentsdock_mail.py +325 -0
  15. package/server/agentsdock_publish.py +404 -0
  16. package/server/agentsdock_team.py +793 -0
  17. package/server/agentsdock_team_hub/__init__.py +33 -0
  18. package/server/agentsdock_team_hub/auth.py +710 -0
  19. package/server/agentsdock_team_hub/cli.py +647 -0
  20. package/server/agentsdock_team_hub/database.py +310 -0
  21. package/server/agentsdock_team_hub/mail_hint_streams.py +176 -0
  22. package/server/agentsdock_team_hub/mail_hints.py +201 -0
  23. package/server/agentsdock_team_hub/migrations/0001_identity_auth.sql +690 -0
  24. package/server/agentsdock_team_hub/migrations/0002_teamspace_ledger.sql +1336 -0
  25. package/server/agentsdock_team_hub/migrations/0003_service_runtime.sql +361 -0
  26. package/server/agentsdock_team_hub/migrations/0004_managed_host_binding.sql +22 -0
  27. package/server/agentsdock_team_hub/migrations/0005_tailnet_bootstrap_delegations.sql +63 -0
  28. package/server/agentsdock_team_hub/migrations/0006_team_network_mailbox.sql +423 -0
  29. package/server/agentsdock_team_hub/migrations/0007_local_agent_mail.sql +63 -0
  30. package/server/agentsdock_team_hub/migrations/0008_managed_server_session.sql +73 -0
  31. package/server/agentsdock_team_hub/migrations/0009_team_messages.sql +324 -0
  32. package/server/agentsdock_team_hub/migrations/0010_team_attachment_orphan_reclamation.sql +52 -0
  33. package/server/agentsdock_team_hub/migrations/0011_human_admin_paging.sql +65 -0
  34. package/server/agentsdock_team_hub/migrations/0012_network_content_deletions.sql +69 -0
  35. package/server/agentsdock_team_hub/migrations/0013_team_message_revisions.sql +48 -0
  36. package/server/agentsdock_team_hub/migrations/0014_managed_network_owner.sql +50 -0
  37. package/server/agentsdock_team_hub/migrations/0015_team_message_inbox_dismissals.sql +3 -0
  38. package/server/agentsdock_team_hub/migrations/0016_team_message_all_servers.sql +4 -0
  39. package/server/agentsdock_team_hub/migrations/0017_skill_announcement_deletions.sql +16 -0
  40. package/server/agentsdock_team_hub/migrations/0018_team_mail_subjects.sql +8 -0
  41. package/server/agentsdock_team_hub/migrations/0019_team_mailbox_state.sql +5 -0
  42. package/server/agentsdock_team_hub/migrations/0020_team_mail_arrivals.sql +44 -0
  43. package/server/agentsdock_team_hub/migrations/0021_team_mail_threads.sql +4 -0
  44. package/server/agentsdock_team_hub/migrations/0022_team_bulletin_changes.sql +49 -0
  45. package/server/agentsdock_team_hub/migrations/0023_team_message_search.sql +93 -0
  46. package/server/agentsdock_team_hub/migrations/__init__.py +1 -0
  47. package/server/agentsdock_team_hub/notification_hints.py +306 -0
  48. package/server/agentsdock_team_hub/secure_peer.py +11215 -0
  49. package/server/agentsdock_team_hub/secure_peer_hub.py +1372 -0
  50. package/server/agentsdock_team_hub/security.py +318 -0
  51. package/server/agentsdock_team_hub/service.py +1737 -0
  52. package/server/agentsdock_team_hub/store.py +16276 -0
  53. package/server/chat_mailbox.py +448 -0
  54. package/server/claude_background_reconciliation.py +92 -0
  55. package/server/claude_goals.py +245 -0
  56. package/server/claude_history_provenance.py +137 -0
  57. package/server/claude_history_repair.py +1601 -0
  58. package/server/claude_sdk_client.py +3731 -0
  59. package/server/claude_side_question.py +212 -0
  60. package/server/codex_app_server.py +3030 -0
  61. package/server/codex_auth.py +93 -0
  62. package/server/codex_history_repair.py +1247 -0
  63. package/server/codex_provider.py +1120 -0
  64. package/server/codex_side_question.py +301 -0
  65. package/server/cursor_agent_client.py +654 -0
  66. package/server/cursor_process_guard.py +347 -0
  67. package/server/execution_activation.py +610 -0
  68. package/server/execution_control.py +233 -0
  69. package/server/execution_durability.py +184 -0
  70. package/server/execution_http.py +256 -0
  71. package/server/execution_install.py +792 -0
  72. package/server/execution_legacy_runner.py +406 -0
  73. package/server/execution_maintenance.py +137 -0
  74. package/server/execution_manage.py +615 -0
  75. package/server/execution_ownership.py +83 -0
  76. package/server/execution_preparation.py +362 -0
  77. package/server/execution_recovery.py +845 -0
  78. package/server/execution_recovery_status.py +195 -0
  79. package/server/execution_service.py +387 -0
  80. package/server/execution_transport.py +622 -0
  81. package/server/execution_uninstall.py +406 -0
  82. package/server/execution_update_status.py +49 -0
  83. package/server/install.sh +7074 -0
  84. package/server/interactive_chat_controls.py +153 -0
  85. package/server/interactive_chat_native.py +66 -0
  86. package/server/interactive_chat_projection.py +152 -0
  87. package/server/interactive_chat_runtime.py +132 -0
  88. package/server/interactive_chat_share_routes.py +534 -0
  89. package/server/interactive_chat_share_web.py +12 -0
  90. package/server/interactive_chat_shares.py +238 -0
  91. package/server/provider_commands.py +425 -0
  92. package/server/public_chat_share_routes.py +426 -0
  93. package/server/public_chat_shares.py +759 -0
  94. package/server/public_chat_transcript.py +357 -0
  95. package/server/pyproject.toml +29 -0
  96. package/server/release-public-key.pem +3 -0
  97. package/server/secure_peer_delivery.py +1236 -0
  98. package/server/secure_peer_runtime.py +7694 -0
  99. package/server/shared_chat_video_stream.py +262 -0
  100. package/server/shared_chat_videos.py +155 -0
  101. package/server/side_questions.py +594 -0
  102. package/server/team_hub_host.py +1354 -0
  103. package/server/team_mail_grants.py +136 -0
  104. package/server/team_mail_runtime.py +302 -0
  105. package/server/team_mail_websocket.py +211 -0
  106. package/server/title_generation.py +333 -0
  107. package/server/uninstall.sh +359 -0
  108. package/server/update_handoff.py +237 -0
  109. package/server/update_preparation.py +264 -0
  110. package/server/update_recovery.py +140 -0
  111. package/server/update_runner.py +1819 -0
  112. package/server/uv.lock +1101 -0
  113. 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
+ ]
@@ -0,0 +1,3 @@
1
+ -----BEGIN PUBLIC KEY-----
2
+ MCowBQYDK2VwAyEAdQ/g4Ta92ClC4Qx2L0Z339ylFhOyE2M6GK5nfXdPk8Y=
3
+ -----END PUBLIC KEY-----