@agentsdock/server 1.0.7 → 1.0.8-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.
- package/README.md +2 -2
- package/package.json +1 -1
- package/server/VERSION +1 -1
- package/server/agent_server.py +392 -60
- package/server/codex_app_server.py +12 -0
- package/server/codex_history_repair.py +79 -14
- package/server/codex_provider.py +159 -76
- package/server/cursor_api_key.py +64 -0
- package/server/install.sh +120 -7
- package/server/opencode_agent_client.py +32 -0
- package/server/provider_connections.py +665 -0
- package/server/pyproject.toml +1 -0
- package/server/title_generation.py +4 -1
- package/server/uv.lock +3 -1
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Cursor-issued keys, not OpenAI-compatible endpoints or a machine login.
|
|
2
|
+
|
|
3
|
+
Cursor exchanges API keys for tokens and normally persists those tokens in the
|
|
4
|
+
user's credential store. Use its in-memory store for every key-bound process;
|
|
5
|
+
an inherited auth token must never take precedence over the selected API key.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
import tempfile
|
|
11
|
+
|
|
12
|
+
from fastapi import HTTPException
|
|
13
|
+
from cursor_agent_client import parse_cursor_models_list
|
|
14
|
+
from side_questions import run_isolated_command, SideQuestionError
|
|
15
|
+
|
|
16
|
+
ENDPOINT = "https://api2.cursor.sh"
|
|
17
|
+
MIN_VERSION = (2026, 9, 26) # First build verified with memory credential isolation.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def overrides(key: str) -> dict[str, str]:
|
|
21
|
+
return {
|
|
22
|
+
"CURSOR_API_KEY": key, "CURSOR_AUTH_TOKEN": "",
|
|
23
|
+
"AGENT_CLI_CREDENTIAL_STORE": "memory",
|
|
24
|
+
"CURSOR_API_ENDPOINT": ENDPOINT, "CURSOR_AGENT_CLI_LOCAL_MODE": "false",
|
|
25
|
+
"DIRENV_DISABLE": "1",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def require_isolation(executable: str | None, env: dict) -> None:
|
|
30
|
+
if not executable:
|
|
31
|
+
raise HTTPException(409, "Install Cursor CLI to use a Cursor API key.")
|
|
32
|
+
try:
|
|
33
|
+
with tempfile.TemporaryDirectory(prefix="agentsdock-cursor-version-") as cwd:
|
|
34
|
+
output = await run_isolated_command([executable, "--version"], prompt="", cwd=cwd,
|
|
35
|
+
env={**env, **overrides("")}, timeout=5)
|
|
36
|
+
match = re.search(r"\b(20\d\d)\.(\d{2})\.(\d{2})\b", output)
|
|
37
|
+
if not match or tuple(map(int, match.groups())) < MIN_VERSION:
|
|
38
|
+
raise ValueError("unsupported build")
|
|
39
|
+
except (SideQuestionError, ValueError):
|
|
40
|
+
raise HTTPException(409, "Update Cursor CLI to 2026.09.26 or later to isolate API keys from CLI login.") from None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def catalog(selected: dict, *, executable: str | None, env: dict) -> dict:
|
|
44
|
+
try:
|
|
45
|
+
await require_isolation(executable, env)
|
|
46
|
+
except HTTPException:
|
|
47
|
+
return {"models": [], "discovery_status": "unavailable", "cli_update_required": True}
|
|
48
|
+
try:
|
|
49
|
+
# Read-only authenticated request; no conversation, model inference,
|
|
50
|
+
# project tools, or native credential writes. Never reflect CLI stderr.
|
|
51
|
+
with tempfile.TemporaryDirectory(prefix="agentsdock-cursor-key-") as cwd:
|
|
52
|
+
output = await run_isolated_command([executable, "--list-models"], prompt="", cwd=cwd,
|
|
53
|
+
env={**env, **overrides(selected["api_key"])}, timeout=20)
|
|
54
|
+
models = [{"value": item["id"], "label": item["label"]}
|
|
55
|
+
for item in parse_cursor_models_list(output)
|
|
56
|
+
if re.fullmatch(r"[\x21-\x7e]{1,256}", item["id"])
|
|
57
|
+
and selected["api_key"] not in item["id"] + item["label"]]
|
|
58
|
+
if not models:
|
|
59
|
+
return {"models": [], "discovery_status": "unavailable"}
|
|
60
|
+
return {"models": models[:256], "discovery_status": "ready"}
|
|
61
|
+
except SideQuestionError:
|
|
62
|
+
# Offline and invalid keys can look identical in CLI output. Do not
|
|
63
|
+
# misclassify an outage as a logout or return key-bearing stderr.
|
|
64
|
+
return {"models": [], "discovery_status": "unavailable"}
|
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 side_questions.py title_generation.py codex_side_question.py claude_side_question.py cursor_agent_client.py cursor_provider_mcp.py opencode_agent_client.py cursor_process_guard.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 provider_connections.py cursor_api_key.py side_questions.py title_generation.py codex_side_question.py claude_side_question.py cursor_agent_client.py cursor_provider_mcp.py opencode_agent_client.py cursor_process_guard.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
|
|
@@ -3055,21 +3055,37 @@ install_lock_control() {
|
|
|
3055
3055
|
local action="$1"
|
|
3056
3056
|
local expected_device="${2:-0}"
|
|
3057
3057
|
local expected_inode="${3:-0}"
|
|
3058
|
+
local wait_for_prepared="false"
|
|
3059
|
+
# Old managed preparers do not pass a managed-update ID. The existing pinned
|
|
3060
|
+
# preparation/activation modes may serialize briefly; this grants no update
|
|
3061
|
+
# authority and leaves ordinary installation and recovery fail-fast.
|
|
3062
|
+
if [[ "$action" == "acquire" && "${RECOVER_ONLY:-false}" != "true" \
|
|
3063
|
+
&& "${RECOVER_UNARMED_ONLY:-false}" != "true" \
|
|
3064
|
+
&& ( "${PREPARE_ONLY:-false}" == "true" || -n "${ACTIVATE_PREPARED:-}" ) \
|
|
3065
|
+
&& "${PREPARED_ARCHIVE_SHA256:-}" =~ ^[0-9a-f]{64}$ \
|
|
3066
|
+
&& "${EXPECTED_API_CONTRACT:-}" =~ ^[1-9][0-9]*$ \
|
|
3067
|
+
&& -n "${RELEASE_VERSION:-}" ]]; then
|
|
3068
|
+
wait_for_prepared="true"
|
|
3069
|
+
fi
|
|
3058
3070
|
local python_path=""
|
|
3059
3071
|
python_path="$(install_lock_python)" || return 1
|
|
3060
3072
|
"$python_path" - \
|
|
3061
|
-
"$action" "$INSTALL_ROOT" "$$" "$expected_device" "$expected_inode" <<'PY'
|
|
3073
|
+
"$action" "$INSTALL_ROOT" "$$" "$expected_device" "$expected_inode" "$wait_for_prepared" <<'PY'
|
|
3062
3074
|
import errno
|
|
3063
3075
|
import os
|
|
3064
3076
|
from pathlib import Path
|
|
3065
3077
|
import secrets
|
|
3078
|
+
import signal
|
|
3066
3079
|
import stat
|
|
3067
3080
|
import sys
|
|
3081
|
+
import time
|
|
3068
3082
|
|
|
3069
|
-
action, raw_root, raw_pid, raw_device, raw_inode = sys.argv[1:]
|
|
3083
|
+
action, raw_root, raw_pid, raw_device, raw_inode, raw_wait = sys.argv[1:]
|
|
3070
3084
|
root = Path(os.path.abspath(raw_root))
|
|
3071
3085
|
pid = int(raw_pid)
|
|
3072
3086
|
lock = root / ".install-lock"
|
|
3087
|
+
PREPARED_LOCK_WAIT_SECONDS = 30.0
|
|
3088
|
+
wait_for_prepared = action == "acquire" and raw_wait == "true"
|
|
3073
3089
|
|
|
3074
3090
|
root_info = root.lstat()
|
|
3075
3091
|
if (
|
|
@@ -3145,7 +3161,56 @@ def read_owner(directory_descriptor: int) -> tuple[int, tuple[int, int]]:
|
|
|
3145
3161
|
return int(value), (int(info.st_dev), int(info.st_ino))
|
|
3146
3162
|
|
|
3147
3163
|
|
|
3164
|
+
def check_wait_root() -> None:
|
|
3165
|
+
current = root.lstat()
|
|
3166
|
+
if ((current.st_dev, current.st_ino) != (root_info.st_dev, root_info.st_ino)
|
|
3167
|
+
or not stat.S_ISDIR(current.st_mode) or current.st_uid != os.geteuid()
|
|
3168
|
+
or stat.S_IMODE(current.st_mode) & 0o022):
|
|
3169
|
+
raise PermissionError("AgentsServer install root changed while waiting")
|
|
3170
|
+
|
|
3171
|
+
|
|
3172
|
+
def same_wait_owner(directory_descriptor, directory_info, owner_pid, owner_identity) -> bool:
|
|
3173
|
+
check_wait_root()
|
|
3174
|
+
current = lock.lstat()
|
|
3175
|
+
if ((current.st_dev, current.st_ino) != (directory_info.st_dev, directory_info.st_ino)):
|
|
3176
|
+
return False
|
|
3177
|
+
if (not stat.S_ISDIR(current.st_mode) or current.st_uid != os.geteuid()
|
|
3178
|
+
or stat.S_IMODE(current.st_mode) & 0o077):
|
|
3179
|
+
raise PermissionError("AgentsServer install lock directory is unsafe")
|
|
3180
|
+
return read_owner(directory_descriptor) == (owner_pid, owner_identity)
|
|
3181
|
+
|
|
3182
|
+
|
|
3183
|
+
def cleanup_wait_candidate(path, expected) -> None:
|
|
3184
|
+
# Cancellation while waiting may remove only our still-unpublished draft.
|
|
3185
|
+
# An acquired-but-unreported lock is left for ordinary dead-owner reaping.
|
|
3186
|
+
try:
|
|
3187
|
+
check_wait_root()
|
|
3188
|
+
descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_DIRECTORY)
|
|
3189
|
+
try:
|
|
3190
|
+
current = os.fstat(descriptor)
|
|
3191
|
+
linked = path.lstat()
|
|
3192
|
+
identity = (expected.st_dev, expected.st_ino)
|
|
3193
|
+
if ((current.st_dev, current.st_ino) != identity
|
|
3194
|
+
or (linked.st_dev, linked.st_ino) != identity):
|
|
3195
|
+
return
|
|
3196
|
+
owner, owner_identity = read_owner(descriptor)
|
|
3197
|
+
if owner != pid or read_owner(descriptor) != (owner, owner_identity):
|
|
3198
|
+
return
|
|
3199
|
+
os.unlink("pid", dir_fd=descriptor)
|
|
3200
|
+
os.fsync(descriptor)
|
|
3201
|
+
linked = path.lstat()
|
|
3202
|
+
if (linked.st_dev, linked.st_ino) == identity:
|
|
3203
|
+
path.rmdir()
|
|
3204
|
+
finally:
|
|
3205
|
+
os.close(descriptor)
|
|
3206
|
+
except (OSError, RuntimeError):
|
|
3207
|
+
pass # Preserve the original cancellation/failure; never guess ownership.
|
|
3208
|
+
|
|
3209
|
+
|
|
3148
3210
|
if action == "acquire":
|
|
3211
|
+
# One budget includes owner/namespace changes. Waiting only observes an
|
|
3212
|
+
# existing safe lock; all authority checks still follow atomic acquisition.
|
|
3213
|
+
deadline = time.monotonic() + PREPARED_LOCK_WAIT_SECONDS
|
|
3149
3214
|
candidate = root / f".install-lock.{pid}.{secrets.token_hex(12)}.tmp"
|
|
3150
3215
|
os.mkdir(candidate, 0o700)
|
|
3151
3216
|
os.chmod(candidate, 0o700)
|
|
@@ -3165,21 +3230,44 @@ if action == "acquire":
|
|
|
3165
3230
|
finally:
|
|
3166
3231
|
os.close(descriptor)
|
|
3167
3232
|
fsync_directory(candidate)
|
|
3168
|
-
|
|
3233
|
+
if wait_for_prepared:
|
|
3234
|
+
def cancelled(signum, _frame):
|
|
3235
|
+
raise SystemExit(128 + signum)
|
|
3236
|
+
for signum in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP):
|
|
3237
|
+
signal.signal(signum, cancelled)
|
|
3238
|
+
namespace_attempts = 0
|
|
3239
|
+
while namespace_attempts < 32:
|
|
3240
|
+
if wait_for_prepared:
|
|
3241
|
+
check_wait_root()
|
|
3242
|
+
if time.monotonic() >= deadline:
|
|
3243
|
+
raise RuntimeError("AgentsServer prepared update timed out waiting for the installation lock")
|
|
3169
3244
|
try:
|
|
3170
3245
|
os.rename(candidate, lock)
|
|
3171
3246
|
except OSError as exc:
|
|
3172
3247
|
if exc.errno not in {errno.EEXIST, errno.ENOTEMPTY}:
|
|
3173
3248
|
raise
|
|
3174
|
-
directory_descriptor, directory_info = open_lock_directory()
|
|
3175
3249
|
try:
|
|
3250
|
+
directory_descriptor, directory_info = open_lock_directory()
|
|
3251
|
+
except FileNotFoundError:
|
|
3252
|
+
if not wait_for_prepared:
|
|
3253
|
+
raise
|
|
3254
|
+
# The incumbent retired after rename observed its lock.
|
|
3255
|
+
namespace_attempts += 1
|
|
3256
|
+
continue
|
|
3257
|
+
try:
|
|
3258
|
+
if wait_for_prepared and any(
|
|
3259
|
+
name != "pid" for name in os.listdir(directory_descriptor)
|
|
3260
|
+
):
|
|
3261
|
+
raise PermissionError("AgentsServer install lock contains unknown files")
|
|
3176
3262
|
try:
|
|
3177
3263
|
owner_pid, owner_identity = read_owner(directory_descriptor)
|
|
3178
3264
|
except FileNotFoundError:
|
|
3179
3265
|
# An old installer may have crashed in mkdir->pid. The
|
|
3180
3266
|
# next atomic directory rename replaces only that
|
|
3181
3267
|
# exact empty namespace; a populated replacement wins.
|
|
3268
|
+
namespace_attempts += 1
|
|
3182
3269
|
continue
|
|
3270
|
+
alive = False
|
|
3183
3271
|
try:
|
|
3184
3272
|
os.kill(owner_pid, 0)
|
|
3185
3273
|
except ProcessLookupError:
|
|
@@ -3189,6 +3277,22 @@ if action == "acquire":
|
|
|
3189
3277
|
f"another AgentsServer installation is active ({owner_pid})"
|
|
3190
3278
|
)
|
|
3191
3279
|
else:
|
|
3280
|
+
alive = True
|
|
3281
|
+
if alive:
|
|
3282
|
+
if wait_for_prepared:
|
|
3283
|
+
try:
|
|
3284
|
+
unchanged = same_wait_owner(directory_descriptor, directory_info,
|
|
3285
|
+
owner_pid, owner_identity)
|
|
3286
|
+
except FileNotFoundError:
|
|
3287
|
+
unchanged = False
|
|
3288
|
+
if not unchanged:
|
|
3289
|
+
namespace_attempts += 1
|
|
3290
|
+
continue
|
|
3291
|
+
remaining = deadline - time.monotonic()
|
|
3292
|
+
if remaining <= 0:
|
|
3293
|
+
raise RuntimeError("AgentsServer prepared update timed out waiting for the installation lock")
|
|
3294
|
+
time.sleep(min(0.1, remaining))
|
|
3295
|
+
continue
|
|
3192
3296
|
raise RuntimeError(
|
|
3193
3297
|
f"another AgentsServer installation is active ({owner_pid})"
|
|
3194
3298
|
)
|
|
@@ -3200,19 +3304,25 @@ if action == "acquire":
|
|
|
3200
3304
|
directory_descriptor
|
|
3201
3305
|
)
|
|
3202
3306
|
except FileNotFoundError:
|
|
3307
|
+
namespace_attempts += 1
|
|
3203
3308
|
continue
|
|
3204
3309
|
if current_owner != owner_pid or current_identity != owner_identity:
|
|
3310
|
+
namespace_attempts += 1
|
|
3205
3311
|
continue
|
|
3206
3312
|
try:
|
|
3207
3313
|
os.unlink("pid", dir_fd=directory_descriptor)
|
|
3208
3314
|
except FileNotFoundError:
|
|
3315
|
+
namespace_attempts += 1
|
|
3209
3316
|
continue
|
|
3210
3317
|
os.fsync(directory_descriptor)
|
|
3211
3318
|
finally:
|
|
3212
3319
|
os.close(directory_descriptor)
|
|
3320
|
+
namespace_attempts += 1
|
|
3213
3321
|
continue
|
|
3214
3322
|
else:
|
|
3215
3323
|
owned = True
|
|
3324
|
+
if wait_for_prepared:
|
|
3325
|
+
check_wait_root()
|
|
3216
3326
|
for _ in range(3):
|
|
3217
3327
|
try:
|
|
3218
3328
|
fsync_directory(root)
|
|
@@ -3232,7 +3342,9 @@ if action == "acquire":
|
|
|
3232
3342
|
raise SystemExit(0)
|
|
3233
3343
|
raise RuntimeError("AgentsServer install lock ownership kept changing")
|
|
3234
3344
|
finally:
|
|
3235
|
-
if not owned:
|
|
3345
|
+
if not owned and wait_for_prepared:
|
|
3346
|
+
cleanup_wait_candidate(candidate, candidate_info)
|
|
3347
|
+
elif not owned:
|
|
3236
3348
|
try:
|
|
3237
3349
|
(candidate / "pid").unlink()
|
|
3238
3350
|
except FileNotFoundError:
|
|
@@ -3640,6 +3752,7 @@ validate_staged_release_runtime() (
|
|
|
3640
3752
|
"$STAGE_DIR/codex_app_server.py" \
|
|
3641
3753
|
"$STAGE_DIR/codex_auth.py" \
|
|
3642
3754
|
"$STAGE_DIR/codex_provider.py" \
|
|
3755
|
+
"$STAGE_DIR/provider_connections.py" "$STAGE_DIR/cursor_api_key.py" \
|
|
3643
3756
|
"$STAGE_DIR/side_questions.py" \
|
|
3644
3757
|
"$STAGE_DIR/workspace_git.py" \
|
|
3645
3758
|
"$STAGE_DIR/codex_side_question.py" \
|
|
@@ -3667,7 +3780,7 @@ validate_staged_release_runtime() (
|
|
|
3667
3780
|
"$STAGE_DIR/update_runner.py"
|
|
3668
3781
|
"$STAGE_DIR/.venv/bin/python" -m py_compile "$STAGE_DIR/server_instances.py"
|
|
3669
3782
|
"$STAGE_DIR/.venv/bin/python" -m compileall -q "$STAGE_DIR/agentsdock_team_hub"
|
|
3670
|
-
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, side_questions, title_generation, codex_side_question, 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
|
|
3783
|
+
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, provider_connections, cursor_api_key, side_questions, title_generation, codex_side_question, 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
|
|
3671
3784
|
)
|
|
3672
3785
|
|
|
3673
3786
|
abort_unclaimed_team_hub_reactivation() {
|
|
@@ -486,6 +486,38 @@ def build_opencode_env_overrides(
|
|
|
486
486
|
}
|
|
487
487
|
|
|
488
488
|
|
|
489
|
+
def build_opencode_mcp_overrides(
|
|
490
|
+
existing_config: str | None, *, name: str, command: list[str],
|
|
491
|
+
environment: dict[str, str], enforced_agent_name: str | None = None,
|
|
492
|
+
) -> dict[str, str]:
|
|
493
|
+
"""Add one unpredictable, run-bound tool without granting shell/MCP wildcards.
|
|
494
|
+
|
|
495
|
+
Merge after the endpoint and permission configuration. Never persist this
|
|
496
|
+
configuration, credentials, or permission to the user's OpenCode files.
|
|
497
|
+
Default mode keeps the native agent and any higher-priority managed rules.
|
|
498
|
+
"""
|
|
499
|
+
if not re.fullmatch(r"agentsdock_[0-9a-f]{32}", name):
|
|
500
|
+
raise ValueError("invalid OpenCode provider tool name")
|
|
501
|
+
config = json.loads(existing_config) if existing_config else {}
|
|
502
|
+
if not isinstance(config, dict):
|
|
503
|
+
raise ValueError("OpenCode inline config must be an object")
|
|
504
|
+
mcp = config.setdefault("mcp", {})
|
|
505
|
+
if not isinstance(mcp, dict) or name in mcp:
|
|
506
|
+
raise ValueError("OpenCode provider MCP configuration conflicts")
|
|
507
|
+
mcp[name] = {"type": "local", "command": command,
|
|
508
|
+
"environment": environment, "enabled": True}
|
|
509
|
+
permissions = config.get("permission", {})
|
|
510
|
+
if isinstance(permissions, str):
|
|
511
|
+
permissions = {"*": permissions}
|
|
512
|
+
if not isinstance(permissions, dict):
|
|
513
|
+
raise ValueError("OpenCode permissions must be an object or action")
|
|
514
|
+
config["permission"] = {**permissions, name + "_run": "allow"}
|
|
515
|
+
if enforced_agent_name:
|
|
516
|
+
agent = config["agent"][enforced_agent_name]
|
|
517
|
+
agent["permission"][name + "_run"] = "allow"
|
|
518
|
+
return {"OPENCODE_CONFIG_CONTENT": json.dumps(config, separators=(",", ":"))}
|
|
519
|
+
|
|
520
|
+
|
|
489
521
|
def build_opencode_cmd(
|
|
490
522
|
sess: dict[str, Any],
|
|
491
523
|
prompt: str,
|