@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,647 @@
1
+ """Team Hub local control and development-listener command line."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from contextlib import suppress
7
+ import hashlib
8
+ import ipaddress
9
+ import os
10
+ from pathlib import Path
11
+ import re
12
+ import secrets
13
+ import stat
14
+ import sys
15
+
16
+ import uvicorn
17
+
18
+ from .security import validate_tls_files
19
+ from .service import create_app
20
+ from .store import HubError, HubStore
21
+
22
+
23
+ DEFAULT_DATA_DIR = Path.home() / ".agentsdock" / "team-hub"
24
+ SERVER_IDENTITY_RE = re.compile(r"[0-9a-f]{24}")
25
+
26
+
27
+ def _legacy_server_identity(state_dir: Path) -> str:
28
+ machine = ""
29
+ for path in (Path("/etc/machine-id"), Path("/var/lib/dbus/machine-id")):
30
+ with suppress(Exception):
31
+ machine = path.read_text(encoding="utf-8").strip()
32
+ if machine:
33
+ break
34
+ if not machine:
35
+ machine = os.uname().nodename
36
+ payload = f"{machine}|{state_dir.resolve()}".encode(
37
+ "utf-8",
38
+ errors="ignore",
39
+ )
40
+ return hashlib.sha256(payload).hexdigest()[:24]
41
+
42
+
43
+ def _read_server_identity_file(path: Path) -> str | None:
44
+ try:
45
+ metadata = path.lstat()
46
+ except FileNotFoundError:
47
+ return None
48
+ if not stat.S_ISREG(metadata.st_mode):
49
+ raise PermissionError("AgentsServer identity is not a regular file")
50
+ flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0)
51
+ flags |= getattr(os, "O_NOFOLLOW", 0)
52
+ descriptor = os.open(path, flags)
53
+ try:
54
+ opened = os.fstat(descriptor)
55
+ if (
56
+ not stat.S_ISREG(opened.st_mode)
57
+ or opened.st_dev != metadata.st_dev
58
+ or opened.st_ino != metadata.st_ino
59
+ or opened.st_uid != os.geteuid()
60
+ or stat.S_IMODE(opened.st_mode) != 0o600
61
+ ):
62
+ raise PermissionError("AgentsServer identity file is unsafe")
63
+ raw = os.read(descriptor, 256)
64
+ if os.read(descriptor, 1):
65
+ raise RuntimeError("AgentsServer identity file is too large")
66
+ finally:
67
+ os.close(descriptor)
68
+ try:
69
+ identity = raw.decode("ascii").strip()
70
+ except UnicodeError as exc:
71
+ raise RuntimeError("AgentsServer identity is not ASCII") from exc
72
+ if SERVER_IDENTITY_RE.fullmatch(identity) is None:
73
+ raise RuntimeError("AgentsServer identity is invalid")
74
+ return identity
75
+
76
+
77
+ def _persist_server_identity(path: Path, identity: str) -> str:
78
+ path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
79
+ parent = path.parent.lstat()
80
+ if (
81
+ not stat.S_ISDIR(parent.st_mode)
82
+ or parent.st_uid != os.geteuid()
83
+ or stat.S_IMODE(parent.st_mode) != 0o700
84
+ ):
85
+ raise PermissionError("AgentsServer state directory is unsafe")
86
+ temporary = path.with_name(
87
+ f".{path.name}.{os.getpid()}.{secrets.token_hex(8)}.tmp"
88
+ )
89
+ descriptor = os.open(
90
+ temporary,
91
+ os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_CLOEXEC", 0),
92
+ 0o600,
93
+ )
94
+ published = False
95
+ published_identity: tuple[int, int] | None = None
96
+ try:
97
+ with os.fdopen(descriptor, "w", encoding="ascii") as stream:
98
+ # Creation mode is still filtered by the caller's umask. Restore
99
+ # the exact owner-only invariant before this inode can be linked
100
+ # into the durable identity path.
101
+ os.fchmod(stream.fileno(), 0o600)
102
+ stream.write(identity + "\n")
103
+ stream.flush()
104
+ os.fsync(stream.fileno())
105
+ opened = os.fstat(stream.fileno())
106
+ published_identity = (int(opened.st_dev), int(opened.st_ino))
107
+ try:
108
+ os.link(temporary, path, follow_symlinks=False)
109
+ except FileExistsError:
110
+ pass
111
+ else:
112
+ published = True
113
+ directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
114
+ directory = os.open(path.parent, directory_flags)
115
+ try:
116
+ os.fsync(directory)
117
+ finally:
118
+ os.close(directory)
119
+ persisted = _read_server_identity_file(path)
120
+ if persisted is None or not secrets.compare_digest(persisted, identity):
121
+ raise RuntimeError("AgentsServer identity changed during reactivation")
122
+ return persisted
123
+ except BaseException:
124
+ if published and published_identity is not None:
125
+ try:
126
+ linked = path.lstat()
127
+ if (int(linked.st_dev), int(linked.st_ino)) == published_identity:
128
+ path.unlink()
129
+ directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
130
+ directory = os.open(path.parent, directory_flags)
131
+ try:
132
+ os.fsync(directory)
133
+ finally:
134
+ os.close(directory)
135
+ except OSError:
136
+ pass
137
+ raise
138
+ finally:
139
+ with suppress(OSError):
140
+ temporary.unlink()
141
+
142
+
143
+ def _reactivation_server_identity(state_dir: Path) -> tuple[str, Path]:
144
+ root = Path(os.path.abspath(os.path.expanduser(os.fspath(state_dir))))
145
+ identity_path = root / "server-identity"
146
+ persisted = _read_server_identity_file(identity_path)
147
+ return (persisted or _legacy_server_identity(root), identity_path)
148
+
149
+
150
+ def _parser() -> argparse.ArgumentParser:
151
+ parser = argparse.ArgumentParser(prog="agentsdock-team-hub")
152
+ subcommands = parser.add_subparsers(dest="command", required=True)
153
+
154
+ serve = subcommands.add_parser("serve", help="run the Team Hub API")
155
+ serve.add_argument("--data-dir", type=Path, default=DEFAULT_DATA_DIR)
156
+ serve.add_argument("--host", default="127.0.0.1")
157
+ serve.add_argument("--port", type=int, default=7851)
158
+ serve.add_argument("--allowed-host", action="append", default=[])
159
+ serve.add_argument("--allowed-origin", action="append", default=[])
160
+ serve.add_argument("--ssl-certfile", type=Path)
161
+ serve.add_argument("--ssl-keyfile", type=Path)
162
+
163
+ proof = subcommands.add_parser(
164
+ "bootstrap-proof", help="renew or locate the local initial-owner proof"
165
+ )
166
+ proof.add_argument("--data-dir", type=Path, default=DEFAULT_DATA_DIR)
167
+
168
+ recovery = subcommands.add_parser(
169
+ "owner-recovery",
170
+ help=(
171
+ "revoke the owner's existing device sessions and issue a local "
172
+ "one-time recovery proof"
173
+ ),
174
+ )
175
+ recovery.add_argument("--data-dir", type=Path, default=DEFAULT_DATA_DIR)
176
+ recovery.add_argument("--email", required=True)
177
+ recovery.add_argument("--device-label", required=True)
178
+ recovery.add_argument("--team-id")
179
+
180
+ device_recovery = subcommands.add_parser(
181
+ "device-recovery",
182
+ help=(
183
+ "revoke a member's existing device sessions and issue a local "
184
+ "one-time recovery proof"
185
+ ),
186
+ )
187
+ device_recovery.add_argument("--data-dir", type=Path, default=DEFAULT_DATA_DIR)
188
+ device_recovery.add_argument("--email", required=True)
189
+ device_recovery.add_argument("--device-label", required=True)
190
+ device_recovery.add_argument("--team-id")
191
+
192
+ for command, help_text in (
193
+ (
194
+ "verify-snapshot",
195
+ "verify an exact managed rollback snapshot without changing Hub state",
196
+ ),
197
+ (
198
+ "rebase-snapshot",
199
+ "refresh an update snapshot from one stopped legacy source",
200
+ ),
201
+ (
202
+ "restore-snapshot",
203
+ "offline verified restore of an exact managed rollback snapshot",
204
+ ),
205
+ ):
206
+ snapshot = subcommands.add_parser(command, help=help_text)
207
+ snapshot.add_argument("--data-dir", type=Path, default=DEFAULT_DATA_DIR)
208
+ snapshot.add_argument("--snapshot", type=Path, required=True)
209
+ snapshot.add_argument("--expected-host-identity", required=True)
210
+ snapshot.add_argument("--expected-hub-id", required=True)
211
+ snapshot.add_argument("--expected-operation-id", required=True)
212
+ for command, help_text in (
213
+ (
214
+ "confirm-restored-snapshot",
215
+ "verify an exact completed managed rollback restore",
216
+ ),
217
+ (
218
+ "acknowledge-restored-snapshot",
219
+ "retire an exact completed managed rollback restore receipt",
220
+ ),
221
+ (
222
+ "confirm-restored-host-reactivation-snapshot",
223
+ "verify an exact completed host-reactivation rollback restore",
224
+ ),
225
+ (
226
+ "acknowledge-restored-host-reactivation-snapshot",
227
+ "retire an exact completed host-reactivation rollback receipt",
228
+ ),
229
+ ):
230
+ restored = subcommands.add_parser(command, help=help_text)
231
+ restored.add_argument("--data-dir", type=Path, default=DEFAULT_DATA_DIR)
232
+ restored.add_argument("--snapshot", type=Path, required=True)
233
+ restored.add_argument("--expected-host-identity", required=True)
234
+ restored.add_argument("--expected-hub-id", required=True)
235
+ restored.add_argument("--expected-operation-id", required=True)
236
+ if command.startswith("acknowledge-"):
237
+ restored.add_argument("--allow-missing", action="store_true")
238
+ for command, help_text in (
239
+ (
240
+ "publish-fenced-start-authority",
241
+ "publish exact private candidate startup authority",
242
+ ),
243
+ (
244
+ "clear-fenced-start-authority",
245
+ "clear exact private candidate startup authority",
246
+ ),
247
+ ):
248
+ authority = subcommands.add_parser(command, help=help_text)
249
+ authority.add_argument("--data-dir", type=Path, default=DEFAULT_DATA_DIR)
250
+ authority.add_argument("--snapshot", type=Path, required=True)
251
+ authority.add_argument("--expected-host-identity", required=True)
252
+ authority.add_argument("--expected-hub-id", required=True)
253
+ authority.add_argument("--expected-reason", required=True)
254
+ authority.add_argument("--expected-operation-id", required=True)
255
+ if command == "clear-fenced-start-authority":
256
+ authority.add_argument("--allow-missing", action="store_true")
257
+ reactivation = subcommands.add_parser(
258
+ "prepare-host-reactivation",
259
+ help=(
260
+ "verify and snapshot preserved managed Hub state before explicitly "
261
+ "reactivating it"
262
+ ),
263
+ )
264
+ reactivation.add_argument("--data-dir", type=Path, default=DEFAULT_DATA_DIR)
265
+ reactivation.add_argument("--server-state-dir", type=Path, required=True)
266
+ begin_guard = subcommands.add_parser(
267
+ "begin-host-cold-handoff",
268
+ help="publish a durable guard before unlinking a legacy host runtime",
269
+ )
270
+ begin_guard.add_argument("--data-dir", type=Path, default=DEFAULT_DATA_DIR)
271
+ begin_guard.add_argument("--server-state-dir", type=Path, required=True)
272
+ clear_guard = subcommands.add_parser(
273
+ "clear-host-cold-handoff",
274
+ help="clear one exact durable cold-handoff startup guard",
275
+ )
276
+ clear_guard.add_argument("--data-dir", type=Path, default=DEFAULT_DATA_DIR)
277
+ clear_guard.add_argument("--server-state-dir", type=Path, required=True)
278
+ clear_guard.add_argument("--expected-guard-id", required=True)
279
+ clear_guard.add_argument("--expected-device", type=int, required=True)
280
+ clear_guard.add_argument("--expected-inode", type=int, required=True)
281
+ identity_check = subcommands.add_parser(
282
+ "verify-server-identity",
283
+ help="verify the exact persisted AgentsServer identity without mutation",
284
+ )
285
+ identity_check.add_argument("--server-state-dir", type=Path, required=True)
286
+ identity_check.add_argument("--expected-identity", required=True)
287
+ abort_reactivation = subcommands.add_parser(
288
+ "abort-host-reactivation-preflight",
289
+ help="release an exact pre-takeover reactivation fence",
290
+ )
291
+ abort_reactivation.add_argument(
292
+ "--data-dir", type=Path, default=DEFAULT_DATA_DIR
293
+ )
294
+ abort_reactivation.add_argument("--server-state-dir", type=Path, required=True)
295
+ abort_reactivation.add_argument("--expected-hub-id", required=True)
296
+ abort_reactivation.add_argument("--expected-operation-id", required=True)
297
+ abort_reactivation.add_argument("--expected-snapshot", type=Path, required=True)
298
+ abort_reactivation.add_argument("--expected-device", type=int, required=True)
299
+ abort_reactivation.add_argument("--expected-inode", type=int, required=True)
300
+ adopt_reactivation = subcommands.add_parser(
301
+ "adopt-host-reactivation-preflight",
302
+ help="atomically adopt an exact prepared reactivation fence",
303
+ )
304
+ adopt_reactivation.add_argument(
305
+ "--data-dir", type=Path, default=DEFAULT_DATA_DIR
306
+ )
307
+ adopt_reactivation.add_argument("--server-state-dir", type=Path, required=True)
308
+ adopt_reactivation.add_argument("--expected-hub-id", required=True)
309
+ adopt_reactivation.add_argument("--expected-operation-id", required=True)
310
+ adopt_reactivation.add_argument("--expected-snapshot", type=Path, required=True)
311
+ adopt_reactivation.add_argument("--expected-device", type=int, required=True)
312
+ adopt_reactivation.add_argument("--expected-inode", type=int, required=True)
313
+ for command, help_text in (
314
+ (
315
+ "verify-host-reactivation-snapshot",
316
+ "verify an exact disabled-host reactivation snapshot",
317
+ ),
318
+ (
319
+ "restore-host-reactivation-snapshot",
320
+ "offline restore of an exact disabled-host reactivation snapshot",
321
+ ),
322
+ ):
323
+ reactivation_snapshot = subcommands.add_parser(command, help=help_text)
324
+ reactivation_snapshot.add_argument(
325
+ "--data-dir", type=Path, default=DEFAULT_DATA_DIR
326
+ )
327
+ reactivation_snapshot.add_argument("--snapshot", type=Path, required=True)
328
+ reactivation_snapshot.add_argument(
329
+ "--expected-host-identity", required=True
330
+ )
331
+ reactivation_snapshot.add_argument("--expected-hub-id", required=True)
332
+ reactivation_snapshot.add_argument(
333
+ "--expected-operation-id", required=True
334
+ )
335
+ return parser
336
+
337
+
338
+ def _loopback(value: str) -> bool:
339
+ try:
340
+ return ipaddress.ip_address(value).is_loopback
341
+ except ValueError:
342
+ return value.lower() == "localhost"
343
+
344
+
345
+ def main(argv: list[str] | None = None) -> int:
346
+ args = _parser().parse_args(argv)
347
+ try:
348
+ if args.command == "serve":
349
+ if not 1 <= args.port <= 65535:
350
+ raise ValueError("port must be between 1 and 65535")
351
+ if (args.ssl_certfile is None) != (args.ssl_keyfile is None):
352
+ raise ValueError("--ssl-certfile and --ssl-keyfile must be supplied together")
353
+ cert_and_key = args.ssl_certfile is not None and args.ssl_keyfile is not None
354
+ if not _loopback(args.host) and not cert_and_key:
355
+ raise ValueError(
356
+ "non-loopback Team Hub listeners require --ssl-certfile and --ssl-keyfile"
357
+ )
358
+ if cert_and_key:
359
+ validate_tls_files(args.ssl_certfile, args.ssl_keyfile)
360
+ allowed_hosts = set(args.allowed_host)
361
+ allowed_hosts.add(args.host)
362
+ if _loopback(args.host):
363
+ allowed_hosts.update({"127.0.0.1", "localhost", "[::1]", "::1"})
364
+ lease = HubStore.acquire_managed_runtime_lease(args.data_dir)
365
+ try:
366
+ app = create_app(
367
+ args.data_dir,
368
+ allowed_hosts=allowed_hosts,
369
+ allowed_origins=set(args.allowed_origin),
370
+ )
371
+ store: HubStore = app.state.store
372
+ scheme = "https" if cert_and_key else "http"
373
+ print(f"Team Hub {store.hub_id} listening at {scheme}://{args.host}:{args.port}")
374
+ if store.health()["bootstrap_required"]:
375
+ print(
376
+ "Initial-owner proof file: "
377
+ f"{store.bootstrap_proof_path} (the secret itself is never printed)"
378
+ )
379
+ uvicorn.run(
380
+ app,
381
+ host=args.host,
382
+ port=args.port,
383
+ ssl_certfile=str(args.ssl_certfile) if args.ssl_certfile else None,
384
+ ssl_keyfile=str(args.ssl_keyfile) if args.ssl_keyfile else None,
385
+ proxy_headers=False,
386
+ server_header=False,
387
+ )
388
+ finally:
389
+ HubStore.release_managed_runtime_lease(lease)
390
+ return 0
391
+ if args.command in {
392
+ "verify-snapshot",
393
+ "rebase-snapshot",
394
+ "restore-snapshot",
395
+ }:
396
+ operation = {
397
+ "verify-snapshot": HubStore.verify_maintenance_snapshot,
398
+ "rebase-snapshot": HubStore.rebase_maintenance_snapshot,
399
+ "restore-snapshot": HubStore.restore_maintenance_snapshot,
400
+ }[args.command]
401
+ operation(
402
+ args.data_dir,
403
+ args.snapshot,
404
+ expected_host_identity=args.expected_host_identity,
405
+ expected_hub_id=args.expected_hub_id,
406
+ expected_operation_id=args.expected_operation_id,
407
+ )
408
+ print(args.snapshot)
409
+ return 0
410
+ if args.command in {
411
+ "confirm-restored-snapshot",
412
+ "acknowledge-restored-snapshot",
413
+ "confirm-restored-host-reactivation-snapshot",
414
+ "acknowledge-restored-host-reactivation-snapshot",
415
+ }:
416
+ host_reactivation = "host-reactivation" in args.command
417
+ expected_reason = (
418
+ "host-reactivation" if host_reactivation else "server-update"
419
+ )
420
+ if args.command.startswith("confirm-"):
421
+ HubStore.confirm_restored_maintenance_snapshot(
422
+ args.data_dir,
423
+ args.snapshot,
424
+ expected_host_identity=args.expected_host_identity,
425
+ expected_hub_id=args.expected_hub_id,
426
+ expected_operation_id=args.expected_operation_id,
427
+ expected_reason=expected_reason,
428
+ )
429
+ else:
430
+ HubStore.acknowledge_restored_maintenance_snapshot(
431
+ args.data_dir,
432
+ args.snapshot,
433
+ expected_host_identity=args.expected_host_identity,
434
+ expected_hub_id=args.expected_hub_id,
435
+ expected_operation_id=args.expected_operation_id,
436
+ expected_reason=expected_reason,
437
+ allow_missing=args.allow_missing,
438
+ )
439
+ print(args.snapshot)
440
+ return 0
441
+ if args.command in {
442
+ "publish-fenced-start-authority",
443
+ "clear-fenced-start-authority",
444
+ }:
445
+ common = {
446
+ "expected_host_identity": args.expected_host_identity,
447
+ "expected_hub_id": args.expected_hub_id,
448
+ "expected_reason": args.expected_reason,
449
+ "expected_operation_id": args.expected_operation_id,
450
+ "expected_snapshot": args.snapshot,
451
+ }
452
+ if args.command == "publish-fenced-start-authority":
453
+ HubStore.publish_managed_startup_authority(
454
+ args.data_dir,
455
+ **common,
456
+ )
457
+ elif not HubStore.clear_managed_startup_authority(
458
+ args.data_dir,
459
+ **common,
460
+ ) and not args.allow_missing:
461
+ raise RuntimeError("exact Team Hub startup authority is missing")
462
+ print(args.snapshot)
463
+ return 0
464
+ if args.command == "prepare-host-reactivation":
465
+ identity, identity_path = _reactivation_server_identity(
466
+ args.server_state_dir
467
+ )
468
+ (
469
+ hub_id,
470
+ snapshot,
471
+ operation_id,
472
+ fence_device,
473
+ fence_inode,
474
+ ) = HubStore.prepare_managed_host_reactivation(
475
+ args.data_dir,
476
+ expected_host_identity=identity,
477
+ )
478
+ try:
479
+ persisted = _persist_server_identity(identity_path, identity)
480
+ print(persisted)
481
+ print(hub_id)
482
+ print(operation_id)
483
+ print(snapshot)
484
+ print(fence_device)
485
+ print(fence_inode)
486
+ except BaseException as output_error:
487
+ try:
488
+ cleared = HubStore.abort_prepared_host_reactivation(
489
+ args.data_dir,
490
+ expected_host_identity=identity,
491
+ expected_hub_id=hub_id,
492
+ expected_operation_id=operation_id,
493
+ expected_snapshot=snapshot,
494
+ expected_device=fence_device,
495
+ expected_inode=fence_inode,
496
+ )
497
+ if not cleared:
498
+ raise RuntimeError(
499
+ "exact Team Hub reactivation fence is missing"
500
+ )
501
+ except BaseException as cleanup_error:
502
+ raise RuntimeError(
503
+ "Team Hub reactivation preflight failed after fencing; "
504
+ "the source remains fail-closed"
505
+ ) from cleanup_error
506
+ raise output_error
507
+ return 0
508
+ if args.command == "begin-host-cold-handoff":
509
+ identity, _identity_path = _reactivation_server_identity(
510
+ args.server_state_dir
511
+ )
512
+ guard_id, guard_device, guard_inode = (
513
+ HubStore.begin_managed_startup_guard(
514
+ args.data_dir,
515
+ expected_host_identity=identity,
516
+ )
517
+ )
518
+ print(identity)
519
+ print(guard_id)
520
+ print(guard_device)
521
+ print(guard_inode)
522
+ return 0
523
+ if args.command == "clear-host-cold-handoff":
524
+ identity, _identity_path = _reactivation_server_identity(
525
+ args.server_state_dir
526
+ )
527
+ if not HubStore.clear_managed_startup_guard(
528
+ args.data_dir,
529
+ expected_host_identity=identity,
530
+ expected_guard_id=args.expected_guard_id,
531
+ expected_device=args.expected_device,
532
+ expected_inode=args.expected_inode,
533
+ ):
534
+ raise RuntimeError("exact Team Hub startup guard is missing")
535
+ print(identity)
536
+ return 0
537
+ if args.command == "adopt-host-reactivation-preflight":
538
+ identity, _identity_path = _reactivation_server_identity(
539
+ args.server_state_dir
540
+ )
541
+ HubStore.adopt_prepared_host_reactivation(
542
+ args.data_dir,
543
+ expected_host_identity=identity,
544
+ expected_hub_id=args.expected_hub_id,
545
+ expected_operation_id=args.expected_operation_id,
546
+ expected_snapshot=args.expected_snapshot,
547
+ expected_device=args.expected_device,
548
+ expected_inode=args.expected_inode,
549
+ )
550
+ print(identity)
551
+ return 0
552
+ if args.command == "verify-server-identity":
553
+ expected = str(args.expected_identity)
554
+ if SERVER_IDENTITY_RE.fullmatch(expected) is None:
555
+ raise RuntimeError("expected AgentsServer identity is invalid")
556
+ root = Path(
557
+ os.path.abspath(
558
+ os.path.expanduser(os.fspath(args.server_state_dir))
559
+ )
560
+ )
561
+ persisted = _read_server_identity_file(root / "server-identity")
562
+ if persisted is None or not secrets.compare_digest(persisted, expected):
563
+ raise RuntimeError(
564
+ "AgentsServer identity changed during Team Hub activation"
565
+ )
566
+ print(persisted)
567
+ return 0
568
+ if args.command == "abort-host-reactivation-preflight":
569
+ identity, _identity_path = _reactivation_server_identity(
570
+ args.server_state_dir
571
+ )
572
+ if not HubStore.abort_prepared_host_reactivation(
573
+ args.data_dir,
574
+ expected_host_identity=identity,
575
+ expected_hub_id=args.expected_hub_id,
576
+ expected_operation_id=args.expected_operation_id,
577
+ expected_snapshot=args.expected_snapshot,
578
+ expected_device=args.expected_device,
579
+ expected_inode=args.expected_inode,
580
+ ):
581
+ raise RuntimeError(
582
+ "exact Team Hub reactivation preflight fence is missing"
583
+ )
584
+ print(identity)
585
+ return 0
586
+ if args.command == "verify-host-reactivation-snapshot":
587
+ HubStore.verify_host_reactivation_snapshot(
588
+ args.data_dir,
589
+ args.snapshot,
590
+ expected_host_identity=args.expected_host_identity,
591
+ expected_hub_id=args.expected_hub_id,
592
+ expected_operation_id=args.expected_operation_id,
593
+ )
594
+ print(args.snapshot)
595
+ return 0
596
+ if args.command == "restore-host-reactivation-snapshot":
597
+ HubStore.restore_host_reactivation_snapshot(
598
+ args.data_dir,
599
+ args.snapshot,
600
+ expected_host_identity=args.expected_host_identity,
601
+ expected_hub_id=args.expected_hub_id,
602
+ expected_operation_id=args.expected_operation_id,
603
+ )
604
+ print(args.snapshot)
605
+ return 0
606
+ # Local control commands remain available for a managed-bound Hub,
607
+ # while the standalone listener above refuses to serve that database.
608
+ # The same bounded interprocess lock fences proof-file changes from an
609
+ # update/restart snapshot.
610
+ with HubStore.maintenance_control_lock(args.data_dir):
611
+ store = HubStore(
612
+ args.data_dir,
613
+ allow_bound_control=True,
614
+ maintenance_control_locked=True,
615
+ )
616
+ if store.maintenance_fence() is not None:
617
+ raise RuntimeError(
618
+ "Team Hub local control is unavailable during managed maintenance"
619
+ )
620
+ if args.command == "bootstrap-proof":
621
+ path = store.renew_bootstrap_proof()
622
+ print(path)
623
+ return 0
624
+ if args.command == "owner-recovery":
625
+ path = store.issue_owner_recovery(
626
+ args.email,
627
+ args.device_label,
628
+ team_id=args.team_id,
629
+ )
630
+ print(path)
631
+ return 0
632
+ if args.command == "device-recovery":
633
+ path = store.issue_device_recovery(
634
+ args.email,
635
+ args.device_label,
636
+ team_id=args.team_id,
637
+ )
638
+ print(path)
639
+ return 0
640
+ except (HubError, RuntimeError, ValueError, PermissionError) as exc:
641
+ print(f"Team Hub: {exc}", file=sys.stderr)
642
+ return 2
643
+ return 2
644
+
645
+
646
+ if __name__ == "__main__":
647
+ raise SystemExit(main())