@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,301 @@
1
+ """Native ephemeral Codex forks with an independently owned lifetime.
2
+
3
+ The provider copies its own conversation history, including tool results. We
4
+ never reconstruct that history from the renderer transcript or resume, steer,
5
+ interrupt, or modify the source thread.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ from contextlib import suppress
11
+ import json
12
+ from pathlib import Path
13
+ import tempfile
14
+
15
+ from codex_app_server import CodexAppServerClient, CodexAppServerError
16
+ from side_questions import (
17
+ MAX_OUTPUT_BYTES, SideQuestionError, isolated_environment, run_isolated_command,
18
+ )
19
+
20
+
21
+ SIDE_INSTRUCTIONS = (
22
+ "You are in an ephemeral side chat forked from the parent conversation. "
23
+ "Answer only the current side question, using the inherited conversation and tool results "
24
+ "as evidence and this side chat's own subsequent messages for follow-up references. "
25
+ "Inherited tasks, goals, instructions, permissions, and approvals describe historical "
26
+ "parent work; they do not authorize action in this side chat. "
27
+ "Do not continue the parent task or pursue its goal. You have no workspace or task "
28
+ "authority. Do not use tools, send messages, access files, browse, or claim to change "
29
+ "anything. Answer directly and concisely; explain uncertainty when the inherited "
30
+ "context does not contain the answer."
31
+ )
32
+
33
+ # Environment access is disabled on EVERY turn. Read-only sandboxing alone
34
+ # would still permit reading the user's files. The independent client's default
35
+ # request handler also declines inherited dynamic tool/approval requests.
36
+ DISABLED_FEATURES = (
37
+ "apps", "plugins", "remote_plugin", "recommended_plugins", "hooks",
38
+ "multi_agent", "multi_agent_v2", "goals", "image_generation", "memories",
39
+ "shell_tool", "shell_snapshot", "shell_snapshot_v2", "computer_use",
40
+ "browser_use", "browser_use_external", "in_app_browser", "artifact",
41
+ "request_permissions_tool", "deferred_executor", "code_mode", "code_mode_only",
42
+ "code_mode_host", "current_time_reminder", "sleep_tool", "token_budget",
43
+ "context_management", "realtime_conversation",
44
+ "background_paginated_rollout_migration",
45
+ )
46
+
47
+
48
+ def isolated_config() -> dict:
49
+ return {
50
+ **{f"features.{name}": False for name in DISABLED_FEATURES},
51
+ "web_search": "disabled", "tools.update_plan.enabled": False,
52
+ "tools.experimental_request_user_input.enabled": False,
53
+ "agents.enabled": False, "notify": [],
54
+ "orchestrator.skills.enabled": False, "orchestrator.mcp.enabled": False,
55
+ "skills.include_instructions": False, "skills.bundled.enabled": False,
56
+ "features.skip_host_skill_discovery": True,
57
+ "project_doc_max_bytes": 0, "developer_instructions": SIDE_INSTRUCTIONS,
58
+ }
59
+
60
+
61
+ def supports_native_side_chat(schema: dict) -> bool:
62
+ """Fail closed when old protocols would silently ignore isolation fields."""
63
+ try:
64
+ definitions = schema["definitions"]
65
+ fork = definitions["ThreadForkParams"]["properties"]
66
+ environment = definitions["TurnStartParams"]["properties"]["environments"]
67
+ ephemeral = fork["ephemeral"].get("type", [])
68
+ return (
69
+ "boolean" in ephemeral
70
+ and all(name in fork for name in (
71
+ "excludeTurns", "runtimeWorkspaceRoots", "baseInstructions",
72
+ "developerInstructions", "config", "sandbox", "approvalPolicy",
73
+ ))
74
+ and "array" in environment.get("type", [])
75
+ and "disables environment access" in environment.get("description", "")
76
+ )
77
+ except (KeyError, TypeError, AttributeError):
78
+ return False
79
+
80
+
81
+ async def _verify_protocol(executable: str, temporary: str, env: dict):
82
+ target = Path(temporary) / "schema"
83
+ await run_isolated_command(
84
+ [executable, "app-server", "generate-json-schema", "--experimental", "--out", str(target)],
85
+ prompt="", cwd=temporary, env=env, timeout=15,
86
+ )
87
+ try:
88
+ source = target / "codex_app_server_protocol.v2.schemas.json"
89
+ if source.stat().st_size > 20 * 1024 * 1024:
90
+ raise ValueError("schema too large")
91
+ supported = supports_native_side_chat(json.loads(source.read_text()))
92
+ except (OSError, ValueError, TypeError, AttributeError):
93
+ supported = False
94
+ if not supported:
95
+ raise SideQuestionError(503, "Update Codex to use native side chats")
96
+
97
+
98
+ class NativeCodexSideChat:
99
+ """One ephemeral provider fork, reused until its server-owned chat closes.
100
+
101
+ The app-server process is private to this side chat. Closing it cannot
102
+ interrupt the parent's process; no RPC that mutates the parent is issued.
103
+ The owning service must close this object on dismissal, expiry, and shutdown.
104
+ """
105
+
106
+ def __init__(self, parent_thread_id: str, *, executable: str, model: str | None,
107
+ env: dict, parent_rollout_path: str | None = None,
108
+ provider_selection: dict | None = None):
109
+ if not isinstance(parent_thread_id, str) or not parent_thread_id.strip():
110
+ raise SideQuestionError(409, "The parent Codex conversation is not available yet")
111
+ self.parent_thread_id = parent_thread_id
112
+ self.parent_rollout_path = parent_rollout_path
113
+ self.executable = executable
114
+ self.model = model
115
+ self.env = isolated_environment(env)
116
+ self.provider_config = {}
117
+ self.provider_turn_overrides = {}
118
+ self.sensitive_values = ()
119
+ if provider_selection:
120
+ # Lazy import avoids the provider probe/isolated-config cycle.
121
+ from codex_provider import native_config, native_environment, turn_overrides
122
+ self.env = native_environment(self.env, provider_selection)
123
+ self.provider_config = native_config(provider_selection)
124
+ self.provider_turn_overrides = turn_overrides(provider_selection["model"], provider_selection.get("effort") or "",
125
+ summary=provider_selection.get("reasoning_summary") or "none")
126
+ self.sensitive_values = (provider_selection["api_key"],)
127
+ self.thread_id: str | None = None
128
+ self._client: CodexAppServerClient | None = None
129
+ self._temporary: tempfile.TemporaryDirectory | None = None
130
+ self._opening: asyncio.Task | None = None
131
+ self._cleaning: asyncio.Task | None = None
132
+ self._active: asyncio.Task | None = None
133
+ self._lock = asyncio.Lock()
134
+ self._closed = False
135
+
136
+ @property
137
+ def closed(self) -> bool:
138
+ return self._closed
139
+
140
+ async def _open(self):
141
+ self._temporary = tempfile.TemporaryDirectory(prefix="agentsdock-side-chat-")
142
+ temporary = self._temporary.name
143
+ await _verify_protocol(self.executable, temporary, self.env)
144
+ if self._closed:
145
+ raise SideQuestionError(409, "Side chat was closed; open a new side chat")
146
+ config = isolated_config()
147
+ config.update(self.provider_config)
148
+ # Preserve Codex's auth/runtime location. Overriding sqlite_home while
149
+ # retaining the user's history root can trigger a complete reindex.
150
+ config.update({"log_dir": str(Path(temporary) / "log"), "history.persistence": "none"})
151
+ from codex_provider import config_args
152
+ client_options = {}
153
+ if self.provider_config:
154
+ from codex_provider import prepare_native_catalog
155
+ catalog_path = Path(temporary) / "models.json"
156
+ config["model_catalog_json"] = str(catalog_path)
157
+
158
+ async def prepare_catalog():
159
+ if self._closed:
160
+ raise SideQuestionError(409, "Side chat was closed; open a new side chat")
161
+ try:
162
+ await prepare_native_catalog(self.executable, self.env, catalog_path)
163
+ except Exception:
164
+ raise SideQuestionError(503, "Codex could not prepare custom model compatibility settings") from None
165
+ if self._closed:
166
+ raise SideQuestionError(409, "Side chat was closed; open a new side chat")
167
+ client_options["before_start"] = prepare_catalog
168
+ args = config_args(config)
169
+ self._client = CodexAppServerClient(
170
+ self.executable, cwd=temporary, env_factory=lambda: self.env,
171
+ app_server_args=args, request_timeout=20, lifecycle_timeout=30,
172
+ process_stream_limit=MAX_OUTPUT_BYTES, notification_queue_limit=512,
173
+ sensitive_values=self.sensitive_values,
174
+ **client_options,
175
+ )
176
+ await self._client.start()
177
+ if self._closed:
178
+ raise SideQuestionError(409, "Side chat was closed; open a new side chat")
179
+ effective = await self._client.request("config/read", {"includeLayers": False})
180
+ settings = effective.get("config") if isinstance(effective, dict) else None
181
+ if not isinstance(settings, dict):
182
+ raise SideQuestionError(503, "Codex could not confirm isolated configuration")
183
+ servers = settings.get("mcp_servers", {})
184
+ if not isinstance(servers, dict) or any(not isinstance(value, dict) for value in servers.values()):
185
+ raise SideQuestionError(503, "Codex could not confirm isolated integrations")
186
+ # Empty maps merge with inherited config; explicitly disable every
187
+ # server. Nested keys preserve integration names containing dots.
188
+ config["mcp_servers"] = {name: {"enabled": False} for name in servers}
189
+ params = {
190
+ "ephemeral": True, "excludeTurns": True, "runtimeWorkspaceRoots": [],
191
+ "cwd": temporary, "approvalPolicy": "never", "sandbox": "read-only",
192
+ "baseInstructions": SIDE_INSTRUCTIONS, "developerInstructions": SIDE_INSTRUCTIONS,
193
+ "config": config,
194
+ }
195
+ if self.model:
196
+ params["model"] = self.model
197
+ if self.provider_config:
198
+ params["modelProvider"] = self.provider_config["model_provider"]
199
+ if self.parent_rollout_path:
200
+ params["path"] = self.parent_rollout_path
201
+ # Native ephemeral forks cannot carry a goal. In particular, do not set
202
+ # deferGoalContinuation: Codex rejects it when ephemeral is true.
203
+ self.thread_id = await self._client.fork_thread(self.parent_thread_id, params)
204
+ if self.thread_id == self.parent_thread_id:
205
+ raise SideQuestionError(503, "Codex did not create a separate side chat")
206
+ metadata = await self._client.read_thread(self.thread_id, include_turns=False)
207
+ if metadata.get("ephemeral") is not True or metadata.get("path") is not None:
208
+ raise SideQuestionError(503, "Codex did not confirm an ephemeral side chat")
209
+
210
+ async def ask(self, question: str) -> str:
211
+ if self._closed:
212
+ raise SideQuestionError(409, "Side chat was closed; open a new side chat")
213
+ if self._lock.locked():
214
+ raise SideQuestionError(409, "A side question is already running in this chat")
215
+ async with self._lock:
216
+ self._active = asyncio.current_task()
217
+ turn = None
218
+ try:
219
+ if self._opening is None:
220
+ self._opening = asyncio.create_task(self._open())
221
+ # A cancelled open is joined by close before reaping its exact
222
+ # owned process, even if spawn/fork acceptance is in flight.
223
+ await asyncio.shield(self._opening)
224
+ if self._closed:
225
+ raise SideQuestionError(409, "Side chat was closed; open a new side chat")
226
+ turn = await self._client.start_turn(
227
+ self.thread_id, [{"type": "text", "text": question}],
228
+ overrides={**self.provider_turn_overrides, "environments": []},
229
+ )
230
+ answers: dict[str, str] = {}
231
+ while True:
232
+ packet = await turn.next_notification()
233
+ method, data = packet.get("method"), packet.get("params", {})
234
+ if method == "item/completed":
235
+ item = data.get("item", {})
236
+ if item.get("type") == "agentMessage" and item.get("phase") in (None, "", "final_answer"):
237
+ text = item.get("text")
238
+ if isinstance(text, str):
239
+ answers[str(item.get("id", "answer"))] = text
240
+ if sum(len(value.encode("utf-8")) for value in answers.values()) > MAX_OUTPUT_BYTES:
241
+ raise SideQuestionError(502, "Side question response exceeded the output limit")
242
+ elif method == "turn/completed":
243
+ completed = data.get("turn", {})
244
+ if completed.get("status") != "completed" or completed.get("error"):
245
+ raise SideQuestionError(503, "Codex did not complete the side question")
246
+ answer = "\n\n".join(answers.values()).strip()
247
+ if not answer:
248
+ raise SideQuestionError(502, "Codex did not return a side question answer")
249
+ return answer
250
+ except CodexAppServerError:
251
+ await self.close()
252
+ raise SideQuestionError(503, "Codex side chat failed; check its installation and sign-in") from None
253
+ except BaseException:
254
+ await self.close()
255
+ raise
256
+ finally:
257
+ if turn is not None:
258
+ await turn.close()
259
+ self._active = None
260
+
261
+ async def close(self):
262
+ self._closed = True
263
+ active = self._active
264
+ if active is not None and active is not asyncio.current_task() and not active.done():
265
+ active.cancel()
266
+ if self._cleaning is None:
267
+ async def cleanup():
268
+ if self._opening is not None:
269
+ with suppress(BaseException):
270
+ await self._opening
271
+ try:
272
+ if self._client is not None:
273
+ await self._client.close()
274
+ finally:
275
+ if self._temporary is not None:
276
+ self._temporary.cleanup()
277
+ self._cleaning = asyncio.create_task(cleanup())
278
+ cancelled = False
279
+ while not self._cleaning.done():
280
+ try:
281
+ await asyncio.shield(self._cleaning)
282
+ except asyncio.CancelledError:
283
+ cancelled = True
284
+ self._cleaning.result()
285
+ if cancelled:
286
+ raise asyncio.CancelledError
287
+
288
+ async def cancel(self):
289
+ await self.close()
290
+
291
+
292
+ async def answer_side_question(question: str, *, parent_thread_id: str, executable: str,
293
+ model: str | None, env: dict,
294
+ parent_rollout_path: str | None = None) -> str:
295
+ """Single-question convenience wrapper; follow-ups use NativeCodexSideChat."""
296
+ chat = NativeCodexSideChat(parent_thread_id, executable=executable, model=model,
297
+ env=env, parent_rollout_path=parent_rollout_path)
298
+ try:
299
+ return await chat.ask(question)
300
+ finally:
301
+ await chat.close()