@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,404 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Publish files to the active AgentsDock chat turn and verify the receipt."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import ipaddress
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import urllib.error
|
|
12
|
+
import urllib.parse
|
|
13
|
+
import urllib.request
|
|
14
|
+
import uuid
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
PROVIDER_RUNTIME_VALUE_MAX_BYTES = 4096
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PublishCLIError(RuntimeError):
|
|
23
|
+
"""A concise user-facing publication failure."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NoRedirectHandler(urllib.request.HTTPRedirectHandler):
|
|
27
|
+
"""Keep the privileged publication request on the validated loopback URL."""
|
|
28
|
+
|
|
29
|
+
def redirect_request(self, *_args: Any, **_kwargs: Any) -> None:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def nonempty_chat_id(value: str) -> str:
|
|
34
|
+
chat_id = value.strip()
|
|
35
|
+
if not chat_id:
|
|
36
|
+
raise argparse.ArgumentTypeError("--chat-id must not be empty")
|
|
37
|
+
return chat_id
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def host_is_loopback(host: str) -> bool:
|
|
41
|
+
try:
|
|
42
|
+
address = ipaddress.ip_address(host)
|
|
43
|
+
except ValueError:
|
|
44
|
+
return host.lower() == "localhost"
|
|
45
|
+
if isinstance(address, ipaddress.IPv6Address) and address.ipv4_mapped:
|
|
46
|
+
address = address.ipv4_mapped
|
|
47
|
+
return address.is_loopback
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def canonical_http_origin(value: str, label: str) -> tuple[str, bool]:
|
|
51
|
+
raw = value.strip()
|
|
52
|
+
try:
|
|
53
|
+
parsed = urllib.parse.urlsplit(raw)
|
|
54
|
+
port = parsed.port or 80
|
|
55
|
+
except ValueError as exc:
|
|
56
|
+
raise PublishCLIError(f"{label} must be an HTTP origin") from exc
|
|
57
|
+
if (
|
|
58
|
+
parsed.scheme.lower() != "http"
|
|
59
|
+
or not parsed.hostname
|
|
60
|
+
or parsed.username is not None
|
|
61
|
+
or parsed.password is not None
|
|
62
|
+
or parsed.path not in {"", "/"}
|
|
63
|
+
or parsed.query
|
|
64
|
+
or parsed.fragment
|
|
65
|
+
):
|
|
66
|
+
raise PublishCLIError(f"{label} must be an HTTP origin")
|
|
67
|
+
host = parsed.hostname.lower()
|
|
68
|
+
try:
|
|
69
|
+
address = ipaddress.ip_address(host)
|
|
70
|
+
if isinstance(address, ipaddress.IPv6Address) and address.ipv4_mapped:
|
|
71
|
+
address = address.ipv4_mapped
|
|
72
|
+
host = address.compressed
|
|
73
|
+
loopback = address.is_loopback
|
|
74
|
+
url_host = f"[{host}]" if isinstance(address, ipaddress.IPv6Address) else host
|
|
75
|
+
except ValueError:
|
|
76
|
+
loopback = host == "localhost"
|
|
77
|
+
url_host = host
|
|
78
|
+
return f"http://{url_host}:{port}", loopback
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def validated_server_url(authority_origin: str = "") -> str:
|
|
82
|
+
raw_server_url = os.environ.get("AGENTSDOCK_SERVER_URL", "").strip()
|
|
83
|
+
if not raw_server_url:
|
|
84
|
+
raise PublishCLIError("missing agent environment: AGENTSDOCK_SERVER_URL")
|
|
85
|
+
server_origin, loopback = canonical_http_origin(
|
|
86
|
+
raw_server_url,
|
|
87
|
+
"AGENTSDOCK_SERVER_URL",
|
|
88
|
+
)
|
|
89
|
+
runtime_origin = bounded_identity_value(
|
|
90
|
+
os.environ.get("AGENTSDOCK_PROVIDER_SERVER_ORIGIN"),
|
|
91
|
+
"AGENTSDOCK_PROVIDER_SERVER_ORIGIN",
|
|
92
|
+
)
|
|
93
|
+
if runtime_origin:
|
|
94
|
+
canonical_runtime, _runtime_loopback = canonical_http_origin(
|
|
95
|
+
runtime_origin,
|
|
96
|
+
"AGENTSDOCK_PROVIDER_SERVER_ORIGIN",
|
|
97
|
+
)
|
|
98
|
+
if canonical_runtime != server_origin:
|
|
99
|
+
raise PublishCLIError(
|
|
100
|
+
"AGENTSDOCK_SERVER_URL conflicts with the live provider origin"
|
|
101
|
+
)
|
|
102
|
+
if loopback:
|
|
103
|
+
return raw_server_url.rstrip("/")
|
|
104
|
+
if not authority_origin:
|
|
105
|
+
raise PublishCLIError(
|
|
106
|
+
"non-loopback AGENTSDOCK_SERVER_URL must match the authority origin"
|
|
107
|
+
)
|
|
108
|
+
canonical_authority, _authority_loopback = canonical_http_origin(
|
|
109
|
+
authority_origin,
|
|
110
|
+
"authority provider_server_origin",
|
|
111
|
+
)
|
|
112
|
+
if canonical_authority != server_origin:
|
|
113
|
+
raise PublishCLIError(
|
|
114
|
+
"non-loopback AGENTSDOCK_SERVER_URL must match the authority origin"
|
|
115
|
+
)
|
|
116
|
+
return server_origin
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def loopback_server_url() -> str:
|
|
120
|
+
"""Compatibility validator for legacy explicit loopback CLI calls."""
|
|
121
|
+
|
|
122
|
+
return validated_server_url()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def bounded_identity_value(value: str | None, label: str) -> str:
|
|
126
|
+
clean = str(value or "").strip()
|
|
127
|
+
try:
|
|
128
|
+
size = len(clean.encode("utf-8"))
|
|
129
|
+
except UnicodeEncodeError as exc:
|
|
130
|
+
raise PublishCLIError(f"{label} is not valid UTF-8") from exc
|
|
131
|
+
if size > PROVIDER_RUNTIME_VALUE_MAX_BYTES:
|
|
132
|
+
raise PublishCLIError(f"{label} exceeds the provider runtime limit")
|
|
133
|
+
return clean
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def selected_authority_path(authority_file: str | None) -> Path:
|
|
137
|
+
explicit = bounded_identity_value(authority_file, "--authority-file")
|
|
138
|
+
ambient = bounded_identity_value(
|
|
139
|
+
os.environ.get("AGENTSDOCK_PROVIDER_AUTHORITY_FILE"),
|
|
140
|
+
"AGENTSDOCK_PROVIDER_AUTHORITY_FILE",
|
|
141
|
+
)
|
|
142
|
+
if explicit and ambient:
|
|
143
|
+
explicit_key = os.path.abspath(os.path.expanduser(explicit))
|
|
144
|
+
ambient_key = os.path.abspath(os.path.expanduser(ambient))
|
|
145
|
+
if explicit_key != ambient_key:
|
|
146
|
+
raise PublishCLIError(
|
|
147
|
+
"--authority-file conflicts with the live provider authority"
|
|
148
|
+
)
|
|
149
|
+
selected = explicit or ambient
|
|
150
|
+
if not selected:
|
|
151
|
+
raise PublishCLIError("--authority-file is required")
|
|
152
|
+
return Path(selected).expanduser()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def provider_authority(authority_file: str | None) -> tuple[str, str]:
|
|
156
|
+
path = selected_authority_path(authority_file)
|
|
157
|
+
try:
|
|
158
|
+
if path.stat().st_mode & 0o077:
|
|
159
|
+
raise PublishCLIError("authority file permissions are unsafe")
|
|
160
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
161
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
162
|
+
raise PublishCLIError(f"could not read authority file: {exc}") from exc
|
|
163
|
+
capability = str(payload.get("provider_capability") or payload.get("capability") or "")
|
|
164
|
+
source_session_id = str(payload.get("source_session_id") or "").strip()
|
|
165
|
+
if not capability or not source_session_id:
|
|
166
|
+
raise PublishCLIError("authority file is invalid")
|
|
167
|
+
return capability, source_session_id
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def authority_server_origin(authority_file: str | None) -> str:
|
|
171
|
+
path = selected_authority_path(authority_file)
|
|
172
|
+
try:
|
|
173
|
+
if path.stat().st_mode & 0o077:
|
|
174
|
+
raise PublishCLIError("authority file permissions are unsafe")
|
|
175
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
176
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
177
|
+
raise PublishCLIError(f"could not read authority file: {exc}") from exc
|
|
178
|
+
return bounded_identity_value(
|
|
179
|
+
payload.get("provider_server_origin"),
|
|
180
|
+
"authority provider_server_origin",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def requested_chat_scope(
|
|
185
|
+
chat_id: str | None,
|
|
186
|
+
authority_chat_id: str,
|
|
187
|
+
) -> str:
|
|
188
|
+
explicit = bounded_identity_value(chat_id, "--chat-id")
|
|
189
|
+
ambient = bounded_identity_value(
|
|
190
|
+
os.environ.get("AGENTSDOCK_CHAT_ID"),
|
|
191
|
+
"AGENTSDOCK_CHAT_ID",
|
|
192
|
+
)
|
|
193
|
+
if explicit and ambient and explicit != ambient:
|
|
194
|
+
raise PublishCLIError("--chat-id conflicts with AGENTSDOCK_CHAT_ID")
|
|
195
|
+
for candidate in (explicit, ambient):
|
|
196
|
+
if candidate and candidate != authority_chat_id:
|
|
197
|
+
raise PublishCLIError("--chat-id does not match the authority file")
|
|
198
|
+
return authority_chat_id
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def load_manifest(path: str) -> list[Any]:
|
|
202
|
+
try:
|
|
203
|
+
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
204
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
205
|
+
raise PublishCLIError(f"could not read manifest {path!r}: {exc}") from exc
|
|
206
|
+
if not isinstance(data, dict) or not isinstance(data.get("files"), list):
|
|
207
|
+
raise PublishCLIError("manifest must be a JSON object with a files array")
|
|
208
|
+
return list(data["files"])
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def parse_entry_json(value: str) -> dict[str, Any]:
|
|
212
|
+
try:
|
|
213
|
+
entry = json.loads(value)
|
|
214
|
+
except json.JSONDecodeError as exc:
|
|
215
|
+
raise argparse.ArgumentTypeError(f"invalid --entry-json: {exc}") from exc
|
|
216
|
+
if not isinstance(entry, dict):
|
|
217
|
+
raise argparse.ArgumentTypeError("--entry-json must decode to an object")
|
|
218
|
+
return entry
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def requested_files(args: argparse.Namespace) -> list[Any]:
|
|
222
|
+
entries: list[Any] = list(args.paths)
|
|
223
|
+
entries.extend(args.entry_json)
|
|
224
|
+
if args.manifest:
|
|
225
|
+
entries.extend(load_manifest(args.manifest))
|
|
226
|
+
if not entries:
|
|
227
|
+
raise PublishCLIError("provide at least one absolute file path")
|
|
228
|
+
return entries
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def http_error_detail(raw: str) -> str:
|
|
232
|
+
try:
|
|
233
|
+
decoded = json.loads(raw)
|
|
234
|
+
except json.JSONDecodeError:
|
|
235
|
+
return raw
|
|
236
|
+
if isinstance(decoded, dict):
|
|
237
|
+
detail = decoded.get("detail")
|
|
238
|
+
if isinstance(detail, str):
|
|
239
|
+
return detail
|
|
240
|
+
if detail is not None:
|
|
241
|
+
return json.dumps(detail, separators=(",", ":"))
|
|
242
|
+
return json.dumps(decoded, separators=(",", ":"))
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def publish(
|
|
246
|
+
chat_id: str | None,
|
|
247
|
+
files: list[Any],
|
|
248
|
+
*,
|
|
249
|
+
publication_id: str | None = None,
|
|
250
|
+
authority_file: str | None = None,
|
|
251
|
+
) -> dict[str, Any]:
|
|
252
|
+
capability, authority_chat_id = provider_authority(authority_file)
|
|
253
|
+
server_url = validated_server_url(authority_server_origin(authority_file))
|
|
254
|
+
chat_id = requested_chat_scope(chat_id, authority_chat_id)
|
|
255
|
+
publication_id = publication_id or f"pub_{uuid.uuid4().hex}"
|
|
256
|
+
encoded_chat_id = urllib.parse.quote(chat_id, safe="")
|
|
257
|
+
body = json.dumps({
|
|
258
|
+
"publication_id": publication_id,
|
|
259
|
+
"files": files,
|
|
260
|
+
}).encode("utf-8")
|
|
261
|
+
headers = {
|
|
262
|
+
"Accept": "application/json",
|
|
263
|
+
"Content-Type": "application/json",
|
|
264
|
+
"X-AgentsDock-Provider-Capability": capability,
|
|
265
|
+
}
|
|
266
|
+
decoded: Any = None
|
|
267
|
+
ambiguous_error: BaseException | None = None
|
|
268
|
+
# urllib honors HTTP_PROXY even for loopback addresses on some hosts.
|
|
269
|
+
# Explicitly disable proxies and redirects so this token cannot leave the
|
|
270
|
+
# already-validated local endpoint.
|
|
271
|
+
opener = urllib.request.build_opener(
|
|
272
|
+
urllib.request.ProxyHandler({}),
|
|
273
|
+
NoRedirectHandler(),
|
|
274
|
+
)
|
|
275
|
+
for attempt in range(2):
|
|
276
|
+
attempt_headers = dict(headers)
|
|
277
|
+
if attempt:
|
|
278
|
+
attempt_headers["X-AgentsDock-Publication-Retry"] = "1"
|
|
279
|
+
request = urllib.request.Request(
|
|
280
|
+
f"{server_url}/api/agent/sessions/{encoded_chat_id}/artifacts",
|
|
281
|
+
data=body,
|
|
282
|
+
headers=attempt_headers,
|
|
283
|
+
method="POST",
|
|
284
|
+
)
|
|
285
|
+
try:
|
|
286
|
+
# Large videos can take time to copy from a network workspace. The
|
|
287
|
+
# server performs that copy outside its event loop and returns only
|
|
288
|
+
# after the artifact events are durable. One retry uses the same
|
|
289
|
+
# publication ID, so a lost response cannot create duplicate cards.
|
|
290
|
+
with opener.open(request, timeout=600) as response:
|
|
291
|
+
raw_response = response.read().decode("utf-8")
|
|
292
|
+
try:
|
|
293
|
+
decoded = json.loads(raw_response)
|
|
294
|
+
except json.JSONDecodeError as exc:
|
|
295
|
+
# A truncated response is ambiguous: the event batch may
|
|
296
|
+
# already be durable. Retry once with the same publication ID.
|
|
297
|
+
ambiguous_error = exc
|
|
298
|
+
if attempt == 0:
|
|
299
|
+
continue
|
|
300
|
+
break
|
|
301
|
+
ambiguous_error = None
|
|
302
|
+
break
|
|
303
|
+
except urllib.error.HTTPError as exc:
|
|
304
|
+
raw = exc.read().decode("utf-8", errors="replace")
|
|
305
|
+
detail = http_error_detail(raw)
|
|
306
|
+
raise PublishCLIError(
|
|
307
|
+
f"server rejected publication ({exc.code}): {detail or exc.reason}"
|
|
308
|
+
) from exc
|
|
309
|
+
except (urllib.error.URLError, TimeoutError, OSError) as exc:
|
|
310
|
+
ambiguous_error = exc
|
|
311
|
+
if attempt == 0:
|
|
312
|
+
continue
|
|
313
|
+
if ambiguous_error is not None:
|
|
314
|
+
reason = getattr(ambiguous_error, "reason", ambiguous_error)
|
|
315
|
+
raise PublishCLIError(
|
|
316
|
+
f"could not confirm publication {publication_id}: {reason}"
|
|
317
|
+
) from ambiguous_error
|
|
318
|
+
if not isinstance(decoded, dict) or decoded.get("ok") is not True:
|
|
319
|
+
raise PublishCLIError("AgentsServer did not confirm publication")
|
|
320
|
+
if decoded.get("publication_id") != publication_id:
|
|
321
|
+
raise PublishCLIError("AgentsServer returned a receipt for another publication")
|
|
322
|
+
if decoded.get("chat_id") != chat_id:
|
|
323
|
+
raise PublishCLIError("AgentsServer returned a receipt for another chat")
|
|
324
|
+
run_id = decoded.get("run_id")
|
|
325
|
+
if not isinstance(run_id, str) or not run_id.strip():
|
|
326
|
+
raise PublishCLIError("AgentsServer returned a receipt without a run ID")
|
|
327
|
+
receipts = decoded.get("receipts")
|
|
328
|
+
if not isinstance(receipts, list) or len(receipts) != len(files):
|
|
329
|
+
raise PublishCLIError("AgentsServer returned an incomplete publication receipt")
|
|
330
|
+
artifact_ids: set[str] = set()
|
|
331
|
+
event_ids: set[str] = set()
|
|
332
|
+
for receipt in receipts:
|
|
333
|
+
artifact_id = str(receipt.get("artifact_id") or "") if isinstance(receipt, dict) else ""
|
|
334
|
+
event_id = str(receipt.get("event_id") or "") if isinstance(receipt, dict) else ""
|
|
335
|
+
if (
|
|
336
|
+
not isinstance(receipt, dict)
|
|
337
|
+
or not artifact_id.strip()
|
|
338
|
+
or not event_id.strip()
|
|
339
|
+
or not isinstance(receipt.get("event_seq"), int)
|
|
340
|
+
or isinstance(receipt.get("event_seq"), bool)
|
|
341
|
+
or int(receipt["event_seq"]) <= 0
|
|
342
|
+
or receipt.get("run_id") != run_id
|
|
343
|
+
or artifact_id in artifact_ids
|
|
344
|
+
or event_id in event_ids
|
|
345
|
+
):
|
|
346
|
+
raise PublishCLIError("AgentsServer returned an invalid publication receipt")
|
|
347
|
+
artifact_ids.add(artifact_id)
|
|
348
|
+
event_ids.add(event_id)
|
|
349
|
+
return decoded
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
353
|
+
parser = argparse.ArgumentParser(
|
|
354
|
+
description="Attach files to the currently active AgentsDock chat turn.",
|
|
355
|
+
allow_abbrev=False,
|
|
356
|
+
)
|
|
357
|
+
parser.add_argument(
|
|
358
|
+
"--authority-file",
|
|
359
|
+
help="mode-0600 per-run AgentsDock provider authority file",
|
|
360
|
+
)
|
|
361
|
+
parser.add_argument(
|
|
362
|
+
"--chat-id",
|
|
363
|
+
type=nonempty_chat_id,
|
|
364
|
+
help="explicit chat scope (defaults to AGENTSDOCK_CHAT_ID)",
|
|
365
|
+
)
|
|
366
|
+
parser.add_argument(
|
|
367
|
+
"--manifest",
|
|
368
|
+
help="read additional entries from a legacy {\"files\": [...]} manifest",
|
|
369
|
+
)
|
|
370
|
+
parser.add_argument(
|
|
371
|
+
"--publication-id",
|
|
372
|
+
help="idempotency key (normally generated automatically)",
|
|
373
|
+
)
|
|
374
|
+
parser.add_argument(
|
|
375
|
+
"--entry-json",
|
|
376
|
+
action="append",
|
|
377
|
+
default=[],
|
|
378
|
+
type=parse_entry_json,
|
|
379
|
+
metavar="JSON",
|
|
380
|
+
help='publish an object entry such as {"path":"/tmp/demo.mp4","title":"Demo"}',
|
|
381
|
+
)
|
|
382
|
+
parser.add_argument("paths", nargs="*", help="absolute file paths")
|
|
383
|
+
return parser
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def main(argv: list[str] | None = None) -> int:
|
|
387
|
+
parser = build_parser()
|
|
388
|
+
args = parser.parse_args(argv)
|
|
389
|
+
try:
|
|
390
|
+
result = publish(
|
|
391
|
+
args.chat_id,
|
|
392
|
+
requested_files(args),
|
|
393
|
+
publication_id=args.publication_id,
|
|
394
|
+
authority_file=args.authority_file,
|
|
395
|
+
)
|
|
396
|
+
except PublishCLIError as exc:
|
|
397
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
398
|
+
return 1
|
|
399
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
400
|
+
return 0
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
if __name__ == "__main__":
|
|
404
|
+
raise SystemExit(main())
|