@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,338 @@
1
+ #!/usr/bin/env python3
2
+ """Raise an explicit emergency alert for the active AgentsDock agent turn."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import ipaddress
8
+ import json
9
+ import os
10
+ import re
11
+ import sys
12
+ import unicodedata
13
+ import urllib.error
14
+ import urllib.parse
15
+ import urllib.request
16
+ import uuid
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+
21
+ PROVIDER_RUNTIME_VALUE_MAX_BYTES = 4096
22
+
23
+
24
+ class EmergencyCLIError(RuntimeError):
25
+ """A concise, user-facing emergency helper failure."""
26
+
27
+
28
+ class NoRedirectHandler(urllib.request.HTTPRedirectHandler):
29
+ def redirect_request(self, *_args: Any, **_kwargs: Any) -> None:
30
+ return None
31
+
32
+
33
+ def clean_emergency_message(value: Any) -> str:
34
+ """Mirror the server's plain-text control and whitespace normalization."""
35
+
36
+ characters: list[str] = []
37
+ for character in str(value or "")[: 500 * 4]:
38
+ category = unicodedata.category(character)
39
+ characters.append(" " if category.startswith("C") else character)
40
+ return " ".join("".join(characters).split())
41
+
42
+
43
+ def nonempty_chat_id(value: str) -> str:
44
+ chat_id = value.strip()
45
+ if not chat_id:
46
+ raise argparse.ArgumentTypeError("--chat-id must not be empty")
47
+ return chat_id
48
+
49
+
50
+ def host_is_loopback(host: str) -> bool:
51
+ try:
52
+ address = ipaddress.ip_address(host)
53
+ except ValueError:
54
+ return host.lower() == "localhost"
55
+ if isinstance(address, ipaddress.IPv6Address) and address.ipv4_mapped:
56
+ address = address.ipv4_mapped
57
+ return address.is_loopback
58
+
59
+
60
+ def canonical_http_origin(value: str, label: str) -> tuple[str, bool]:
61
+ raw = value.strip()
62
+ try:
63
+ parsed = urllib.parse.urlsplit(raw)
64
+ port = parsed.port or 80
65
+ except ValueError as exc:
66
+ raise EmergencyCLIError(f"{label} must be an HTTP origin") from exc
67
+ if (
68
+ parsed.scheme.lower() != "http"
69
+ or not parsed.hostname
70
+ or parsed.username is not None
71
+ or parsed.password is not None
72
+ or parsed.path not in {"", "/"}
73
+ or parsed.query
74
+ or parsed.fragment
75
+ ):
76
+ raise EmergencyCLIError(f"{label} must be an HTTP origin")
77
+ host = parsed.hostname.lower()
78
+ try:
79
+ address = ipaddress.ip_address(host)
80
+ if isinstance(address, ipaddress.IPv6Address) and address.ipv4_mapped:
81
+ address = address.ipv4_mapped
82
+ host = address.compressed
83
+ loopback = address.is_loopback
84
+ url_host = f"[{host}]" if isinstance(address, ipaddress.IPv6Address) else host
85
+ except ValueError:
86
+ loopback = host == "localhost"
87
+ url_host = host
88
+ return f"http://{url_host}:{port}", loopback
89
+
90
+
91
+ def bounded_identity_value(value: str | None, label: str) -> str:
92
+ clean = str(value or "").strip()
93
+ try:
94
+ size = len(clean.encode("utf-8"))
95
+ except UnicodeEncodeError as exc:
96
+ raise EmergencyCLIError(f"{label} is not valid UTF-8") from exc
97
+ if size > PROVIDER_RUNTIME_VALUE_MAX_BYTES:
98
+ raise EmergencyCLIError(f"{label} exceeds the provider runtime limit")
99
+ return clean
100
+
101
+
102
+ def selected_authority_path(authority_file: str | None) -> Path:
103
+ explicit = bounded_identity_value(authority_file, "--authority-file")
104
+ ambient = bounded_identity_value(
105
+ os.environ.get("AGENTSDOCK_PROVIDER_AUTHORITY_FILE"),
106
+ "AGENTSDOCK_PROVIDER_AUTHORITY_FILE",
107
+ )
108
+ if explicit and ambient:
109
+ explicit_key = os.path.abspath(os.path.expanduser(explicit))
110
+ ambient_key = os.path.abspath(os.path.expanduser(ambient))
111
+ if explicit_key != ambient_key:
112
+ raise EmergencyCLIError(
113
+ "--authority-file conflicts with the live provider authority"
114
+ )
115
+ selected = explicit or ambient
116
+ if not selected:
117
+ raise EmergencyCLIError("--authority-file is required")
118
+ return Path(selected).expanduser()
119
+
120
+
121
+ def provider_authority(authority_file: str | None) -> tuple[str, str]:
122
+ path = selected_authority_path(authority_file)
123
+ try:
124
+ if path.stat().st_mode & 0o077:
125
+ raise EmergencyCLIError("authority file permissions are unsafe")
126
+ payload = json.loads(path.read_text(encoding="utf-8"))
127
+ except (OSError, json.JSONDecodeError) as exc:
128
+ raise EmergencyCLIError(f"could not read authority file: {exc}") from exc
129
+ capability = str(
130
+ payload.get("provider_capability") or payload.get("capability") or ""
131
+ )
132
+ chat_id = str(payload.get("source_session_id") or "").strip()
133
+ if not capability or not chat_id:
134
+ raise EmergencyCLIError("authority file is invalid")
135
+ return capability, chat_id
136
+
137
+
138
+ def authority_server_origin(authority_file: str | None) -> str:
139
+ path = selected_authority_path(authority_file)
140
+ try:
141
+ if path.stat().st_mode & 0o077:
142
+ raise EmergencyCLIError("authority file permissions are unsafe")
143
+ payload = json.loads(path.read_text(encoding="utf-8"))
144
+ except (OSError, json.JSONDecodeError) as exc:
145
+ raise EmergencyCLIError(f"could not read authority file: {exc}") from exc
146
+ return bounded_identity_value(
147
+ payload.get("provider_server_origin"),
148
+ "authority provider_server_origin",
149
+ )
150
+
151
+
152
+ def validated_server_url(authority_origin: str) -> str:
153
+ raw_server_url = os.environ.get("AGENTSDOCK_SERVER_URL", "").strip()
154
+ if not raw_server_url:
155
+ raise EmergencyCLIError("missing agent environment: AGENTSDOCK_SERVER_URL")
156
+ server_origin, loopback = canonical_http_origin(
157
+ raw_server_url,
158
+ "AGENTSDOCK_SERVER_URL",
159
+ )
160
+ runtime_origin = bounded_identity_value(
161
+ os.environ.get("AGENTSDOCK_PROVIDER_SERVER_ORIGIN"),
162
+ "AGENTSDOCK_PROVIDER_SERVER_ORIGIN",
163
+ )
164
+ if runtime_origin:
165
+ canonical_runtime, _runtime_loopback = canonical_http_origin(
166
+ runtime_origin,
167
+ "AGENTSDOCK_PROVIDER_SERVER_ORIGIN",
168
+ )
169
+ if canonical_runtime != server_origin:
170
+ raise EmergencyCLIError(
171
+ "AGENTSDOCK_SERVER_URL conflicts with the live provider origin"
172
+ )
173
+ if loopback:
174
+ return raw_server_url.rstrip("/")
175
+ if not authority_origin:
176
+ raise EmergencyCLIError(
177
+ "non-loopback AGENTSDOCK_SERVER_URL must match the authority origin"
178
+ )
179
+ canonical_authority, _authority_loopback = canonical_http_origin(
180
+ authority_origin,
181
+ "authority provider_server_origin",
182
+ )
183
+ if canonical_authority != server_origin:
184
+ raise EmergencyCLIError(
185
+ "non-loopback AGENTSDOCK_SERVER_URL must match the authority origin"
186
+ )
187
+ return server_origin
188
+
189
+
190
+ def required_environment(
191
+ authority_file: str | None,
192
+ requested_chat_id: str | None,
193
+ ) -> tuple[str, str, str]:
194
+ token, authority_chat_id = provider_authority(authority_file)
195
+ server_url = validated_server_url(authority_server_origin(authority_file))
196
+ environment_chat_id = bounded_identity_value(
197
+ os.environ.get("AGENTSDOCK_CHAT_ID"),
198
+ "AGENTSDOCK_CHAT_ID",
199
+ )
200
+ explicit_chat_id = bounded_identity_value(requested_chat_id, "--chat-id")
201
+ if (
202
+ explicit_chat_id
203
+ and environment_chat_id
204
+ and explicit_chat_id != environment_chat_id
205
+ ):
206
+ raise EmergencyCLIError("--chat-id conflicts with AGENTSDOCK_CHAT_ID")
207
+ for candidate in (explicit_chat_id, environment_chat_id):
208
+ if candidate and candidate != authority_chat_id:
209
+ raise EmergencyCLIError("--chat-id does not match the authority file")
210
+ return server_url, authority_chat_id, token
211
+
212
+
213
+ def raise_alert(
214
+ message: str,
215
+ *,
216
+ authority_file: str | None = None,
217
+ chat_id: str | None = None,
218
+ request_id: str | None = None,
219
+ ) -> dict[str, Any]:
220
+ clean_message = clean_emergency_message(message)
221
+ if not clean_message:
222
+ raise EmergencyCLIError("--message must not be empty")
223
+ if len(clean_message) > 500:
224
+ raise EmergencyCLIError("--message must be 500 characters or fewer")
225
+ server_url, authority_chat_id, token = required_environment(
226
+ authority_file,
227
+ chat_id,
228
+ )
229
+ request_id = request_id or f"emg_{uuid.uuid4().hex}"
230
+ if not re.fullmatch(r"[A-Za-z0-9._:-]{8,128}", request_id):
231
+ raise EmergencyCLIError("--request-id is invalid")
232
+ body = json.dumps({
233
+ "request_id": request_id,
234
+ "message": clean_message,
235
+ }, separators=(",", ":")).encode("utf-8")
236
+ encoded_chat_id = urllib.parse.quote(authority_chat_id, safe="")
237
+ url = f"{server_url}/api/agent/sessions/{encoded_chat_id}/emergency-alerts"
238
+ opener = urllib.request.build_opener(
239
+ urllib.request.ProxyHandler({}),
240
+ NoRedirectHandler(),
241
+ )
242
+ decoded: Any = None
243
+ ambiguous_error: BaseException | None = None
244
+ for attempt in range(2):
245
+ request = urllib.request.Request(
246
+ url,
247
+ data=body,
248
+ headers={
249
+ "Accept": "application/json",
250
+ "Content-Type": "application/json",
251
+ "X-AgentsDock-Provider-Capability": token,
252
+ **({"X-AgentsDock-Emergency-Retry": "1"} if attempt else {}),
253
+ },
254
+ method="POST",
255
+ )
256
+ try:
257
+ with opener.open(request, timeout=30) as response:
258
+ decoded = json.loads(response.read().decode("utf-8"))
259
+ ambiguous_error = None
260
+ break
261
+ except urllib.error.HTTPError as exc:
262
+ raw = exc.read().decode("utf-8", errors="replace")
263
+ try:
264
+ detail = json.loads(raw).get("detail") or raw
265
+ except json.JSONDecodeError:
266
+ detail = raw
267
+ raise EmergencyCLIError(
268
+ f"server rejected emergency alert ({exc.code}): {detail or exc.reason}"
269
+ ) from exc
270
+ except (urllib.error.URLError, TimeoutError, OSError, json.JSONDecodeError) as exc:
271
+ ambiguous_error = exc
272
+ if ambiguous_error is not None:
273
+ reason = getattr(ambiguous_error, "reason", ambiguous_error)
274
+ raise EmergencyCLIError(
275
+ f"could not confirm whether the emergency alert was raised: {reason}"
276
+ ) from ambiguous_error
277
+ alert = decoded.get("alert") if isinstance(decoded, dict) else None
278
+ if (
279
+ not isinstance(decoded, dict)
280
+ or decoded.get("ok") is not True
281
+ or decoded.get("chat_id") != authority_chat_id
282
+ or not isinstance(alert, dict)
283
+ or not re.fullmatch(r"emergency_[0-9a-f]{32}", str(alert.get("id") or ""))
284
+ or alert.get("status") != "active"
285
+ or alert.get("severity") != "critical"
286
+ or alert.get("message") != clean_message
287
+ or not isinstance(alert.get("raised_at"), str)
288
+ or not str(alert.get("raised_at") or "").strip()
289
+ or not isinstance(decoded.get("event_id"), str)
290
+ or not str(decoded.get("event_id") or "").strip()
291
+ or not isinstance(decoded.get("event_seq"), int)
292
+ or isinstance(decoded.get("event_seq"), bool)
293
+ or int(decoded["event_seq"]) <= 0
294
+ or not isinstance(decoded.get("unacknowledged_emergency_count"), int)
295
+ or isinstance(decoded.get("unacknowledged_emergency_count"), bool)
296
+ or int(decoded["unacknowledged_emergency_count"]) < 0
297
+ ):
298
+ raise EmergencyCLIError("AgentsServer returned an invalid emergency receipt")
299
+ return decoded
300
+
301
+
302
+ def build_parser() -> argparse.ArgumentParser:
303
+ parser = argparse.ArgumentParser(
304
+ description="Contact the user about a critical AgentsDock emergency.",
305
+ allow_abbrev=False,
306
+ )
307
+ parser.add_argument(
308
+ "--authority-file",
309
+ help="mode-0600 per-run AgentsDock provider authority file",
310
+ )
311
+ parser.add_argument("--chat-id", type=nonempty_chat_id)
312
+ subparsers = parser.add_subparsers(dest="command", required=True)
313
+ alert = subparsers.add_parser(
314
+ "alert", help="raise a critical alert", allow_abbrev=False
315
+ )
316
+ alert.add_argument("--message", required=True)
317
+ alert.add_argument("--request-id", help="idempotency key (normally generated)")
318
+ return parser
319
+
320
+
321
+ def main(argv: list[str] | None = None) -> int:
322
+ args = build_parser().parse_args(argv)
323
+ try:
324
+ result = raise_alert(
325
+ args.message,
326
+ authority_file=args.authority_file,
327
+ chat_id=args.chat_id,
328
+ request_id=args.request_id,
329
+ )
330
+ except EmergencyCLIError as exc:
331
+ print(f"error: {exc}", file=sys.stderr)
332
+ return 1
333
+ print(json.dumps(result, indent=2, sort_keys=True))
334
+ return 0
335
+
336
+
337
+ if __name__ == "__main__":
338
+ raise SystemExit(main())