@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,347 @@
1
+ """Small parent-death guard for a Cursor CLI subprocess.
2
+
3
+ AgentsServer launches this helper in its own process group. The helper launches
4
+ Cursor in a child group, forwards inherited stdio unchanged, and terminates the
5
+ entire Cursor tree when its leader exits or AgentsServer disappears abruptly.
6
+ This prevents a detached agent from continuing workspace edits after the turn.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import signal
13
+ import subprocess
14
+ import sys
15
+ import time
16
+ from threading import Event
17
+ from typing import NamedTuple
18
+
19
+
20
+ POLL_SECONDS = 0.02
21
+ DESCENDANT_SNAPSHOT_INTERVAL_SECONDS = 0.1
22
+ TERMINATE_GRACE_SECONDS = 0.5
23
+ # The server gives this wrapper a bounded grace window before killing it. Keep
24
+ # each process-table probe well below that outer deadline even if ``ps`` wedges.
25
+ PROCESS_SNAPSHOT_TIMEOUT_SECONDS = 0.2
26
+ MAX_PROCESS_SNAPSHOT_ROWS = 32_768
27
+ MAX_CAPTURED_DESCENDANTS = 4_096
28
+
29
+
30
+ class _ProcessIdentity(NamedTuple):
31
+ pid: int
32
+ parent_pid: int
33
+ process_group: int
34
+ started_at: str
35
+
36
+
37
+ def _process_snapshot() -> dict[int, _ProcessIdentity]:
38
+ """Return one bounded POSIX process snapshot with stable-enough identities.
39
+
40
+ ``lstart`` is intentionally retained as a PID-reuse fence. Before sending
41
+ an individual signal we require both the PID and captured start marker to
42
+ match a fresh snapshot, so a descendant that exits during the grace period
43
+ cannot cause its recycled PID to be killed.
44
+ """
45
+
46
+ ps_path = next(
47
+ (candidate for candidate in ("/bin/ps", "/usr/bin/ps") if os.path.isfile(candidate)),
48
+ None,
49
+ )
50
+ if ps_path is None:
51
+ return {}
52
+ env = dict(os.environ)
53
+ env["LC_ALL"] = "C"
54
+ try:
55
+ result = subprocess.run(
56
+ [ps_path, "-axo", "pid=,ppid=,pgid=,lstart="],
57
+ text=True,
58
+ capture_output=True,
59
+ timeout=PROCESS_SNAPSHOT_TIMEOUT_SECONDS,
60
+ check=False,
61
+ env=env,
62
+ )
63
+ except (OSError, subprocess.SubprocessError):
64
+ return {}
65
+ if result.returncode != 0:
66
+ return {}
67
+
68
+ snapshot: dict[int, _ProcessIdentity] = {}
69
+ for index, raw_line in enumerate(result.stdout.splitlines()):
70
+ if index >= MAX_PROCESS_SNAPSHOT_ROWS:
71
+ break
72
+ parts = raw_line.strip().split(None, 3)
73
+ if len(parts) != 4:
74
+ continue
75
+ try:
76
+ pid, parent_pid, process_group = map(int, parts[:3])
77
+ except ValueError:
78
+ continue
79
+ if pid <= 1 or parent_pid < 0 or process_group <= 0:
80
+ continue
81
+ snapshot[pid] = _ProcessIdentity(
82
+ pid,
83
+ parent_pid,
84
+ process_group,
85
+ parts[3],
86
+ )
87
+ return snapshot
88
+
89
+
90
+ def _recursive_descendants(
91
+ root_pid: int,
92
+ snapshot: dict[int, _ProcessIdentity],
93
+ ) -> tuple[_ProcessIdentity, ...]:
94
+ return _recursive_descendants_from_roots((root_pid,), snapshot)
95
+
96
+
97
+ def _recursive_descendants_from_roots(
98
+ root_pids: tuple[int, ...],
99
+ snapshot: dict[int, _ProcessIdentity],
100
+ ) -> tuple[_ProcessIdentity, ...]:
101
+ children: dict[int, list[_ProcessIdentity]] = {}
102
+ for identity in snapshot.values():
103
+ children.setdefault(identity.parent_pid, []).append(identity)
104
+
105
+ descendants: list[_ProcessIdentity] = []
106
+ pending = list(root_pids)
107
+ seen = set(root_pids)
108
+ while pending and len(descendants) < MAX_CAPTURED_DESCENDANTS:
109
+ parent_pid = pending.pop()
110
+ for identity in children.get(parent_pid, ()):
111
+ if identity.pid in seen:
112
+ continue
113
+ seen.add(identity.pid)
114
+ descendants.append(identity)
115
+ pending.append(identity.pid)
116
+ if len(descendants) >= MAX_CAPTURED_DESCENDANTS:
117
+ break
118
+ return tuple(descendants)
119
+
120
+
121
+ def _matching_survivors(
122
+ identities: tuple[_ProcessIdentity, ...],
123
+ snapshot: dict[int, _ProcessIdentity],
124
+ ) -> tuple[_ProcessIdentity, ...]:
125
+ return tuple(
126
+ identity
127
+ for identity in identities
128
+ if (
129
+ (current := snapshot.get(identity.pid)) is not None
130
+ and current.started_at == identity.started_at
131
+ )
132
+ )
133
+
134
+
135
+ def _signal_identities(
136
+ identities: tuple[_ProcessIdentity, ...],
137
+ signum: int,
138
+ snapshot: dict[int, _ProcessIdentity],
139
+ ) -> None:
140
+ guard_pid = os.getpid()
141
+ parent_pid = os.getppid()
142
+ for identity in _matching_survivors(identities, snapshot):
143
+ if identity.pid in {guard_pid, parent_pid}:
144
+ continue
145
+ try:
146
+ os.kill(identity.pid, signum)
147
+ except (OSError, ProcessLookupError):
148
+ pass
149
+
150
+
151
+ def _track_child_processes(
152
+ child_pid: int,
153
+ child_group: int,
154
+ known: dict[int, _ProcessIdentity],
155
+ snapshot: dict[int, _ProcessIdentity],
156
+ *,
157
+ child_alive: bool,
158
+ ) -> None:
159
+ """Accumulate bounded child identities before they can be reparented.
160
+
161
+ Same-group processes remain discoverable after the Cursor leader exits.
162
+ Detached descendants do not, so every still-matching captured descendant is
163
+ also treated as a root while the leader is alive. This follows children of
164
+ a previously detached tool without trusting a recycled PID.
165
+ """
166
+
167
+ roots: list[int] = [child_pid] if child_alive else []
168
+ roots.extend(
169
+ identity.pid
170
+ for identity in known.values()
171
+ if (
172
+ (current := snapshot.get(identity.pid)) is not None
173
+ and current.started_at == identity.started_at
174
+ )
175
+ )
176
+ # A normal leader exit reparents children before the next snapshot, but
177
+ # members of its dedicated process group remain attributable to this run.
178
+ discovered: dict[int, _ProcessIdentity] = {}
179
+ for identity in snapshot.values():
180
+ if identity.pid != child_pid and identity.process_group == child_group:
181
+ discovered.setdefault(identity.pid, identity)
182
+ if len(discovered) >= MAX_CAPTURED_DESCENDANTS:
183
+ break
184
+
185
+ for identity in _recursive_descendants_from_roots(
186
+ tuple(roots),
187
+ snapshot,
188
+ ):
189
+ discovered.setdefault(identity.pid, identity)
190
+ if len(discovered) >= MAX_CAPTURED_DESCENDANTS:
191
+ break
192
+
193
+ for identity in discovered.values():
194
+ if len(known) >= MAX_CAPTURED_DESCENDANTS:
195
+ break
196
+ previous = known.get(identity.pid)
197
+ if previous is None or previous.started_at == identity.started_at:
198
+ known[identity.pid] = identity
199
+
200
+
201
+ def _terminate_child_group(
202
+ child: subprocess.Popen[bytes],
203
+ group: int,
204
+ tracked_descendants: tuple[_ProcessIdentity, ...] = (),
205
+ ) -> None:
206
+ known = {identity.pid: identity for identity in tracked_descendants}
207
+
208
+ initial_snapshot = _process_snapshot()
209
+ _track_child_processes(
210
+ child.pid,
211
+ group,
212
+ known,
213
+ initial_snapshot,
214
+ child_alive=child.poll() is None,
215
+ )
216
+ descendants = tuple(known.values())
217
+ detached = tuple(
218
+ identity for identity in descendants if identity.process_group != group
219
+ )
220
+ group_members = tuple(
221
+ identity for identity in descendants if identity.process_group == group
222
+ )
223
+ if child.poll() is None or group_members:
224
+ try:
225
+ os.killpg(group, signal.SIGTERM)
226
+ except (OSError, ProcessLookupError):
227
+ pass
228
+ # A child can use setsid()/setpgid() for a background shell and escape the
229
+ # Cursor process group. Signal only descendants captured before teardown,
230
+ # and revalidate their start marker immediately before each signal.
231
+ _signal_identities(detached, signal.SIGTERM, _process_snapshot())
232
+
233
+ deadline = time.monotonic() + TERMINATE_GRACE_SECONDS
234
+ survivors: tuple[_ProcessIdentity, ...] = descendants
235
+ while time.monotonic() < deadline:
236
+ snapshot = _process_snapshot()
237
+ _track_child_processes(
238
+ child.pid,
239
+ group,
240
+ known,
241
+ snapshot,
242
+ child_alive=child.poll() is None,
243
+ )
244
+ descendants = tuple(known.values())
245
+ survivors = _matching_survivors(descendants, snapshot)
246
+ if child.poll() is not None and not survivors:
247
+ return
248
+ time.sleep(0.05)
249
+
250
+ final_snapshot = _process_snapshot()
251
+ _track_child_processes(
252
+ child.pid,
253
+ group,
254
+ known,
255
+ final_snapshot,
256
+ child_alive=child.poll() is None,
257
+ )
258
+ descendants = tuple(known.values())
259
+ survivors = _matching_survivors(descendants, final_snapshot)
260
+ group_survives = child.poll() is None or any(
261
+ final_snapshot[identity.pid].process_group == group
262
+ for identity in survivors
263
+ )
264
+ if group_survives:
265
+ try:
266
+ os.killpg(group, signal.SIGKILL)
267
+ except (OSError, ProcessLookupError):
268
+ pass
269
+ _signal_identities(
270
+ tuple(
271
+ identity
272
+ for identity in survivors
273
+ if final_snapshot[identity.pid].process_group != group
274
+ ),
275
+ signal.SIGKILL,
276
+ final_snapshot,
277
+ )
278
+
279
+
280
+ def main(argv: list[str]) -> int:
281
+ if len(argv) < 4 or argv[0] != "--parent-pid" or argv[2] != "--":
282
+ return 64
283
+ try:
284
+ expected_parent = int(argv[1])
285
+ except ValueError:
286
+ return 64
287
+ if expected_parent <= 1:
288
+ return 64
289
+ command = argv[3:]
290
+ if not command:
291
+ return 64
292
+
293
+ stop = Event()
294
+
295
+ def request_stop(_signum: int, _frame: object) -> None:
296
+ stop.set()
297
+
298
+ signal.signal(signal.SIGTERM, request_stop)
299
+ signal.signal(signal.SIGINT, request_stop)
300
+ signal.signal(signal.SIGHUP, request_stop)
301
+
302
+ # The server can disappear after spawning this wrapper but before Python
303
+ # reaches Popen. Do not briefly launch an editing agent in that window.
304
+ if stop.is_set() or os.getppid() != expected_parent:
305
+ return 143
306
+
307
+ try:
308
+ child = subprocess.Popen(command, start_new_session=True)
309
+ except OSError:
310
+ return 127
311
+
312
+ interrupted = False
313
+ child_group = child.pid
314
+ tracked_descendants: dict[int, _ProcessIdentity] = {}
315
+ next_descendant_snapshot = 0.0
316
+ while True:
317
+ now = time.monotonic()
318
+ child_alive = child.poll() is None
319
+ if now >= next_descendant_snapshot:
320
+ snapshot = _process_snapshot()
321
+ child_alive = child.poll() is None
322
+ _track_child_processes(
323
+ child.pid,
324
+ child_group,
325
+ tracked_descendants,
326
+ snapshot,
327
+ child_alive=child_alive,
328
+ )
329
+ next_descendant_snapshot = (
330
+ now + DESCENDANT_SNAPSHOT_INTERVAL_SECONDS
331
+ )
332
+ if not child_alive:
333
+ break
334
+ if stop.is_set() or os.getppid() != expected_parent:
335
+ interrupted = True
336
+ break
337
+ time.sleep(POLL_SECONDS)
338
+ _terminate_child_group(
339
+ child,
340
+ child_group,
341
+ tuple(tracked_descendants.values()),
342
+ )
343
+ return 143 if interrupted else int(child.returncode or 0)
344
+
345
+
346
+ if __name__ == "__main__":
347
+ raise SystemExit(main(sys.argv[1:]))