@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,318 @@
1
+ """Local key lifecycle and compact signed Team Hub access credentials."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import base64
7
+ import hashlib
8
+ import hmac
9
+ import json
10
+ import os
11
+ from pathlib import Path
12
+ import secrets
13
+ import stat
14
+ import time
15
+ from typing import Any
16
+
17
+
18
+ ACCESS_TOKEN_TTL_SECONDS = 10 * 60
19
+ SESSION_TTL_SECONDS = 30 * 24 * 60 * 60
20
+ BOOTSTRAP_PROOF_TTL_SECONDS = 15 * 60
21
+ TOKEN_ISSUER = "agentsdock-team-hub"
22
+ TOKEN_AUDIENCE = "agentsdock-team-hub-api"
23
+
24
+
25
+ class TokenError(RuntimeError):
26
+ """A signed or opaque credential failed closed."""
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class MintedAccessToken:
31
+ token: str
32
+ jti: str
33
+ expires_at: int
34
+
35
+
36
+ def now_seconds() -> int:
37
+ return int(time.time())
38
+
39
+
40
+ def token_hash(value: str) -> bytes:
41
+ if not isinstance(value, str) or not 16 <= len(value) <= 4096:
42
+ hashlib.sha256(b"invalid-team-hub-credential").digest()
43
+ raise TokenError("credential is invalid or unavailable")
44
+ try:
45
+ encoded = value.encode("utf-8")
46
+ except UnicodeEncodeError as exc:
47
+ hashlib.sha256(b"invalid-team-hub-credential").digest()
48
+ raise TokenError("credential is invalid or unavailable") from exc
49
+ return hashlib.sha256(encoded).digest()
50
+
51
+
52
+ def opaque_secret(prefix: str) -> tuple[str, bytes]:
53
+ token = f"{prefix}.{secrets.token_urlsafe(32)}"
54
+ return token, hashlib.sha256(token.encode("ascii")).digest()
55
+
56
+
57
+ def _b64url_encode(value: bytes) -> str:
58
+ return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii")
59
+
60
+
61
+ def _b64url_decode(value: str) -> bytes:
62
+ if not value or len(value) > 8192:
63
+ raise TokenError("credential is invalid or unavailable")
64
+ try:
65
+ return base64.b64decode(
66
+ value.encode("ascii") + b"=" * (-len(value) % 4),
67
+ altchars=b"-_",
68
+ validate=True,
69
+ )
70
+ except (UnicodeEncodeError, ValueError) as exc:
71
+ raise TokenError("credential is invalid or unavailable") from exc
72
+
73
+
74
+ def canonical_json(value: Any) -> bytes:
75
+ return json.dumps(
76
+ value, sort_keys=True, separators=(",", ":"), ensure_ascii=False
77
+ ).encode("utf-8")
78
+
79
+
80
+ def canonical_fingerprint(value: Any) -> bytes:
81
+ return hashlib.sha256(canonical_json(value)).digest()
82
+
83
+
84
+ def _validate_secret_file(path: Path) -> bytes:
85
+ flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0)
86
+ descriptor = os.open(path, flags)
87
+ try:
88
+ info = os.fstat(descriptor)
89
+ if (
90
+ not stat.S_ISREG(info.st_mode)
91
+ or info.st_nlink != 1
92
+ or info.st_uid != os.getuid()
93
+ ):
94
+ raise PermissionError(f"secret path is not an owner-controlled regular file: {path}")
95
+ if stat.S_IMODE(info.st_mode) != 0o600:
96
+ raise PermissionError(f"secret file must have mode 0600: {path}")
97
+ chunks: list[bytes] = []
98
+ remaining = 4097
99
+ while remaining:
100
+ chunk = os.read(descriptor, min(remaining, 4097))
101
+ if not chunk:
102
+ break
103
+ chunks.append(chunk)
104
+ remaining -= len(chunk)
105
+ data = b"".join(chunks)
106
+ if not 32 <= len(data) <= 4096:
107
+ raise PermissionError(f"secret file has an invalid size: {path}")
108
+ return data
109
+ finally:
110
+ os.close(descriptor)
111
+
112
+
113
+ def ensure_private_directory(path: Path) -> None:
114
+ path.mkdir(mode=0o700, parents=True, exist_ok=True)
115
+ flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0)
116
+ flags |= getattr(os, "O_DIRECTORY", 0)
117
+ descriptor = os.open(path, flags)
118
+ try:
119
+ info = os.fstat(descriptor)
120
+ if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid():
121
+ raise PermissionError(f"Team Hub data path must be an owner-controlled directory: {path}")
122
+ os.fchmod(descriptor, 0o700)
123
+ hardened = os.fstat(descriptor)
124
+ if stat.S_IMODE(hardened.st_mode) != 0o700 or hardened.st_uid != os.getuid():
125
+ raise PermissionError(f"Team Hub data directory must have mode 0700: {path}")
126
+ finally:
127
+ os.close(descriptor)
128
+
129
+
130
+ def create_secret_file(path: Path, value: bytes) -> None:
131
+ """Create an owner-only secret without following links or overwriting data."""
132
+
133
+ ensure_private_directory(path.parent)
134
+ directory_flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0)
135
+ directory_flags |= getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_DIRECTORY", 0)
136
+ directory_descriptor = os.open(path.parent, directory_flags)
137
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
138
+ flags |= getattr(os, "O_CLOEXEC", 0)
139
+ flags |= getattr(os, "O_NOFOLLOW", 0)
140
+ try:
141
+ descriptor = os.open(path.name, flags, 0o600, dir_fd=directory_descriptor)
142
+ try:
143
+ os.fchmod(descriptor, 0o600)
144
+ written = 0
145
+ while written < len(value):
146
+ written += os.write(descriptor, value[written:])
147
+ os.fsync(descriptor)
148
+ finally:
149
+ os.close(descriptor)
150
+ os.fsync(directory_descriptor)
151
+ finally:
152
+ os.close(directory_descriptor)
153
+ _validate_secret_file(path)
154
+
155
+
156
+ def load_or_create_signing_key(path: Path) -> bytes:
157
+ try:
158
+ return _validate_secret_file(path)
159
+ except FileNotFoundError:
160
+ key = secrets.token_bytes(32)
161
+ try:
162
+ create_secret_file(path, key)
163
+ except FileExistsError:
164
+ return _validate_secret_file(path)
165
+ return key
166
+
167
+
168
+ def read_secret_file(path: Path) -> bytes:
169
+ return _validate_secret_file(path)
170
+
171
+
172
+ def validate_tls_files(certificate_path: Path, private_key_path: Path) -> None:
173
+ """Fail closed on unsafe listener TLS material before starting Uvicorn.
174
+
175
+ Certificates are public and may use ordinary read permissions. The
176
+ private key must be a single-link, current-user-owned regular file with
177
+ exact mode 0600. Both files are opened without following symlinks and are
178
+ bounded so devices/FIFOs and implausibly large inputs never reach the TLS
179
+ loader.
180
+ """
181
+
182
+ def inspect(path: Path, *, private: bool) -> None:
183
+ flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0)
184
+ descriptor = os.open(path, flags)
185
+ try:
186
+ info = os.fstat(descriptor)
187
+ if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
188
+ raise PermissionError(f"TLS path must be a single-link regular file: {path}")
189
+ if private and (
190
+ info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) != 0o600
191
+ ):
192
+ raise PermissionError(
193
+ f"TLS private key must be owned by the current user with mode 0600: {path}"
194
+ )
195
+ if not 1 <= info.st_size <= 1024 * 1024:
196
+ raise PermissionError(f"TLS file has an invalid size: {path}")
197
+ # Force a read now so an unreadable or unexpectedly truncated file
198
+ # is rejected before the listener is created. Uvicorn performs the
199
+ # cryptographic PEM validation when loading its SSL context.
200
+ if not os.read(descriptor, 1):
201
+ raise PermissionError(f"TLS file is empty: {path}")
202
+ finally:
203
+ os.close(descriptor)
204
+
205
+ inspect(Path(certificate_path), private=False)
206
+ inspect(Path(private_key_path), private=True)
207
+
208
+
209
+ class AccessTokenSigner:
210
+ """Mint and verify audience-bound HMAC-SHA256 access tokens.
211
+
212
+ The local signing key never enters SQLite, HTTP bodies, logs, or renderer
213
+ state. Authorization still re-reads session, principal, membership and ACL
214
+ state from SQLite for every request.
215
+ """
216
+
217
+ def __init__(self, key: bytes) -> None:
218
+ if len(key) < 32:
219
+ raise ValueError("access-token signing key must contain at least 32 bytes")
220
+ self._key = bytes(key)
221
+
222
+ def mint(
223
+ self,
224
+ human_principal_id: str,
225
+ device_session_id: str,
226
+ *,
227
+ now: int | None = None,
228
+ ttl_seconds: int = ACCESS_TOKEN_TTL_SECONDS,
229
+ ) -> MintedAccessToken:
230
+ issued_at = now_seconds() if now is None else int(now)
231
+ if not 1 <= int(ttl_seconds) <= ACCESS_TOKEN_TTL_SECONDS:
232
+ raise ValueError("access-token ttl is outside the supported bound")
233
+ if (
234
+ not 1 <= len(human_principal_id) <= 240
235
+ or not 1 <= len(device_session_id) <= 240
236
+ or not all(33 <= ord(char) <= 126 for char in human_principal_id)
237
+ or not all(33 <= ord(char) <= 126 for char in device_session_id)
238
+ ):
239
+ raise ValueError("access-token subject or session is invalid")
240
+ expires_at = issued_at + int(ttl_seconds)
241
+ jti = secrets.token_urlsafe(18)
242
+ header = {"alg": "HS256", "typ": "ADTH-AT1"}
243
+ payload = {
244
+ "iss": TOKEN_ISSUER,
245
+ "aud": TOKEN_AUDIENCE,
246
+ "sub": human_principal_id,
247
+ "sid": device_session_id,
248
+ "jti": jti,
249
+ "iat": issued_at,
250
+ "nbf": issued_at,
251
+ "exp": expires_at,
252
+ }
253
+ encoded_header = _b64url_encode(canonical_json(header))
254
+ encoded_payload = _b64url_encode(canonical_json(payload))
255
+ signing_input = f"{encoded_header}.{encoded_payload}".encode("ascii")
256
+ signature = hmac.new(self._key, signing_input, hashlib.sha256).digest()
257
+ return MintedAccessToken(
258
+ f"{encoded_header}.{encoded_payload}.{_b64url_encode(signature)}",
259
+ jti,
260
+ expires_at,
261
+ )
262
+
263
+ def verify(self, token: str, *, now: int | None = None) -> dict[str, Any]:
264
+ if not isinstance(token, str) or not 40 <= len(token) <= 8192:
265
+ raise TokenError("credential is invalid or unavailable")
266
+ parts = token.split(".")
267
+ if len(parts) != 3:
268
+ raise TokenError("credential is invalid or unavailable")
269
+ try:
270
+ signing_input = f"{parts[0]}.{parts[1]}".encode("ascii", "strict")
271
+ except UnicodeEncodeError as exc:
272
+ raise TokenError("credential is invalid or unavailable") from exc
273
+ supplied_signature = _b64url_decode(parts[2])
274
+ expected_signature = hmac.new(
275
+ self._key, signing_input, hashlib.sha256
276
+ ).digest()
277
+ if not hmac.compare_digest(supplied_signature, expected_signature):
278
+ raise TokenError("credential is invalid or unavailable")
279
+ try:
280
+ header = json.loads(_b64url_decode(parts[0]))
281
+ payload = json.loads(_b64url_decode(parts[1]))
282
+ except (UnicodeDecodeError, json.JSONDecodeError) as exc:
283
+ raise TokenError("credential is invalid or unavailable") from exc
284
+ if header != {"alg": "HS256", "typ": "ADTH-AT1"}:
285
+ raise TokenError("credential is invalid or unavailable")
286
+ if not isinstance(payload, dict) or set(payload) != {
287
+ "iss", "aud", "sub", "sid", "jti", "iat", "nbf", "exp"
288
+ }:
289
+ raise TokenError("credential is invalid or unavailable")
290
+ if payload["iss"] != TOKEN_ISSUER or payload["aud"] != TOKEN_AUDIENCE:
291
+ raise TokenError("credential is invalid or unavailable")
292
+ if not all(isinstance(payload[name], str) for name in ("sub", "sid", "jti")):
293
+ raise TokenError("credential is invalid or unavailable")
294
+ if (
295
+ not 1 <= len(payload["sub"]) <= 240
296
+ or not 1 <= len(payload["sid"]) <= 240
297
+ or not 1 <= len(payload["jti"]) <= 128
298
+ or any(
299
+ not all(33 <= ord(char) <= 126 for char in payload[name])
300
+ for name in ("sub", "sid", "jti")
301
+ )
302
+ ):
303
+ raise TokenError("credential is invalid or unavailable")
304
+ if not all(
305
+ isinstance(payload[name], int) and not isinstance(payload[name], bool)
306
+ for name in ("iat", "nbf", "exp")
307
+ ):
308
+ raise TokenError("credential is invalid or unavailable")
309
+ timestamp = now_seconds() if now is None else int(now)
310
+ if payload["nbf"] != payload["iat"] or payload["iat"] > timestamp + 5:
311
+ raise TokenError("credential is invalid or unavailable")
312
+ if (
313
+ payload["exp"] <= timestamp
314
+ or payload["exp"] <= payload["iat"]
315
+ or payload["exp"] - payload["iat"] > ACCESS_TOKEN_TTL_SECONDS
316
+ ):
317
+ raise TokenError("credential is invalid or unavailable")
318
+ return payload