@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.
- package/NOTICE +7 -0
- package/package.json +1 -1
- package/server/NOTICE +7 -0
- package/server/VERSION +1 -1
- package/server/agent_server.py +1421 -313
- package/server/agentsdock_jobs.py +2 -2
- package/server/codex_auth.py +48 -0
- package/server/codex_history_repair.py +180 -15
- package/server/cursor_process_guard.py +67 -4
- package/server/cursor_provider_mcp.py +2 -0
- package/server/execution_durability.py +87 -11
- package/server/execution_install.py +5 -1
- package/server/gemini_acp_client.py +432 -0
- package/server/gemini_api.py +128 -0
- package/server/gemini_content.py +102 -0
- package/server/gemini_history.py +293 -0
- package/server/gemini_import.py +126 -0
- package/server/gemini_permissions.py +47 -0
- package/server/gemini_runner.py +493 -0
- package/server/gemini_runtime.py +322 -0
- package/server/install.sh +10 -2
- package/server/interactive_chat_controls.py +9 -3
- package/server/interactive_chat_native.py +1 -1
- package/server/interactive_chat_share_routes.py +1 -1
- package/server/interactive_chat_share_web.py +2 -2
- package/server/local_session_ownership.py +1 -0
- package/server/provider_commands.py +65 -0
- package/server/provider_connections.py +39 -10
- package/server/server_instances.py +195 -15
- package/server/title_generation.py +60 -1
- package/server/transcript_reader.py +20 -8
- package/server/update_runner.py +4 -2
|
@@ -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,
|
|
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
|
-
|
|
147
|
-
|
|
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 = {
|