@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,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())
|