@agentsdock/server 1.0.10 → 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 +1658 -498
- package/server/agentsdock_jobs.py +2 -2
- package/server/claude_history_repair.py +46 -61
- package/server/codex_auth.py +76 -7
- package/server/codex_history_repair.py +203 -38
- 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 +26 -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 +217 -15
- package/server/shared_chat_analytics_web.py +5 -0
- package/server/title_generation.py +60 -1
- package/server/transcript_reader.py +334 -0
- package/server/update_runner.py +4 -2
|
@@ -26,6 +26,7 @@ import time
|
|
|
26
26
|
from typing import Any, Callable, Iterator
|
|
27
27
|
|
|
28
28
|
from shared_chat_videos import normalize_shared_chat_videos
|
|
29
|
+
from shared_chat_analytics_web import SCRIPT as _ANALYTICS_SCRIPT, CSP_HASH as _ANALYTICS_HASH
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
# Pagination targets, not quotas: one complete message may exceed a page target.
|
|
@@ -644,16 +645,21 @@ def _render_message_markdown(text: str) -> str:
|
|
|
644
645
|
return "".join(parts)
|
|
645
646
|
|
|
646
647
|
|
|
647
|
-
def public_chat_share_headers(*, allow_unlock_form: bool = False) -> dict[str, str]:
|
|
648
|
+
def public_chat_share_headers(*, allow_unlock_form: bool = False, analytics: bool = False) -> dict[str, str]:
|
|
648
649
|
"""Use these headers for both successful viewer pages and unavailable pages."""
|
|
650
|
+
analytics = analytics and not allow_unlock_form
|
|
649
651
|
return {
|
|
650
652
|
"Content-Type": "text/html; charset=utf-8",
|
|
651
653
|
"Content-Security-Policy": (
|
|
652
|
-
"default-src 'none';
|
|
653
|
-
f"
|
|
654
|
-
"
|
|
654
|
+
"default-src 'none'; "
|
|
655
|
+
+ (f"script-src 'sha256-{_ANALYTICS_HASH}'; " if analytics else "script-src 'none'; ")
|
|
656
|
+
+ f"style-src 'sha256-{_STYLE_HASH}'; "
|
|
657
|
+
"img-src 'none'; media-src 'self'; "
|
|
658
|
+
+ ("connect-src https://api.mixpanel.com; " if analytics else "connect-src 'none'; ")
|
|
659
|
+
+ "base-uri 'none'; "
|
|
655
660
|
+ ("form-action 'self'; frame-ancestors 'none'; sandbox allow-forms allow-same-origin"
|
|
656
|
-
if allow_unlock_form else "form-action 'none'; frame-ancestors 'none'; sandbox allow-same-origin"
|
|
661
|
+
if allow_unlock_form else "form-action 'none'; frame-ancestors 'none'; sandbox allow-same-origin"
|
|
662
|
+
+ (" allow-scripts" if analytics else ""))
|
|
657
663
|
),
|
|
658
664
|
"Cache-Control": "no-store",
|
|
659
665
|
# A native navigation form under no-referrer submits Origin: null in
|
|
@@ -694,7 +700,8 @@ def render_public_chat_unlock_html(share_id: str, *, invalid_token: bool = False
|
|
|
694
700
|
|
|
695
701
|
|
|
696
702
|
def render_public_chat_html(snapshot: dict[str, Any], *, page: int = 0, page_count: int = 1,
|
|
697
|
-
message_count: int | None = None, navigation_base: str | None = None
|
|
703
|
+
message_count: int | None = None, navigation_base: str | None = None,
|
|
704
|
+
analytics: bool = False) -> bytes:
|
|
698
705
|
"""Render a static transcript with escaped, presentation-only Markdown."""
|
|
699
706
|
if not isinstance(snapshot, dict) or set(snapshot) != {"title", "created_at", "messages"}:
|
|
700
707
|
raise PublicChatShareValidationError("Invalid public snapshot fields.")
|
|
@@ -753,5 +760,8 @@ def render_public_chat_html(snapshot: dict[str, Any], *, page: int = 0, page_cou
|
|
|
753
760
|
f'<figcaption>{filename}</figcaption></figure>')
|
|
754
761
|
parts.append('</div>')
|
|
755
762
|
parts.append('</section>')
|
|
756
|
-
parts.append(f'</div>{navigation}<footer class="conversation-footer"><p>This is a saved copy. Replies and updates stay in the original chat.</p><p class="footer-brand">Shared with AgentsDock</p></footer><div id="conversation-end"></div></main
|
|
763
|
+
parts.append(f'</div>{navigation}<footer class="conversation-footer"><p>This is a saved copy. Replies and updates stay in the original chat.</p><p class="footer-brand">Shared with AgentsDock</p></footer><div id="conversation-end"></div></main>')
|
|
764
|
+
if analytics:
|
|
765
|
+
parts.append(f'<script data-share-analytics>{_ANALYTICS_SCRIPT}</script>')
|
|
766
|
+
parts.append('</body></html>')
|
|
757
767
|
return "".join(parts).encode("utf-8")
|
|
@@ -587,6 +587,20 @@ def describe(instance: Instance) -> dict:
|
|
|
587
587
|
}
|
|
588
588
|
|
|
589
589
|
|
|
590
|
+
def pairing_info(instance: Instance) -> dict:
|
|
591
|
+
"""Private pipe to the CLI QR renderer; never write these details to logs."""
|
|
592
|
+
for target in (instance.config, instance.config / "env"):
|
|
593
|
+
check_path(target, instance.home)
|
|
594
|
+
env = read_config(instance)
|
|
595
|
+
token = env.get("AGENTSDOCK_AGENT_TOKEN", "")
|
|
596
|
+
if not token or any(ord(char) < 32 or ord(char) == 127 for char in token):
|
|
597
|
+
raise ValueError("This instance has no usable saved access token.")
|
|
598
|
+
port = int(env.get("AGENTSDOCK_AGENT_PORT", "7850"))
|
|
599
|
+
bind = env.get("AGENTSDOCK_AGENT_BIND", "0.0.0.0")
|
|
600
|
+
choices = connection_choices(bind, port, candidate_addresses(bind, port), tailscale_status())
|
|
601
|
+
return {"name": instance.name, "accessToken": token, "choices": choices}
|
|
602
|
+
|
|
603
|
+
|
|
590
604
|
def connection_choices(bind: str, port: int, addresses: list[str], network: dict[str, str]) -> dict:
|
|
591
605
|
"""Label candidates, never infer Tailscale connectivity from an IP range."""
|
|
592
606
|
choices = {"local": [], "lan": [], "other": [], "tailscale": "", "tailscale_note": ""}
|
|
@@ -652,11 +666,8 @@ def show(instance: Instance, *, network: dict[str, str] | None = None):
|
|
|
652
666
|
print()
|
|
653
667
|
|
|
654
668
|
|
|
655
|
-
def
|
|
656
|
-
"""
|
|
657
|
-
item = describe(instance)
|
|
658
|
-
print(terminal_color(f"Server ({item['name']}):", "34"))
|
|
659
|
-
print(f" Status: {item['status']}")
|
|
669
|
+
def show_connection_addresses(instance: Instance, item: dict, network: dict[str, str]) -> None:
|
|
670
|
+
"""Use the same binding-aware address order for status and token display."""
|
|
660
671
|
addresses = []
|
|
661
672
|
if item["port"]:
|
|
662
673
|
choices = connection_choices(read_config(instance).get("AGENTSDOCK_AGENT_BIND", "0.0.0.0"),
|
|
@@ -669,11 +680,150 @@ def show_status(instance: Instance, *, network: dict[str, str]) -> None:
|
|
|
669
680
|
print(" Address: " + (addresses[0] if addresses else "Not configured"))
|
|
670
681
|
for address in addresses[1:]:
|
|
671
682
|
print(" " + address)
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def show_status(instance: Instance, *, network: dict[str, str]) -> None:
|
|
686
|
+
"""Read-only per-instance summary; never print configuration or tokens."""
|
|
687
|
+
item = describe(instance)
|
|
688
|
+
print(terminal_color(f"Server ({item['name']}):", "34"))
|
|
689
|
+
print(f" Status: {item['status']}")
|
|
690
|
+
show_connection_addresses(instance, item, network)
|
|
672
691
|
print(f" Version: {item['version'] or 'unknown'}")
|
|
673
692
|
print(f" Port: {item['port'] or 'Not configured'}")
|
|
674
693
|
print()
|
|
675
694
|
|
|
676
695
|
|
|
696
|
+
LOG_READ_BYTES = 1024 * 1024
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def log_line_count(value: str) -> int:
|
|
700
|
+
if not re.fullmatch(r"[0-9]{1,5}", value) or int(value) > 10000:
|
|
701
|
+
raise argparse.ArgumentTypeError("Log lines must be between 0 and 10000.")
|
|
702
|
+
return int(value)
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def log_sources(instance: Instance, platform: str) -> tuple[list[Path], list[str]]:
|
|
706
|
+
"""Resolve fixed local log sources; never execute configuration or adopt paths."""
|
|
707
|
+
manifest = instance.runtime / "execution-layout.json"
|
|
708
|
+
check_path(manifest, instance.home)
|
|
709
|
+
if manifest.exists():
|
|
710
|
+
from execution_install import installed_layout
|
|
711
|
+
|
|
712
|
+
layout = installed_layout(instance.runtime)
|
|
713
|
+
if (instance.name != "default" or layout.install_root != instance.runtime
|
|
714
|
+
or layout.config_root != instance.config or layout.state_root != instance.state
|
|
715
|
+
or layout.home != instance.home
|
|
716
|
+
or layout.platform != ("Darwin" if platform == "darwin" else "Linux")):
|
|
717
|
+
raise ValueError("Execution log layout does not match the selected instance.")
|
|
718
|
+
return ([instance.state / "execution/logs" / f"{role}.{stream}.log"
|
|
719
|
+
for role in ("gateway", "worker") for stream in ("stdout", "stderr")],
|
|
720
|
+
["agents-server-gateway.service", "agents-server.service"])
|
|
721
|
+
validate_binding(instance, platform)
|
|
722
|
+
return ([instance.logs / "server.log", instance.logs / "server-error.log"],
|
|
723
|
+
[service_name(instance.name) + ".service"])
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def read_log_slice(instance: Instance, path: Path, lines: int,
|
|
727
|
+
cursor: tuple[int, int, int] | None = None) -> tuple[bytes, tuple[int, int, int] | None, bool]:
|
|
728
|
+
"""Bounded tail on first read, then bounded appended bytes; reopen on rotation.
|
|
729
|
+
|
|
730
|
+
Revalidate every open, including followed replacements. No shell tail process,
|
|
731
|
+
FIFO, symlink or file outside the selected fixed log roots is followed.
|
|
732
|
+
"""
|
|
733
|
+
check_path(path, instance.home)
|
|
734
|
+
try:
|
|
735
|
+
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
|
|
736
|
+
except FileNotFoundError:
|
|
737
|
+
# Remember that viewing has begun so a file created during --follow is
|
|
738
|
+
# read from its start, including with --lines 0.
|
|
739
|
+
return b"", cursor or (0, 0, 0), False
|
|
740
|
+
with os.fdopen(fd, "rb") as handle:
|
|
741
|
+
info = os.fstat(handle.fileno())
|
|
742
|
+
if (not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid()
|
|
743
|
+
or info.st_mode & 0o022 or info.st_nlink != 1):
|
|
744
|
+
raise ValueError(f"Unsafe service log: {path.name}")
|
|
745
|
+
identity = (info.st_dev, info.st_ino)
|
|
746
|
+
truncated = False
|
|
747
|
+
if cursor is None:
|
|
748
|
+
start = max(0, info.st_size - LOG_READ_BYTES) if lines else info.st_size
|
|
749
|
+
truncated = bool(start and lines)
|
|
750
|
+
elif cursor[:2] == identity and info.st_size >= cursor[2]:
|
|
751
|
+
start = cursor[2]
|
|
752
|
+
else:
|
|
753
|
+
start = 0 # The file was replaced or truncated, not a new history scan.
|
|
754
|
+
handle.seek(start)
|
|
755
|
+
data = handle.read(min(LOG_READ_BYTES, max(0, info.st_size - start)))
|
|
756
|
+
next_cursor = (*identity, start + len(data))
|
|
757
|
+
if cursor is None:
|
|
758
|
+
if truncated:
|
|
759
|
+
_partial, separator, rest = data.partition(b"\n")
|
|
760
|
+
data = rest if separator else b""
|
|
761
|
+
data = b"\n".join(data.split(b"\n")[-lines - (1 if data.endswith(b"\n") else 0):]) if lines else b""
|
|
762
|
+
return data, next_cursor, truncated
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
def print_log_data(data: bytes) -> None:
|
|
766
|
+
# Logs are untrusted text; do not execute terminal escape/control sequences.
|
|
767
|
+
text = data.decode("utf-8", errors="replace")
|
|
768
|
+
text = "".join(f"\\x{ord(char):02x}" if (ord(char) < 32 and char not in "\n\t")
|
|
769
|
+
or 127 <= ord(char) < 160 else char for char in text)
|
|
770
|
+
print(text, end="" if text.endswith("\n") else "\n", flush=True)
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def show_logs(instance: Instance, *, lines: int = 100, follow: bool = False,
|
|
774
|
+
platform: str = sys.platform) -> int:
|
|
775
|
+
if platform != "darwin" and not platform.startswith("linux"):
|
|
776
|
+
raise ValueError("Service logs are supported on macOS and Linux only.")
|
|
777
|
+
paths, units = log_sources(instance, platform)
|
|
778
|
+
port = read_config(instance).get("AGENTSDOCK_AGENT_PORT", "")
|
|
779
|
+
print(terminal_color(f"Logs ({instance.name}):", "34"), flush=True)
|
|
780
|
+
if port.isdigit() and 1 <= int(port) <= 65535:
|
|
781
|
+
print(f" Port: {int(port)}", flush=True)
|
|
782
|
+
print(" Logs may contain private data. Review before sharing.", flush=True)
|
|
783
|
+
if follow:
|
|
784
|
+
print(" Following new entries; Ctrl+C stops viewing, not the server.", flush=True)
|
|
785
|
+
if platform.startswith("linux"):
|
|
786
|
+
print(f" Last {lines} journal entries across: {', '.join(units)}\n", flush=True)
|
|
787
|
+
command = ["journalctl", "--user", "--no-pager", "--output=short-iso", "--lines", str(lines)]
|
|
788
|
+
for unit in units:
|
|
789
|
+
command += ["--unit", unit]
|
|
790
|
+
if follow:
|
|
791
|
+
command.append("--follow")
|
|
792
|
+
try:
|
|
793
|
+
result = subprocess.run(command, env={**os.environ, "SYSTEMD_COLORS": "0",
|
|
794
|
+
"SYSTEMD_PAGERSECURE": "1"}, check=False,
|
|
795
|
+
timeout=None if follow else 15)
|
|
796
|
+
return result.returncode if result.returncode >= 0 else 130
|
|
797
|
+
except subprocess.TimeoutExpired:
|
|
798
|
+
raise ValueError("Reading the service journal timed out. Try again with fewer lines.") from None
|
|
799
|
+
except KeyboardInterrupt:
|
|
800
|
+
print("\nStopped viewing logs.", flush=True)
|
|
801
|
+
return 0
|
|
802
|
+
print(f" Last {lines} lines per file (up to 1 MiB per file).\n", flush=True)
|
|
803
|
+
cursors: dict[Path, tuple[int, int, int] | None] = {path: None for path in paths}
|
|
804
|
+
first = True
|
|
805
|
+
try:
|
|
806
|
+
while True:
|
|
807
|
+
for path in paths:
|
|
808
|
+
data, cursor, truncated = read_log_slice(instance, path, lines, cursors[path])
|
|
809
|
+
cursors[path] = cursor
|
|
810
|
+
if first or data or truncated:
|
|
811
|
+
print(f"==> {path.name} <==", flush=True)
|
|
812
|
+
if truncated:
|
|
813
|
+
print("[Older bytes omitted: initial read is limited to 1 MiB.]", flush=True)
|
|
814
|
+
if data:
|
|
815
|
+
print_log_data(data)
|
|
816
|
+
elif first:
|
|
817
|
+
print("(no log entries to show)", flush=True)
|
|
818
|
+
if not follow:
|
|
819
|
+
return 0
|
|
820
|
+
first = False
|
|
821
|
+
time.sleep(0.25)
|
|
822
|
+
except KeyboardInterrupt:
|
|
823
|
+
print("\nStopped viewing logs.", flush=True)
|
|
824
|
+
return 0
|
|
825
|
+
|
|
826
|
+
|
|
677
827
|
def read_token_menu_key(fd: int) -> str:
|
|
678
828
|
key = os.read(fd, 1)
|
|
679
829
|
if key in (b"", b"\x03", b"\x04"):
|
|
@@ -740,9 +890,10 @@ def arrow_token_choice(rows: list[str], fd: int, settings: list) -> int | None:
|
|
|
740
890
|
sys.stdout.flush()
|
|
741
891
|
|
|
742
892
|
|
|
743
|
-
def choose_token_instance(registry: Registry) -> Instance | None:
|
|
893
|
+
def choose_token_instance(registry: Registry, items: list[Instance] | None = None) -> Instance | None:
|
|
744
894
|
"""Select by stable instance name; never default or expose unselected tokens."""
|
|
745
|
-
items
|
|
895
|
+
if items is None:
|
|
896
|
+
items = registry.instances()
|
|
746
897
|
if not items:
|
|
747
898
|
print("No installations found. Run agentsdock setup to create your first server.")
|
|
748
899
|
return None
|
|
@@ -793,6 +944,34 @@ def choose_token_instance(registry: Registry) -> Instance | None:
|
|
|
793
944
|
print(f"Please enter a number from 1 to {len(items)} or a listed server name (Enter cancels).")
|
|
794
945
|
|
|
795
946
|
|
|
947
|
+
def show_instance_token(instance: Instance, *, network: dict[str, str] | None = None) -> int:
|
|
948
|
+
# Redirected explicit token commands retain their raw, single-token output.
|
|
949
|
+
if sys.stdin.isatty() and sys.stdout.isatty():
|
|
950
|
+
print(terminal_color(f"\nServer ({instance.name}):", "34"))
|
|
951
|
+
show_connection_addresses(instance, describe(instance), network if network is not None else tailscale_status())
|
|
952
|
+
sys.stdout.flush() # Keep the URL before the child helper's token/prompt.
|
|
953
|
+
try:
|
|
954
|
+
run(["/bin/bash", str(ROOT / "install.sh"), "--instance", instance.name, "--show-token"],
|
|
955
|
+
env=clean_environment(instance), cwd=ROOT)
|
|
956
|
+
return 0
|
|
957
|
+
except subprocess.CalledProcessError as exc:
|
|
958
|
+
return exc.returncode if exc.returncode > 0 else 1
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
def offer_status_token(registry: Registry, items: list[Instance], network: dict[str, str]) -> int:
|
|
962
|
+
if not items or not (sys.stdin.isatty() and sys.stdout.isatty()):
|
|
963
|
+
return 0
|
|
964
|
+
try:
|
|
965
|
+
answer = input("View a server's URL and access token? [y/N] ").strip().lower()
|
|
966
|
+
except (EOFError, KeyboardInterrupt):
|
|
967
|
+
print()
|
|
968
|
+
return 0
|
|
969
|
+
if answer not in {"y", "yes"}:
|
|
970
|
+
return 0
|
|
971
|
+
selected = choose_token_instance(registry, items)
|
|
972
|
+
return show_instance_token(selected, network=network) if selected is not None else 0
|
|
973
|
+
|
|
974
|
+
|
|
796
975
|
def terminal_color(text: str, code: str) -> str:
|
|
797
976
|
color = sys.stdout.isatty() and os.environ.get("TERM") != "dumb" and "NO_COLOR" not in os.environ
|
|
798
977
|
return f"\033[{code}m{text}\033[0m" if color else text
|
|
@@ -938,11 +1117,19 @@ def parser() -> argparse.ArgumentParser:
|
|
|
938
1117
|
result = argparse.ArgumentParser(description="Independent AgentsServer instances for the current OS user. Defaults to list.")
|
|
939
1118
|
commands = result.add_subparsers(dest="command")
|
|
940
1119
|
commands.add_parser("list")
|
|
941
|
-
commands.add_parser("token", help="Choose an existing server and display
|
|
1120
|
+
token = commands.add_parser("token", help="Choose an existing server and display its URL and access token")
|
|
1121
|
+
token.add_argument("name", nargs="?", type=instance_name)
|
|
942
1122
|
info = commands.add_parser("info")
|
|
943
1123
|
info.add_argument("name", type=instance_name)
|
|
1124
|
+
pairing = commands.add_parser("_pair-info", help=argparse.SUPPRESS)
|
|
1125
|
+
pairing.add_argument("name", type=instance_name)
|
|
944
1126
|
status = commands.add_parser("status", help="Show readable details for all instances or one name")
|
|
945
1127
|
status.add_argument("name", nargs="?", type=instance_name)
|
|
1128
|
+
logs = commands.add_parser("logs", help="Read recent local service logs; use --follow for live output")
|
|
1129
|
+
logs.add_argument("name", nargs="?", type=instance_name)
|
|
1130
|
+
logs.add_argument("--instance", dest="named", type=instance_name)
|
|
1131
|
+
logs.add_argument("-n", "--lines", type=log_line_count, default=100)
|
|
1132
|
+
logs.add_argument("-f", "--follow", action="store_true")
|
|
946
1133
|
new = commands.add_parser("new")
|
|
947
1134
|
new.add_argument("--name", type=instance_name)
|
|
948
1135
|
new.add_argument("--port", type=int)
|
|
@@ -979,16 +1166,30 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
979
1166
|
validate_binding(instance)
|
|
980
1167
|
print(instance.shell_bindings())
|
|
981
1168
|
return 0
|
|
1169
|
+
if args.command == "_pair-info":
|
|
1170
|
+
selected = next((item for item in registry.instances() if item.name == args.name), None)
|
|
1171
|
+
if selected is None:
|
|
1172
|
+
raise ValueError("Unknown instance. Use agentsdock list.")
|
|
1173
|
+
print(json.dumps(pairing_info(selected)))
|
|
1174
|
+
return 0
|
|
1175
|
+
if args.command == "logs":
|
|
1176
|
+
if args.name and args.named:
|
|
1177
|
+
raise ValueError("Select the log instance once: NAME or --instance NAME.")
|
|
1178
|
+
name = args.name or args.named or "default"
|
|
1179
|
+
selected = next((item for item in registry.instances() if item.name == name), None)
|
|
1180
|
+
if selected is None:
|
|
1181
|
+
raise ValueError(f"Unknown instance {name!r}. Use agentsdock list to find its name.")
|
|
1182
|
+
return show_logs(selected, lines=args.lines, follow=args.follow)
|
|
982
1183
|
if args.command == "token":
|
|
983
|
-
|
|
1184
|
+
if args.name:
|
|
1185
|
+
# The installer retains read-only legacy token discovery even
|
|
1186
|
+
# when an old default install is absent from the new registry.
|
|
1187
|
+
selected = Instance(args.name, registry.home)
|
|
1188
|
+
else:
|
|
1189
|
+
selected = choose_token_instance(registry)
|
|
984
1190
|
if selected is None:
|
|
985
1191
|
return 0
|
|
986
|
-
|
|
987
|
-
run(["/bin/bash", str(ROOT / "install.sh"), "--instance", selected.name, "--show-token"],
|
|
988
|
-
env=clean_environment(selected), cwd=ROOT)
|
|
989
|
-
return 0
|
|
990
|
-
except subprocess.CalledProcessError as exc:
|
|
991
|
-
return exc.returncode if exc.returncode > 0 else 1
|
|
1192
|
+
return show_instance_token(selected)
|
|
992
1193
|
if args.command in {None, "list", "info", "status"}:
|
|
993
1194
|
items = registry.instances()
|
|
994
1195
|
if args.command in {"info", "status"} and args.name:
|
|
@@ -1003,6 +1204,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1003
1204
|
show_status(item, network=network)
|
|
1004
1205
|
if not items:
|
|
1005
1206
|
print("No installations found. Run agentsdock setup to create your first server.")
|
|
1207
|
+
return offer_status_token(registry, items, network)
|
|
1006
1208
|
else:
|
|
1007
1209
|
network = tailscale_status() if items else None
|
|
1008
1210
|
print("NAME STATUS PORT")
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"""Generated from the limited shared-chat analytics entry; do not edit by hand."""
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
SCRIPT = json.loads("\"/*! @noble/hashes\\nThe MIT License (MIT)\\n\\nCopyright (c) 2022 Paul Miller (https://paulmillr.com)\\n\\nPermission is hereby granted, free of charge, to any person obtaining a copy\\nof this software and associated documentation files (the “Software”), to deal\\nin the Software without restriction, including without limitation the rights\\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\\ncopies of the Software, and to permit persons to whom the Software is\\nfurnished to do so, subject to the following conditions:\\n\\nThe above copyright notice and this permission notice shall be included in\\nall copies or substantial portions of the Software.\\n\\nTHE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\\nTHE SOFTWARE.\\n*/(function(){\\\"use strict\\\";function H(e){return e instanceof Uint8Array||ArrayBuffer.isView(e)&&e.constructor.name===\\\"Uint8Array\\\"&&\\\"BYTES_PER_ELEMENT\\\"in e&&e.BYTES_PER_ELEMENT===1}function p(e,t,s=\\\"\\\"){const n=H(e),r=e?.length;if(!n||t!==void 0){const i=s&&`\\\"${s}\\\" `,o=\\\"\\\",f=n?`length=${r}`:`type=${typeof e}`,h=i+\\\"expected Uint8Array\\\"+o+\\\", got \\\"+f;throw n?new RangeError(h):new TypeError(h)}return e}function m(e,t=!0){if(e.destroyed)throw new Error(\\\"Hash instance has been destroyed\\\");if(t&&e.finished)throw new Error(\\\"Hash#digest() has already been called\\\")}function S(e,t){p(e,void 0,\\\"digestInto() output\\\");const s=t.outputLen;if(e.length<s)throw new RangeError('\\\"digestInto() output\\\" expected to be of length >='+s)}function y(...e){for(let t=0;t<e.length;t++)e[t].fill(0)}function _(e){return new DataView(e.buffer,e.byteOffset,e.byteLength)}function d(e,t){return e<<32-t|e>>>t}const T=typeof Uint8Array.from([]).toHex==\\\"function\\\"&&typeof Uint8Array.fromHex==\\\"function\\\",k=Array.from({length:256},(e,t)=>t.toString(16).padStart(2,\\\"0\\\"));function B(e){if(p(e),T)return e.toHex();let t=\\\"\\\";for(let s=0;s<e.length;s++)t+=k[e[s]];return t}function O(e){if(typeof e!=\\\"string\\\")throw new TypeError(\\\"string expected\\\");return new Uint8Array(new TextEncoder().encode(e))}function U(e,t={}){const s=(r,c)=>e(c).update(r).digest(),n=e(void 0);return s.outputLen=n.outputLen,s.blockLen=n.blockLen,s.canXOF=n.canXOF,s.create=r=>e(r),Object.assign(s,t),Object.freeze(s)}const v=e=>({oid:Uint8Array.from([6,9,96,134,72,1,101,3,4,2,e])});function C(e,t,s){return e&t^~e&s}function I(e,t,s){return e&t^e&s^t&s}class D{blockLen;outputLen;canXOF=!1;padOffset;isLE;buffer;view;finished=!1;length=0;pos=0;destroyed=!1;constructor(t,s,n,r){this.blockLen=t,this.outputLen=s,this.padOffset=n,this.isLE=r,this.buffer=new Uint8Array(t),this.view=_(this.buffer)}update(t){m(this),p(t);const{view:s,buffer:n,blockLen:r}=this,c=t.length;for(let i=0;i<c;){const o=Math.min(r-this.pos,c-i);if(o===r){const f=_(t);for(;r<=c-i;i+=r)this.process(f,i);continue}n.set(t.subarray(i,i+o),this.pos),this.pos+=o,i+=o,this.pos===r&&(this.process(s,0),this.pos=0)}return this.length+=t.length,this.roundClean(),this}digestInto(t){m(this),S(t,this),this.finished=!0;const{buffer:s,view:n,blockLen:r,isLE:c}=this;let{pos:i}=this;s[i++]=128,y(this.buffer.subarray(i)),this.padOffset>r-i&&(this.process(n,0),i=0);for(let a=i;a<r;a++)s[a]=0;n.setBigUint64(r-8,BigInt(this.length*8),c),this.process(n,0);const o=_(t),f=this.outputLen;if(f%4)throw new Error(\\\"_sha2: outputLen must be aligned to 32bit\\\");const h=f/4,l=this.get();if(h>l.length)throw new Error(\\\"_sha2: outputLen bigger than state\\\");for(let a=0;a<h;a++)o.setUint32(4*a,l[a],c)}digest(){const{buffer:t,outputLen:s}=this;this.digestInto(t);const n=t.slice(0,s);return this.destroy(),n}_cloneInto(t){t||=new this.constructor,t.set(...this.get());const{blockLen:s,buffer:n,length:r,finished:c,destroyed:i,pos:o}=this;return t.destroyed=i,t.finished=c,t.length=r,t.pos=o,r%s&&t.buffer.set(n),t}clone(){return this._cloneInto()}}const x=Uint32Array.from([1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225]),N=Uint32Array.from([1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298]),u=new Uint32Array(64);class F extends D{constructor(t){super(64,t,8,!1)}get(){const{A:t,B:s,C:n,D:r,E:c,F:i,G:o,H:f}=this;return[t,s,n,r,c,i,o,f]}set(t,s,n,r,c,i,o,f){this.A=t|0,this.B=s|0,this.C=n|0,this.D=r|0,this.E=c|0,this.F=i|0,this.G=o|0,this.H=f|0}process(t,s){for(let a=0;a<16;a++,s+=4)u[a]=t.getUint32(s,!1);for(let a=16;a<64;a++){const g=u[a-15],b=u[a-2],L=d(g,7)^d(g,18)^g>>>3,w=d(b,17)^d(b,19)^b>>>10;u[a]=w+u[a-7]+L+u[a-16]|0}let{A:n,B:r,C:c,D:i,E:o,F:f,G:h,H:l}=this;for(let a=0;a<64;a++){const g=d(o,6)^d(o,11)^d(o,25),b=l+g+C(o,f,h)+N[a]+u[a]|0,w=(d(n,2)^d(n,13)^d(n,22))+I(n,r,c)|0;l=h,h=f,f=o,o=i+b|0,i=c,c=r,r=n,n=b+w|0}n=n+this.A|0,r=r+this.B|0,c=c+this.C|0,i=i+this.D|0,o=o+this.E|0,f=f+this.F|0,h=h+this.G|0,l=l+this.H|0,this.set(n,r,c,i,o,f,h,l)}roundClean(){y(u)}destroy(){this.destroyed=!0,this.set(0,0,0,0,0,0,0,0),y(this.buffer)}}class M extends F{A=x[0]|0;B=x[1]|0;C=x[2]|0;D=x[3]|0;E=x[4]|0;F=x[5]|0;G=x[6]|0;H=x[7]|0;constructor(){super(32)}}const V=U(()=>new M,v(1)),$=[\\\"shared_chat_snapshot_activated\\\",\\\"shared_chat_interactive_activated\\\",\\\"shared_chat_message_sent\\\"],E=\\\"agentsdock:shared-analytics-seen:v1\\\",A=new Set;function G(e,t,s=\\\"\\\"){try{if(!$.includes(e)||!/^\\\\/(?:interactive-chat\\\\/interactive_[a-f0-9]{32}|shared-chat\\\\/share_[a-f0-9]{32}|share\\\\/[A-Za-z0-9_-]{43})$/.test(t)||e===\\\"shared_chat_message_sent\\\"&&!/^[A-Za-z0-9_-]{16,128}$/.test(s)||navigator.doNotTrack===\\\"1\\\"||navigator.globalPrivacyControl||localStorage.getItem(\\\"agentsdock:analytics-consent:v1\\\")===\\\"denied\\\")return;const n=B(V(O(JSON.stringify([e,t,s])))),r=JSON.parse(sessionStorage.getItem(E)??\\\"[]\\\"),c=Array.isArray(r)?r.filter(f=>typeof f==\\\"string\\\"&&/^[a-f0-9]{64}$/.test(f)):[];if(A.has(n)||c.includes(n)||c.length>=4096)return;sessionStorage.setItem(E,JSON.stringify([...c,n])),A.add(n);const i=new AbortController,o=setTimeout(()=>i.abort(),5e3);try{fetch(\\\"https://api.mixpanel.com/track?ip=0\\\",{method:\\\"POST\\\",credentials:\\\"omit\\\",referrerPolicy:\\\"no-referrer\\\",headers:{\\\"Content-Type\\\":\\\"application/json\\\"},signal:i.signal,body:JSON.stringify([{event:e,properties:{token:\\\"66b4fef625e5750d5527870f0ca96d5e\\\",distinct_id:\\\"\\\",platform:\\\"shared_web\\\",time:Math.floor(Date.now()/1e3)}}])}).catch(()=>{}).finally(()=>clearTimeout(o))}catch{clearTimeout(o)}}catch{}}G(\\\"shared_chat_snapshot_activated\\\",location.pathname)})();\"")
|
|
5
|
+
CSP_HASH = "mGoIxolAbIiMaBueHq0pZetv9HOC3PsCEy+QNKDYsaM="
|
|
@@ -13,6 +13,9 @@ import sys
|
|
|
13
13
|
import tempfile
|
|
14
14
|
import time
|
|
15
15
|
import unicodedata
|
|
16
|
+
from urllib.parse import quote
|
|
17
|
+
|
|
18
|
+
import httpx
|
|
16
19
|
|
|
17
20
|
from codex_app_server import CodexAppServerClient
|
|
18
21
|
from codex_side_question import isolated_config
|
|
@@ -35,6 +38,12 @@ class TitleGenerationError(Exception):
|
|
|
35
38
|
"""Keep errors content-free: prompts, credentials and output are private."""
|
|
36
39
|
|
|
37
40
|
|
|
41
|
+
def gemini_title_seed(text: str) -> str:
|
|
42
|
+
# Pasted logs often put the actual question at the end. Keep the same
|
|
43
|
+
# bounded budget as other providers without storing the full log twice.
|
|
44
|
+
return text if len(text) <= 1600 else text[:780] + '\n[excerpt omitted]\n' + text[-800:]
|
|
45
|
+
|
|
46
|
+
|
|
38
47
|
def title_prompt(user_text: str, reply: str) -> str:
|
|
39
48
|
return INSTRUCTIONS + "\n\n" + json.dumps(
|
|
40
49
|
{"user_message": user_text[:1600], "assistant_reply": reply[:800]},
|
|
@@ -329,8 +338,58 @@ async def generate_cursor_title(prompt: str, *, executable: str, model: str | No
|
|
|
329
338
|
return clean_title(result)
|
|
330
339
|
|
|
331
340
|
|
|
341
|
+
async def generate_gemini_title(prompt: str, *, model: str | None, provider_selection: dict) -> str | None:
|
|
342
|
+
"""One tool-free API request on the exact chat binding; no native session.
|
|
343
|
+
|
|
344
|
+
No CLI, hooks, MCP, filesystem tools, account fallback or model discovery.
|
|
345
|
+
The caller resolves CLI Auto from this conversation's native model metadata.
|
|
346
|
+
"""
|
|
347
|
+
from gemini_api import MODEL, api_base
|
|
348
|
+
|
|
349
|
+
if (not isinstance(model, str) or model == 'auto' or not MODEL.fullmatch(model)
|
|
350
|
+
or provider_selection.get('protocol') != 'gemini'
|
|
351
|
+
or provider_selection.get('auth_header') != 'x-goog-api-key'
|
|
352
|
+
or not provider_selection.get('api_key')):
|
|
353
|
+
raise TitleGenerationError('Gemini title connection/model unavailable')
|
|
354
|
+
try:
|
|
355
|
+
async with httpx.AsyncClient(timeout=TIMEOUT_SECONDS, follow_redirects=False, trust_env=False) as client:
|
|
356
|
+
async with client.stream('POST', api_base(provider_selection) + '/models/' + quote(model, safe='') + ':generateContent',
|
|
357
|
+
headers={'x-goog-api-key': provider_selection['api_key']},
|
|
358
|
+
json={'contents': [{'role': 'user', 'parts': [{'text': prompt}]}],
|
|
359
|
+
'generationConfig': {'maxOutputTokens': 1024, 'candidateCount': 1}}) as response:
|
|
360
|
+
if response.status_code != 200:
|
|
361
|
+
raise TitleGenerationError('Gemini title request failed')
|
|
362
|
+
raw = bytearray()
|
|
363
|
+
async for chunk in response.aiter_bytes(chunk_size=8192):
|
|
364
|
+
raw.extend(chunk)
|
|
365
|
+
if len(raw) > 64 * 1024:
|
|
366
|
+
raise TitleGenerationError('Excessive Gemini title output')
|
|
367
|
+
value = json.loads(raw)
|
|
368
|
+
candidates = value.get('candidates')
|
|
369
|
+
if not isinstance(candidates, list) or len(candidates) != 1 or candidates[0].get('finishReason') != 'STOP':
|
|
370
|
+
raise TitleGenerationError('Gemini title request did not complete')
|
|
371
|
+
parts = candidates[0].get('content', {}).get('parts')
|
|
372
|
+
if not isinstance(parts, list) or not parts or len(parts) > 32:
|
|
373
|
+
raise TitleGenerationError('Invalid Gemini title output')
|
|
374
|
+
text = []
|
|
375
|
+
for part in parts:
|
|
376
|
+
if (not isinstance(part, dict) or not isinstance(part.get('text'), str)
|
|
377
|
+
or set(part) - {'text', 'thought', 'thoughtSignature'}
|
|
378
|
+
or ('thought' in part and not isinstance(part['thought'], bool))):
|
|
379
|
+
raise TitleGenerationError('Unexpected Gemini title content')
|
|
380
|
+
if part.get('thought') is not True:
|
|
381
|
+
text.append(part['text'])
|
|
382
|
+
title = clean_title(''.join(text))
|
|
383
|
+
if title and provider_selection['api_key'] in title:
|
|
384
|
+
return None
|
|
385
|
+
return title
|
|
386
|
+
except (httpx.HTTPError, ValueError, TypeError, AttributeError, KeyError):
|
|
387
|
+
raise TitleGenerationError('Gemini title request failed') from None
|
|
388
|
+
|
|
389
|
+
|
|
332
390
|
async def generate_title(backend: str, user_text: str, reply: str, **options) -> str | None:
|
|
333
|
-
generator = {'codex': generate_codex_title, 'cursor': generate_cursor_title
|
|
391
|
+
generator = {'codex': generate_codex_title, 'cursor': generate_cursor_title,
|
|
392
|
+
'gemini': generate_gemini_title}.get(backend)
|
|
334
393
|
if generator is None:
|
|
335
394
|
return None
|
|
336
395
|
return await asyncio.wait_for(generator(title_prompt(user_text, reply), **options), TIMEOUT_SECONDS)
|