@agentsdock/server 1.0.11-beta.1 → 1.0.11-beta.2

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.
@@ -0,0 +1,322 @@
1
+ """Gemini runtime resolution and explicit per-process credential isolation.
2
+
3
+ No login, settings mutation or model request takes place during discovery.
4
+ New API chats use private profiles. Explicitly connected native imports keep
5
+ their original history root, selecting API auth only in the child process.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import copy
11
+ from contextlib import suppress
12
+ import os
13
+ from pathlib import Path
14
+ import shutil
15
+ import selectors
16
+ import signal
17
+ import subprocess
18
+ import sys
19
+ import tempfile
20
+ import time
21
+
22
+ VERSION = "0.64.0"
23
+ API_ONLY_MESSAGE = ("Gemini in AgentsDock supports API connections only. Configure a Gemini API key "
24
+ "in My Agents, then connect this imported chat to that API or create a Gemini API chat. Existing history is preserved.")
25
+ AUTH_ENV = (
26
+ "GEMINI_API_KEY", "GOOGLE_API_KEY", "GOOGLE_APPLICATION_CREDENTIALS",
27
+ "GOOGLE_CLOUD_ACCESS_TOKEN", "GOOGLE_GENAI_USE_GCA", "GOOGLE_GENAI_USE_VERTEXAI",
28
+ "GOOGLE_GEMINI_BASE_URL", "GOOGLE_VERTEX_BASE_URL", "GEMINI_CLI_AUTH_OVERRIDE",
29
+ "GEMINI_API_KEY_AUTH_MECHANISM", "GEMINI_CLI_USE_COMPUTE_ADC",
30
+ "GOOGLE_GENAI_API_VERSION", "GEMINI_CLI_CUSTOM_HEADERS", "CLOUD_SHELL",
31
+ )
32
+
33
+
34
+ class GeminiConfigurationError(ValueError):
35
+ pass
36
+
37
+
38
+ def read_settings(path: Path) -> dict:
39
+ try:
40
+ if path.stat().st_size > 1024 * 1024:
41
+ raise GeminiConfigurationError("Gemini settings exceed the supported size.")
42
+ value = json.loads(path.read_text())
43
+ if not isinstance(value, dict):
44
+ raise GeminiConfigurationError("Gemini settings must be a JSON object.")
45
+ return value
46
+ except FileNotFoundError:
47
+ return {}
48
+ except (OSError, ValueError) as exc:
49
+ raise GeminiConfigurationError("Gemini settings could not be read safely.") from exc
50
+
51
+
52
+ def native_home(env: dict[str, str]) -> Path:
53
+ # Unlike the wrapper's memory heuristic, core Storage treats this as a HOME
54
+ # prefix and appends .gemini itself (verified in the pinned CLI package).
55
+ return Path(env.get("GEMINI_CLI_HOME") or env.get("HOME") or Path.home())
56
+
57
+
58
+ def native_credentials_present(env: dict[str, str]) -> bool:
59
+ try:
60
+ directory = native_home(env) / ".gemini"
61
+ settings = read_settings(directory / "settings.json")
62
+ selected = settings.get("security", {}).get("auth", {}).get("selectedType")
63
+ # This is evidence of cached credentials, not a remote validity check.
64
+ return selected == "oauth-personal" and (directory / "oauth_creds.json").is_file()
65
+ except (GeminiConfigurationError, AttributeError):
66
+ return False
67
+
68
+
69
+ def resolve_executable(env: dict[str, str]) -> str | None:
70
+ override = env.get("AGENTSDOCK_GEMINI_BIN")
71
+ candidates = [override] if override else ["gemini", str(Path.home() / ".local/bin/gemini"),
72
+ "/opt/homebrew/bin/gemini", "/usr/local/bin/gemini"]
73
+ for candidate in candidates:
74
+ if candidate and (value := shutil.which(candidate, path=env.get("PATH"))):
75
+ return str(Path(value).absolute())
76
+ return None
77
+
78
+
79
+ def probe(env: dict[str, str], *, timeout: float = 8) -> dict:
80
+ executable = resolve_executable(env)
81
+ base = {"backend": "gemini", "available": False, "authenticated": None,
82
+ "installed": bool(executable), "version": None, "_executable": executable}
83
+ if not executable:
84
+ return {**base, "status": "missing", "message": "Gemini CLI is not installed.",
85
+ "action": "Install @google/gemini-cli on the server, then configure a Gemini API key in My Agents."}
86
+ try:
87
+ version = version_output(executable, env, timeout).strip()
88
+ except (OSError, subprocess.SubprocessError):
89
+ return {**base, "status": "error", "message": "Gemini CLI version check failed.",
90
+ "action": "Check gemini --version on the server, then retry."}
91
+ if version != VERSION:
92
+ return {**base, "status": "error", "version": version[:64],
93
+ "message": f"This Gemini integration requires CLI {VERSION}.",
94
+ "action": f"Install @google/gemini-cli@{VERSION} on the server."}
95
+ return {**base, "version": version, "status": "ready", "available": True,
96
+ "message": "Gemini checks the selected credentials when a message is sent.", "action": None}
97
+
98
+
99
+ def version_output(executable: str, env: dict[str, str], timeout: float) -> str:
100
+ """A broken wrapper cannot fill memory or keep catalog discovery alive."""
101
+ process = subprocess.Popen([executable, "--version"], env=env, stdin=subprocess.DEVNULL,
102
+ stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, start_new_session=True)
103
+ output = bytearray()
104
+ deadline = time.monotonic() + timeout
105
+ try:
106
+ with selectors.DefaultSelector() as selector:
107
+ selector.register(process.stdout, selectors.EVENT_READ)
108
+ while True:
109
+ remaining = deadline - time.monotonic()
110
+ if remaining <= 0 or not selector.select(remaining):
111
+ raise subprocess.TimeoutExpired("gemini --version", timeout)
112
+ chunk = os.read(process.stdout.fileno(), 4097)
113
+ if not chunk:
114
+ break
115
+ output.extend(chunk)
116
+ if len(output) > 4096:
117
+ raise subprocess.SubprocessError("Gemini version output exceeded its limit.")
118
+ if process.wait(timeout=max(.01, deadline - time.monotonic())) != 0:
119
+ raise subprocess.SubprocessError("Gemini version check failed.")
120
+ return output.decode("utf-8", errors="replace")
121
+ finally:
122
+ # Kill only this probe's process group, including pipe-holding children.
123
+ try:
124
+ os.killpg(process.pid, signal.SIGKILL)
125
+ except ProcessLookupError:
126
+ pass
127
+ process.wait(timeout=2)
128
+ process.stdout.close()
129
+
130
+
131
+ def build_environment(base: dict[str, str], *, cwd: str, api_home: Path | None = None,
132
+ api_key: str | None = None, base_url: str | None = None,
133
+ native_history: bool = False) -> dict[str, str]:
134
+ """Explicit auth selection; no fallback between API and Google accounts.
135
+
136
+ API home is server-owned and durable for this connection's native history.
137
+ Never point it at an ordinary user's home or reset/delete it on Forget.
138
+ """
139
+ if api_home is None:
140
+ raise GeminiConfigurationError(API_ONLY_MESSAGE)
141
+ env = {key: value for key, value in base.items() if key not in AUTH_ENV}
142
+ env.update({"GEMINI_CLI_NO_RELAUNCH": "1", "NO_BROWSER": "true",
143
+ "GEMINI_TELEMETRY_ENABLED": "false"})
144
+ expected_auth = "gemini-api-key"
145
+ system_path = Path(env.get("GEMINI_CLI_SYSTEM_SETTINGS_PATH") or (
146
+ "/Library/Application Support/GeminiCli/settings.json" if sys.platform == "darwin" else "/etc/gemini-cli/settings.json"))
147
+ # System-selected auth outranks our private profile. Do not bypass an
148
+ # organization's enforcement, or accidentally use another account type.
149
+ try:
150
+ system_auth = read_settings(system_path).get("security", {}).get("auth", {})
151
+ if any(system_auth.get(key) not in {None, expected_auth} for key in ("selectedType", "enforcedType")):
152
+ raise GeminiConfigurationError("Server-managed Gemini authentication conflicts with this connection.")
153
+ except AttributeError as exc:
154
+ raise GeminiConfigurationError("Invalid managed Gemini authentication settings.") from exc
155
+ project = read_settings(Path(cwd) / ".gemini/settings.json")
156
+ try:
157
+ auth = project.get("security", {}).get("auth", {})
158
+ project_auth = auth.get("selectedType")
159
+ if auth.get("enforcedType") not in {None, expected_auth}:
160
+ raise GeminiConfigurationError("The workspace enforces a different Gemini authentication method.")
161
+ except AttributeError as exc:
162
+ raise GeminiConfigurationError("Invalid project Gemini authentication settings.") from exc
163
+ if not api_key or not base_url:
164
+ raise GeminiConfigurationError("Configure the Gemini API connection before sending a message.")
165
+ if project_auth not in {None, "gemini-api-key"}:
166
+ raise GeminiConfigurationError("The workspace overrides Gemini API authentication. Its settings must match this connection.")
167
+ if native_history:
168
+ # Pinned 0.64's in-memory user auth override preserves normal settings
169
+ # precedence, unlike ACP authenticate (which writes settings/credentials).
170
+ # Keep the ORIGINAL history root: no transcript copy, symlink or new ID.
171
+ original = native_home(base)
172
+ try:
173
+ auth = read_settings(original / ".gemini/settings.json").get("security", {}).get("auth", {})
174
+ if auth.get("enforcedType") not in {None, expected_auth}:
175
+ raise GeminiConfigurationError("Native Gemini settings enforce a different authentication method.")
176
+ except AttributeError as exc:
177
+ raise GeminiConfigurationError("Invalid native Gemini authentication settings.") from exc
178
+ env.update({"GEMINI_CLI_HOME": str(original), "GEMINI_CLI_AUTH_OVERRIDE": expected_auth})
179
+ return api_environment(env, api_key, base_url)
180
+ api_home = api_home.absolute()
181
+ original = native_home(base).resolve()
182
+ if api_home.resolve() == original or api_home.is_symlink():
183
+ raise GeminiConfigurationError("The Gemini API profile must be separate from the native account.")
184
+ if api_home.is_symlink() or api_home.parent.is_symlink():
185
+ raise GeminiConfigurationError("The Gemini API profile cannot use linked directories.")
186
+ api_home.mkdir(parents=True, exist_ok=True, mode=0o700)
187
+ info = api_home.stat()
188
+ if info.st_uid != os.getuid() or info.st_mode & 0o077:
189
+ raise GeminiConfigurationError("The Gemini API profile is not private.")
190
+ directory = api_home / ".gemini"
191
+ directory.mkdir(parents=True, exist_ok=True, mode=0o700)
192
+ if directory.is_symlink() or directory.stat().st_uid != os.getuid() or directory.stat().st_mode & 0o077:
193
+ raise GeminiConfigurationError("The Gemini API profile is not private.")
194
+ settings_path = directory / "settings.json"
195
+ settings = read_settings(settings_path)
196
+ inherit_skill_configuration(original, api_home, settings)
197
+ # Only this private profile is changed; project and global settings remain
198
+ # untouched. Preserve existing skills, policies and user choices here.
199
+ settings.setdefault("security", {}).setdefault("auth", {})["selectedType"] = "gemini-api-key"
200
+ descriptor, temporary = tempfile.mkstemp(prefix=".settings-", dir=directory)
201
+ try:
202
+ with os.fdopen(descriptor, "w") as handle:
203
+ json.dump(settings, handle)
204
+ handle.flush()
205
+ os.fsync(handle.fileno())
206
+ os.replace(temporary, settings_path)
207
+ finally:
208
+ if os.path.exists(temporary):
209
+ os.unlink(temporary)
210
+ env["GEMINI_CLI_HOME"] = str(api_home)
211
+ return api_environment(env, api_key, base_url)
212
+
213
+
214
+ def api_environment(env: dict[str, str], api_key: str, base_url: str) -> dict[str, str]:
215
+ normalized_base = base_url.rstrip("/")
216
+ for version in ("v1beta", "v1"):
217
+ if normalized_base.endswith("/" + version):
218
+ normalized_base = normalized_base[:-(len(version) + 1)]
219
+ env["GOOGLE_GENAI_API_VERSION"] = version
220
+ break
221
+ env.update({"GEMINI_API_KEY": api_key,
222
+ "GOOGLE_GEMINI_BASE_URL": normalized_base})
223
+ return env
224
+
225
+
226
+ def history_environment(base: dict, session: dict, connection: dict, root: Path) -> dict:
227
+ env = dict(base)
228
+ if connection and not session.get("gemini_native_history"):
229
+ env["GEMINI_CLI_HOME"] = str(root / ("gemini-runtime-" + connection["credential_id"]))
230
+ return env
231
+
232
+
233
+ def inherit_skill_configuration(original: Path, profile: Path, settings: dict) -> None:
234
+ """Reference installed skills/extensions, never copy or reinstall them.
235
+
236
+ Share only named non-login assets, NOT the Gemini home, settings or credential
237
+ files. Native trust/skill disables and tool policies remain authoritative.
238
+ Auth and history stay in the private API profile. Existing private assets
239
+ are not overwritten silently.
240
+ """
241
+ native = read_settings(original / ".gemini/settings.json")
242
+ try:
243
+ if native.get("security", {}).get("auth", {}).get("enforcedType") not in {None, "gemini-api-key"}:
244
+ raise GeminiConfigurationError("Native Gemini settings enforce a different authentication method.")
245
+ except AttributeError:
246
+ raise GeminiConfigurationError("Invalid Gemini authentication settings.") from None
247
+ for key in ("skills", "tools", "policy", "extensions", "admin"):
248
+ if key in native:
249
+ settings[key] = copy.deepcopy(native[key])
250
+ else:
251
+ settings.pop(key, None)
252
+ security = copy.deepcopy(native.get("security") or {})
253
+ if not isinstance(security, dict):
254
+ raise GeminiConfigurationError("Invalid Gemini security settings.")
255
+ security.pop("auth", None)
256
+ settings["security"] = security
257
+ # Plan availability is native configuration, not a different login type.
258
+ # Copy only this non-auth subtree, not global default approval preferences.
259
+ general = native.get("general") or {}
260
+ if not isinstance(general, dict):
261
+ raise GeminiConfigurationError("Invalid Gemini general settings.")
262
+ settings.setdefault("general", {}).pop("plan", None)
263
+ if "plan" in general:
264
+ settings["general"]["plan"] = copy.deepcopy(general["plan"])
265
+ for relative in (".gemini/skills", ".agents/skills", ".gemini/extensions",
266
+ ".gemini/policies", ".gemini/trustedFolders.json"):
267
+ source, target = original / relative, profile / relative
268
+ if target.is_symlink():
269
+ if os.readlink(target) != str(source):
270
+ raise GeminiConfigurationError("Gemini API skill configuration changed; inspect the private profile before retrying.")
271
+ continue
272
+ if not source.exists():
273
+ continue
274
+ if target.exists():
275
+ raise GeminiConfigurationError("Gemini API profile has separate skill configuration; reconcile it before retrying.")
276
+ target.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
277
+ if target.parent.is_symlink():
278
+ raise GeminiConfigurationError("The Gemini API profile is not private.")
279
+ try:
280
+ target.symlink_to(source, target_is_directory=source.is_dir())
281
+ except FileExistsError:
282
+ # Concurrent discovery and a turn may initialize the same profile.
283
+ if not target.is_symlink() or os.readlink(target) != str(source):
284
+ raise GeminiConfigurationError("Gemini API skill configuration changed.") from None
285
+
286
+
287
+ def list_native_skills(executable: str, env: dict[str, str], cwd: str, timeout: float = 15) -> str:
288
+ """Pinned native discovery, including enabled extension skills and trust.
289
+
290
+ Include built-ins with the native --all flag; it does not enable disabled
291
+ skills or override workspace trust. This is local discovery, not an ACP
292
+ model prompt/authenticate.
293
+ Kill the owned group on timeout/output overflow; never publish stderr.
294
+ """
295
+ process = subprocess.Popen([executable, "skills", "list", "--all"], cwd=cwd,
296
+ env={**env, "NO_COLOR": "1", "FORCE_COLOR": "0"}, stdin=subprocess.DEVNULL,
297
+ stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, start_new_session=True)
298
+ data = bytearray()
299
+ deadline = time.monotonic() + timeout
300
+ try:
301
+ with selectors.DefaultSelector() as selector:
302
+ selector.register(process.stdout, selectors.EVENT_READ)
303
+ while True:
304
+ remaining = deadline - time.monotonic()
305
+ if remaining <= 0 or not selector.select(remaining):
306
+ raise GeminiConfigurationError("Gemini skill discovery timed out.")
307
+ chunk = os.read(process.stdout.fileno(), 65536)
308
+ if not chunk:
309
+ break
310
+ data.extend(chunk)
311
+ if len(data) > 1024 * 1024:
312
+ raise GeminiConfigurationError("Gemini skill discovery exceeded its output limit.")
313
+ if process.wait(timeout=max(.01, deadline - time.monotonic())) != 0:
314
+ raise GeminiConfigurationError("Gemini skill discovery failed.")
315
+ return data.decode("utf-8", errors="strict")
316
+ except (OSError, UnicodeError, subprocess.SubprocessError):
317
+ raise GeminiConfigurationError("Gemini skill discovery failed.") from None
318
+ finally:
319
+ with suppress(ProcessLookupError):
320
+ os.killpg(process.pid, signal.SIGKILL)
321
+ process.wait(timeout=2)
322
+ process.stdout.close()
package/server/install.sh CHANGED
@@ -1839,7 +1839,7 @@ if [[ "$TEAM_HUB_MODE" == "host" && "$TEAM_HUB_OPERATION_PENDING" != "true" \
1839
1839
  fi
1840
1840
  fi
1841
1841
 
1842
- RELEASE_FILES=(activation_transaction.py execution_activation.py execution_legacy_runner.py execution_preparation.py update_preparation.py update_handoff.py update_recovery.py execution_update_status.py execution_uninstall.py execution_recovery.py execution_recovery_status.py execution_http.py execution_durability.py execution_control.py execution_install.py execution_maintenance.py execution_manage.py execution_ownership.py execution_service.py execution_transport.py agent_server.py local_session_ownership.py cursor_history.py workspace_git.py team_hub_host.py secure_peer_runtime.py team_mail_runtime.py team_mail_websocket.py team_mail_grants.py secure_peer_delivery.py agentsdock_jobs.py agentsdock_chats.py chat_mailbox.py agentsdock_emergency.py agentsdock_publish.py agentsdock_mail.py agentsdock_team.py provider_commands.py provider_usage.py claude_sdk_client.py claude_model_catalog.py claude_goals.py claude_background_reconciliation.py codex_app_server.py codex_auth.py codex_provider.py codex_response_stream.py provider_connections.py cursor_api_key.py side_questions.py title_generation.py codex_side_question.py codex_side_chat_progress.py claude_side_question.py cursor_agent_client.py cursor_provider_mcp.py opencode_agent_client.py cursor_process_guard.py transcript_reader.py claude_history_repair.py claude_history_provenance.py codex_history_repair.py public_chat_shares.py public_chat_transcript.py public_chat_share_routes.py interactive_chat_shares.py interactive_chat_share_routes.py interactive_chat_share_web.py interactive_chat_projection.py interactive_chat_runtime.py interactive_chat_native.py shared_chat_videos.py shared_chat_video_stream.py interactive_chat_controls.py install.sh uninstall.sh instances.sh server_instances.py update_runner.py pyproject.toml uv.lock VERSION release-public-key.pem LICENSE NOTICE)
1842
+ RELEASE_FILES=(activation_transaction.py execution_activation.py execution_legacy_runner.py execution_preparation.py update_preparation.py update_handoff.py update_recovery.py execution_update_status.py execution_uninstall.py execution_recovery.py execution_recovery_status.py execution_http.py execution_durability.py execution_control.py execution_install.py execution_maintenance.py execution_manage.py execution_ownership.py execution_service.py execution_transport.py agent_server.py local_session_ownership.py cursor_history.py workspace_git.py team_hub_host.py secure_peer_runtime.py team_mail_runtime.py team_mail_websocket.py team_mail_grants.py secure_peer_delivery.py agentsdock_jobs.py agentsdock_chats.py chat_mailbox.py agentsdock_emergency.py agentsdock_publish.py agentsdock_mail.py agentsdock_team.py provider_commands.py provider_usage.py claude_sdk_client.py claude_model_catalog.py claude_goals.py claude_background_reconciliation.py codex_app_server.py codex_auth.py codex_provider.py codex_response_stream.py provider_connections.py cursor_api_key.py side_questions.py title_generation.py codex_side_question.py codex_side_chat_progress.py claude_side_question.py cursor_agent_client.py cursor_provider_mcp.py opencode_agent_client.py gemini_acp_client.py gemini_history.py gemini_content.py gemini_import.py gemini_runtime.py gemini_runner.py gemini_permissions.py gemini_api.py cursor_process_guard.py transcript_reader.py claude_history_repair.py claude_history_provenance.py codex_history_repair.py public_chat_shares.py public_chat_transcript.py public_chat_share_routes.py interactive_chat_shares.py interactive_chat_share_routes.py interactive_chat_share_web.py interactive_chat_projection.py interactive_chat_runtime.py interactive_chat_native.py shared_chat_videos.py shared_chat_video_stream.py interactive_chat_controls.py install.sh uninstall.sh instances.sh server_instances.py update_runner.py pyproject.toml uv.lock VERSION release-public-key.pem LICENSE NOTICE)
1843
1843
  RELEASE_DIRECTORIES=(agentsdock_team_hub)
1844
1844
  TEAM_HUB_RELEASE_FILES=(
1845
1845
  __init__.py
@@ -3762,6 +3762,14 @@ validate_staged_release_runtime() (
3762
3762
  "$STAGE_DIR/cursor_agent_client.py" \
3763
3763
  "$STAGE_DIR/cursor_provider_mcp.py" \
3764
3764
  "$STAGE_DIR/opencode_agent_client.py" \
3765
+ "$STAGE_DIR/gemini_acp_client.py" \
3766
+ "$STAGE_DIR/gemini_history.py" \
3767
+ "$STAGE_DIR/gemini_content.py" \
3768
+ "$STAGE_DIR/gemini_import.py" \
3769
+ "$STAGE_DIR/gemini_runtime.py" \
3770
+ "$STAGE_DIR/gemini_runner.py" \
3771
+ "$STAGE_DIR/gemini_permissions.py" \
3772
+ "$STAGE_DIR/gemini_api.py" \
3765
3773
  "$STAGE_DIR/cursor_process_guard.py" \
3766
3774
  "$STAGE_DIR/transcript_reader.py" \
3767
3775
  "$STAGE_DIR/claude_history_repair.py" \
@@ -3782,7 +3790,7 @@ validate_staged_release_runtime() (
3782
3790
  "$STAGE_DIR/update_runner.py"
3783
3791
  "$STAGE_DIR/.venv/bin/python" -m py_compile "$STAGE_DIR/server_instances.py"
3784
3792
  "$STAGE_DIR/.venv/bin/python" -m compileall -q "$STAGE_DIR/agentsdock_team_hub"
3785
- PYTHONPATH="$STAGE_DIR" "$STAGE_DIR/.venv/bin/python" -c 'import execution_activation, execution_preparation, update_preparation, update_handoff, update_recovery, execution_update_status, execution_uninstall, execution_recovery, execution_recovery_status, execution_http, execution_durability, execution_control, execution_install, execution_maintenance, execution_manage, execution_ownership, execution_service, execution_transport; import workspace_git, claude_model_catalog, server_instances, local_session_ownership, cursor_history; import codex_auth, codex_provider, transcript_reader, codex_response_stream, provider_connections, cursor_api_key, side_questions, title_generation, codex_side_question, codex_side_chat_progress, claude_side_question, agentsdock_team_hub, cursor_agent_client, cursor_provider_mcp, opencode_agent_client, cursor_process_guard, secure_peer_delivery, secure_peer_runtime, team_mail_runtime, team_mail_websocket, team_mail_grants, team_hub_host, agentsdock_mail, agentsdock_team, claude_history_repair, claude_history_provenance, claude_goals, claude_background_reconciliation, codex_history_repair, public_chat_shares, public_chat_transcript, public_chat_share_routes, interactive_chat_shares, interactive_chat_share_routes, interactive_chat_share_web, interactive_chat_projection, interactive_chat_runtime, interactive_chat_native, shared_chat_videos, shared_chat_video_stream, interactive_chat_controls, chat_mailbox, provider_commands, provider_usage; from agentsdock_team_hub import secure_peer, secure_peer_hub' >/dev/null
3793
+ PYTHONPATH="$STAGE_DIR" "$STAGE_DIR/.venv/bin/python" -c 'import execution_activation, execution_preparation, update_preparation, update_handoff, update_recovery, execution_update_status, execution_uninstall, execution_recovery, execution_recovery_status, execution_http, execution_durability, execution_control, execution_install, execution_maintenance, execution_manage, execution_ownership, execution_service, execution_transport; import workspace_git, claude_model_catalog, server_instances, local_session_ownership, cursor_history; import codex_auth, codex_provider, codex_response_stream, provider_connections, cursor_api_key, side_questions, title_generation, codex_side_question, codex_side_chat_progress, claude_side_question, agentsdock_team_hub, cursor_agent_client, cursor_provider_mcp, opencode_agent_client, gemini_acp_client, gemini_history, gemini_content, gemini_import, gemini_runtime, gemini_runner, gemini_permissions, gemini_api, cursor_process_guard, secure_peer_delivery, secure_peer_runtime, team_mail_runtime, team_mail_websocket, team_mail_grants, team_hub_host, agentsdock_mail, agentsdock_team, transcript_reader, claude_history_repair, claude_history_provenance, claude_goals, claude_background_reconciliation, codex_history_repair, public_chat_shares, public_chat_transcript, public_chat_share_routes, interactive_chat_shares, interactive_chat_share_routes, interactive_chat_share_web, interactive_chat_projection, interactive_chat_runtime, interactive_chat_native, shared_chat_videos, shared_chat_video_stream, interactive_chat_controls, chat_mailbox, provider_commands, provider_usage; from agentsdock_team_hub import secure_peer, secure_peer_hub' >/dev/null
3786
3794
  )
3787
3795
 
3788
3796
  abort_unclaimed_team_hub_reactivation() {
@@ -140,11 +140,17 @@ class InteractiveChatControls:
140
140
  elif action == "approval.respond":
141
141
  _fields(payload, {"id", "backend", "response"}, {"id", "backend", "response"})
142
142
  backend = payload["backend"]
143
- if backend not in ("codex", "claude") or not isinstance(payload["response"], dict):
143
+ if backend not in ("codex", "claude", "gemini") or not isinstance(payload["response"], dict):
144
144
  raise ChatControlError()
145
145
  args.append(_identity(payload["id"]))
146
- args.append(self._model("CodexInteractionResponseRequest" if backend == "codex" else "ClaudeInteractionResponseRequest",
147
- {"response": payload["response"]}))
146
+ if backend == "gemini":
147
+ response = payload["response"]
148
+ if set(response) != {"decision"} or response["decision"] not in ("accept", "decline", "cancel"):
149
+ raise ChatControlError()
150
+ args.append(response)
151
+ else:
152
+ args.append(self._model("CodexInteractionResponseRequest" if backend == "codex" else "ClaudeInteractionResponseRequest",
153
+ {"response": payload["response"]}))
148
154
  callback_key = "approval." + backend
149
155
 
150
156
  callback = self.callbacks.get(callback_key)
@@ -11,7 +11,7 @@ PRIVATE_FIELDS = frozenset({
11
11
  "token", "access_token", "refresh_token", "api_key", "password", "secret",
12
12
  "authority", "authority_file", "authority_path", "request_prompt", "provider_cross_chat_route_snapshot",
13
13
  "secure_peer_route_snapshots", "chat_references", "team_references",
14
- "claude_session_id", "codex_thread_id", "cursor_session_id",
14
+ "claude_session_id", "codex_thread_id", "cursor_session_id", "gemini_session_id", "gemini_session_cwd",
15
15
  })
16
16
  SESSION_FIELDS = frozenset({
17
17
  "id", "title", "backend", "model", "effort", "system_prompt", "backend_locked",
@@ -41,7 +41,7 @@ CONTROL_ACTIONS = frozenset({
41
41
  })
42
42
  CONTROL_READ_ACTIONS = frozenset({"timeline.older", "timeline.around", "timeline.trace", "timeline.index", "jobs.runs", "runtime.catalog", "handoffs.get"})
43
43
  NATIVE_STATE_FIELDS = frozenset({"revision", "session", "events", "queue", "active", "goal", "jobs",
44
- "codex_runtime", "claude_runtime", "health", "runtime_catalog", "hasMoreEvents", "nextTimelineBefore", "eventsTotal"})
44
+ "codex_runtime", "claude_runtime", "gemini_runtime", "health", "runtime_catalog", "hasMoreEvents", "nextTimelineBefore", "eventsTotal"})
45
45
  COOKIE = "__Secure-AgentsDock-Chat"
46
46
  HTTP_COOKIE = "AgentsDock-Chat"
47
47
  HEADERS = {