@agentsdock/server 1.0.11-beta.1 → 1.0.11-beta.12
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 +1474 -318
- 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 +25 -2
- package/server/interactive_chat_controls.py +9 -3
- package/server/interactive_chat_native.py +1 -1
- package/server/interactive_chat_share_routes.py +2 -2
- package/server/interactive_chat_share_web.py +2 -2
- package/server/local_session_ownership.py +1 -0
- package/server/npm/agentsdock/QR-NOTICES.txt +56 -0
- package/server/npm/agentsdock/pair.cjs +123 -0
- package/server/npm/agentsdock/qr-renderer.cjs +15 -0
- package/server/provider_commands.py +65 -0
- package/server/provider_connections.py +39 -10
- package/server/public_chat_share_routes.py +8 -6
- package/server/public_chat_shares.py +17 -7
- package/server/server_instances.py +195 -15
- package/server/shared_chat_analytics_web.py +5 -0
- 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
|
@@ -1387,6 +1387,13 @@ print_token_for_copy() {
|
|
|
1387
1387
|
local copy_reply=""
|
|
1388
1388
|
local clipboard_command=()
|
|
1389
1389
|
printf '\nAccess token (%s):\n%s\n\n' "$INSTANCE_NAME" "$access_token"
|
|
1390
|
+
# Render locally, only in an interactive terminal. No credentials in argv.
|
|
1391
|
+
local pairing_script="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/npm/agentsdock/pair.cjs"
|
|
1392
|
+
if [[ -f "$pairing_script" ]] && command -v node >/dev/null 2>&1; then
|
|
1393
|
+
node "$pairing_script" "$INSTANCE_NAME" || true
|
|
1394
|
+
else
|
|
1395
|
+
printf 'Show a connection QR with: agentsdock pair %s\n' "$INSTANCE_NAME"
|
|
1396
|
+
fi
|
|
1390
1397
|
# A remote clipboard is not the user's local clipboard. Do not emit OSC 52
|
|
1391
1398
|
# sequences or attempt to modify a remote desktop's clipboard over SSH.
|
|
1392
1399
|
if [[ -n "${SSH_CONNECTION:-}${SSH_CLIENT:-}${SSH_TTY:-}" ]]; then
|
|
@@ -1839,7 +1846,7 @@ if [[ "$TEAM_HUB_MODE" == "host" && "$TEAM_HUB_OPERATION_PENDING" != "true" \
|
|
|
1839
1846
|
fi
|
|
1840
1847
|
fi
|
|
1841
1848
|
|
|
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)
|
|
1849
|
+
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_analytics_web.py shared_chat_video_stream.py interactive_chat_controls.py install.sh uninstall.sh instances.sh server_instances.py npm/agentsdock/pair.cjs npm/agentsdock/qr-renderer.cjs npm/agentsdock/QR-NOTICES.txt update_runner.py pyproject.toml uv.lock VERSION release-public-key.pem LICENSE NOTICE)
|
|
1843
1850
|
RELEASE_DIRECTORIES=(agentsdock_team_hub)
|
|
1844
1851
|
TEAM_HUB_RELEASE_FILES=(
|
|
1845
1852
|
__init__.py
|
|
@@ -1880,6 +1887,12 @@ TEAM_HUB_RELEASE_FILES=(
|
|
|
1880
1887
|
migrations/0023_team_message_search.sql
|
|
1881
1888
|
)
|
|
1882
1889
|
|
|
1890
|
+
for directory in npm npm/agentsdock; do
|
|
1891
|
+
if [[ ! -d "$SOURCE_DIR/$directory" || -L "$SOURCE_DIR/$directory" ]]; then
|
|
1892
|
+
echo "$directory is missing beside install.sh or is not a real directory." >&2
|
|
1893
|
+
exit 1
|
|
1894
|
+
fi
|
|
1895
|
+
done
|
|
1883
1896
|
for name in "${RELEASE_FILES[@]}"; do
|
|
1884
1897
|
if [[ ! -f "$SOURCE_DIR/$name" || -L "$SOURCE_DIR/$name" ]]; then
|
|
1885
1898
|
echo "$name is missing beside install.sh or is not a regular release file." >&2
|
|
@@ -3762,6 +3775,14 @@ validate_staged_release_runtime() (
|
|
|
3762
3775
|
"$STAGE_DIR/cursor_agent_client.py" \
|
|
3763
3776
|
"$STAGE_DIR/cursor_provider_mcp.py" \
|
|
3764
3777
|
"$STAGE_DIR/opencode_agent_client.py" \
|
|
3778
|
+
"$STAGE_DIR/gemini_acp_client.py" \
|
|
3779
|
+
"$STAGE_DIR/gemini_history.py" \
|
|
3780
|
+
"$STAGE_DIR/gemini_content.py" \
|
|
3781
|
+
"$STAGE_DIR/gemini_import.py" \
|
|
3782
|
+
"$STAGE_DIR/gemini_runtime.py" \
|
|
3783
|
+
"$STAGE_DIR/gemini_runner.py" \
|
|
3784
|
+
"$STAGE_DIR/gemini_permissions.py" \
|
|
3785
|
+
"$STAGE_DIR/gemini_api.py" \
|
|
3765
3786
|
"$STAGE_DIR/cursor_process_guard.py" \
|
|
3766
3787
|
"$STAGE_DIR/transcript_reader.py" \
|
|
3767
3788
|
"$STAGE_DIR/claude_history_repair.py" \
|
|
@@ -3777,12 +3798,13 @@ validate_staged_release_runtime() (
|
|
|
3777
3798
|
"$STAGE_DIR/interactive_chat_runtime.py" \
|
|
3778
3799
|
"$STAGE_DIR/interactive_chat_native.py" \
|
|
3779
3800
|
"$STAGE_DIR/shared_chat_videos.py" \
|
|
3801
|
+
"$STAGE_DIR/shared_chat_analytics_web.py" \
|
|
3780
3802
|
"$STAGE_DIR/shared_chat_video_stream.py" \
|
|
3781
3803
|
"$STAGE_DIR/interactive_chat_controls.py" \
|
|
3782
3804
|
"$STAGE_DIR/update_runner.py"
|
|
3783
3805
|
"$STAGE_DIR/.venv/bin/python" -m py_compile "$STAGE_DIR/server_instances.py"
|
|
3784
3806
|
"$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,
|
|
3807
|
+
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_analytics_web, 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
3808
|
)
|
|
3787
3809
|
|
|
3788
3810
|
abort_unclaimed_team_hub_reactivation() {
|
|
@@ -4100,6 +4122,7 @@ if [[ "$stage_identity" != *$'\n'* \
|
|
|
4100
4122
|
exit 1
|
|
4101
4123
|
fi
|
|
4102
4124
|
for name in "${RELEASE_FILES[@]}"; do
|
|
4125
|
+
mkdir -p "$(dirname -- "$STAGE_DIR/$name")"
|
|
4103
4126
|
install -m 644 "$SOURCE_DIR/$name" "$STAGE_DIR/$name"
|
|
4104
4127
|
done
|
|
4105
4128
|
mkdir -p "$STAGE_DIR/agentsdock_team_hub/migrations"
|
|
@@ -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,13 +41,13 @@ 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 = {
|
|
48
48
|
"Cache-Control": "no-store", "Referrer-Policy": "no-referrer", "X-Content-Type-Options": "nosniff",
|
|
49
49
|
"X-Robots-Tag": "noindex, nofollow, noarchive", "Cross-Origin-Resource-Policy": "same-origin",
|
|
50
|
-
"Content-Security-Policy": "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data: blob:; media-src 'self' blob:; font-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
|
|
50
|
+
"Content-Security-Policy": "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' https://api.mixpanel.com; img-src 'self' data: blob:; media-src 'self' blob:; font-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
|