FrameBridge 0.3.1 community source release
This commit is contained in:
commit
b23819fff1
1354 files changed
+343716
No files matched your search
@@ -0,0 +1,497 @@
|
||||
"""Manual ChatGPT connection using the packaged, unmodified tunnel client."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import getpass
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shlex
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from typing import Callable
|
||||
from urllib.parse import urlsplit
|
||||
import warnings
|
||||
|
||||
KEY_ENV = "FRAMEBRIDGE_EXPANDED_RUNTIME_KEY"
|
||||
_ID = re.compile(r"tunnel_[A-Za-z0-9_-]{1,128}\Z")
|
||||
_ENV_REF = re.compile(r"env:[A-Za-z_][A-Za-z0-9_]*\Z")
|
||||
_RUNTIME_PREFIXES = ("TUNNEL_CLIENT_", "CONTROL_PLANE_", "OPENAI_", "MCP_", "HEALTH_", "LOG_", "CLOUDFLARED_", "HARPOON_")
|
||||
_RUNTIME_NAMES = {"TUNNEL_ID", "TUNNEL_BIN", "MCP_URL", "CRED_FILE", "OPEN_WEB_UI", "PID_FILE", "ALLOW_REMOTE_UI", "ADMIN_UI_LOG_BUFFER_EVENTS", "PROXY_CHECK_INTERVAL", KEY_ENV}
|
||||
|
||||
|
||||
class TunnelInputError(ValueError):
|
||||
"""A safe error that never includes the pasted command or credential."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TunnelSpec:
|
||||
tunnel_id: str
|
||||
organization_id: str = ""
|
||||
url_path: str = ""
|
||||
|
||||
def __post_init__(self):
|
||||
if not isinstance(self.tunnel_id, str) or not _ID.fullmatch(self.tunnel_id):
|
||||
raise TunnelInputError("A valid tunnel ID is required.")
|
||||
if not isinstance(self.organization_id, str) or (self.organization_id and not re.fullmatch(r"[A-Za-z0-9_-]{1,128}", self.organization_id)):
|
||||
raise TunnelInputError("The organization identifier is invalid.")
|
||||
if not isinstance(self.url_path, str) or (self.url_path and (not self.url_path.startswith("/") or self.url_path.startswith("//") or any(c in self.url_path for c in ("?", "#", "\\", "..")))):
|
||||
raise TunnelInputError("The control-plane path is invalid.")
|
||||
|
||||
def public_settings(self):
|
||||
return {"tunnel_id": self.tunnel_id,
|
||||
"control_plane_organization_id": self.organization_id,
|
||||
"control_plane_url_path": self.url_path}
|
||||
|
||||
|
||||
def parse_tunnel_input(text: str) -> TunnelSpec:
|
||||
"""Extract public setup data; never execute or retain the pasted command."""
|
||||
if not isinstance(text, str) or len(text) > 16384:
|
||||
raise TunnelInputError("Paste a tunnel ID or a supported tunnel-client command.")
|
||||
raw = text.strip()
|
||||
if any(c in raw for c in ("\n", "\r", "\x00", ";", "|", "&", "<", ">", "`", "$(", "${", "#")):
|
||||
raise TunnelInputError("Shell expressions are not accepted. Paste only the tunnel ID or one tunnel-client command.")
|
||||
try:
|
||||
words = shlex.split(raw, posix=True)
|
||||
except ValueError:
|
||||
raise TunnelInputError("The pasted command has incomplete quoting.") from None
|
||||
if len(words) == 1 and _ID.fullmatch(words[0]):
|
||||
return TunnelSpec(words[0])
|
||||
if not words:
|
||||
raise TunnelInputError("A tunnel ID is required.")
|
||||
program = Path(words.pop(0)).name
|
||||
if program in {"framebridge-original", "framebridge", "fb"}:
|
||||
if not words or words.pop(0) != "tunnel":
|
||||
raise TunnelInputError("Paste a tunnel-client command or only its tunnel ID.")
|
||||
if words[:1] == ["--"]:
|
||||
words.pop(0)
|
||||
elif program != "tunnel-client":
|
||||
raise TunnelInputError("Only a tunnel-client command or a tunnel ID is accepted.")
|
||||
if words[:2] == ["runtimes", "connect"]:
|
||||
mode, words = "connect", words[2:]
|
||||
elif words[:1] in (["run"], ["init"]):
|
||||
mode, words = words[0], words[1:]
|
||||
else:
|
||||
raise TunnelInputError("Use a run, init, or runtimes connect command containing an explicit tunnel ID.")
|
||||
options = {
|
||||
"--control-plane.tunnel-id": "id", "--tunnel-id": "id",
|
||||
"--control-plane.organization-id": "org", "--organization-id": "org",
|
||||
"--control-plane.base-url": "base", "--control-plane-base-url": "base",
|
||||
"--control-plane.url-path": "path", "--control-plane-url-path": "path",
|
||||
"--control-plane.api-key": "credential", "--runtime-api-key": "credential",
|
||||
"--control-plane-api-key-ref": "credential",
|
||||
"--mcp.server-url": "mcp", "--mcp-server-url": "mcp",
|
||||
"--health.listen-addr": "health", "--health-listen-addr": "health",
|
||||
"--health.url-file": "health_file",
|
||||
"--alias": "alias", "--name": "name", "--description": "description",
|
||||
"--profile": "profile", "--profile-dir": "profile_dir", "--sample": "sample",
|
||||
}
|
||||
found = {}
|
||||
while words:
|
||||
flag = words.pop(0)
|
||||
name, equals, value = flag.partition("=")
|
||||
if name == "--json" and mode == "connect" and not equals:
|
||||
continue
|
||||
kind = options.get(name)
|
||||
if kind is None:
|
||||
raise TunnelInputError("That command contains an unsupported option. Paste only its tunnel ID instead.")
|
||||
if not equals:
|
||||
if not words or words[0].startswith("--"):
|
||||
raise TunnelInputError("The pasted command has an option without a value.")
|
||||
value = words.pop(0)
|
||||
if not value or kind in found:
|
||||
raise TunnelInputError("The pasted command contains missing or repeated setup values.")
|
||||
found[kind] = value
|
||||
identifier = found.get("id", "")
|
||||
if not _ID.fullmatch(identifier):
|
||||
raise TunnelInputError("The command must contain one explicit tunnel ID.")
|
||||
if "credential" in found and not _ENV_REF.fullmatch(found["credential"]):
|
||||
raise TunnelInputError("Do not include a literal key or a key file. Enter the runtime key at the separate hidden prompt.")
|
||||
if "base" in found:
|
||||
u = urlsplit(found["base"])
|
||||
if u.scheme != "https" or u.netloc != "api.openai.com" or u.path not in ("", "/") or u.query or u.fragment:
|
||||
raise TunnelInputError("Use the OpenAI control plane. Paste only the tunnel ID if the command uses another endpoint.")
|
||||
if "mcp" in found:
|
||||
hint = found["mcp"]
|
||||
if hint.startswith("url="):
|
||||
hint = hint[4:].split(",", 1)[0]
|
||||
u = urlsplit(hint)
|
||||
if u.scheme not in {"http", "https"} or not u.hostname or u.username or u.password or u.query or u.fragment:
|
||||
raise TunnelInputError("The copied MCP URL cannot contain credentials, a query, or a fragment.")
|
||||
org = found.get("org", "")
|
||||
if org and not re.fullmatch(r"[A-Za-z0-9_-]{1,128}", org):
|
||||
raise TunnelInputError("The organization identifier is invalid.")
|
||||
path = found.get("path", "")
|
||||
if path and (not path.startswith("/") or path.startswith("//") or any(c in path for c in ("?", "#", "\\", ".."))):
|
||||
raise TunnelInputError("The control-plane path must be a plain absolute URL path.")
|
||||
return TunnelSpec(identifier, org, path)
|
||||
|
||||
|
||||
def child_environment(secret: str, base=None, *, state=None):
|
||||
"""Return a new child environment; never change os.environ."""
|
||||
env = dict(os.environ if base is None else base)
|
||||
for key in list(env):
|
||||
if key.startswith(_RUNTIME_PREFIXES) or key in _RUNTIME_NAMES:
|
||||
env.pop(key, None)
|
||||
if state is not None:
|
||||
from .config import private_dir
|
||||
home = private_dir(Path(state) / "chatgpt-home")
|
||||
for name in (".config", ".cache", ".local/state", ".run", "tmp"):
|
||||
private_dir(home / name)
|
||||
env.update({"HOME": str(home), "XDG_CONFIG_HOME": str(home / ".config"),
|
||||
"XDG_CACHE_HOME": str(home / ".cache"), "XDG_STATE_HOME": str(home / ".local/state"),
|
||||
"XDG_RUNTIME_DIR": str(home / ".run"), "TMPDIR": str(home / "tmp")})
|
||||
env[KEY_ENV] = secret
|
||||
return env
|
||||
|
||||
|
||||
def build_command(binary, state, mcp_url, spec: TunnelSpec):
|
||||
u = urlsplit(mcp_url)
|
||||
if u.scheme != "http" or u.hostname not in {"localhost", "127.0.0.1", "::1"} or u.username or u.password or u.query or u.fragment:
|
||||
raise TunnelInputError("The application MCP endpoint must be local HTTP without credentials.")
|
||||
args = [str(binary), "run",
|
||||
"--control-plane.base-url", "https://api.openai.com",
|
||||
"--control-plane.tunnel-id", spec.tunnel_id,
|
||||
"--control-plane.api-key", "env:" + KEY_ENV,
|
||||
"--mcp.server-url", "url=" + mcp_url,
|
||||
"--health.listen-addr", "127.0.0.1:0",
|
||||
"--log.level", "info"]
|
||||
if spec.organization_id:
|
||||
args.extend(["--control-plane.organization-id", spec.organization_id])
|
||||
if spec.url_path:
|
||||
args.extend(["--control-plane.url-path", spec.url_path])
|
||||
return args
|
||||
|
||||
|
||||
def _stop_owned(process):
|
||||
if process.poll() is not None:
|
||||
return
|
||||
for sig, seconds in ((signal.SIGINT, 5), (signal.SIGTERM, 2), (signal.SIGKILL, 2)):
|
||||
try:
|
||||
os.killpg(process.pid, sig)
|
||||
except ProcessLookupError:
|
||||
return
|
||||
try:
|
||||
process.wait(timeout=seconds)
|
||||
return
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
|
||||
|
||||
def run_foreground(command, secret, *, state=None, output=None, popen=None, observe=None):
|
||||
"""Keep the owned client in the foreground, with private logs and status."""
|
||||
from . import connection_monitor as monitor
|
||||
output = output or sys.stdout
|
||||
popen = popen or subprocess.Popen
|
||||
progress = monitor.Progress(output)
|
||||
process = reader = log = attempt = None
|
||||
reader_errors = []
|
||||
stop_reader = threading.Event()
|
||||
env = {}
|
||||
previous = {}
|
||||
ready_once = False
|
||||
last_detail = None
|
||||
def interrupt(signum, frame):
|
||||
raise KeyboardInterrupt
|
||||
try:
|
||||
if state is None:
|
||||
raise monitor.ConnectionMonitorError("A private profile is required for connection logs.")
|
||||
log = monitor.PrivateLog(state, secret)
|
||||
attempt = monitor.Attempt(state)
|
||||
args = list(command)
|
||||
# Never reuse an endpoint file from an earlier connection attempt.
|
||||
while "--health.url-file" in args:
|
||||
index = args.index("--health.url-file")
|
||||
del args[index:index + 2]
|
||||
args.extend(["--health.url-file", str(attempt.health_file)])
|
||||
expected_tunnel = args[args.index("--control-plane.tunnel-id") + 1]
|
||||
expected_mcp = args[args.index("--mcp.server-url") + 1]
|
||||
if expected_mcp.startswith("url="):
|
||||
expected_mcp = expected_mcp[4:]
|
||||
env = child_environment(secret, state=state)
|
||||
for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP):
|
||||
previous[sig] = signal.signal(sig, interrupt)
|
||||
progress.change("starting", "[1/3] Starting the secure tunnel client.")
|
||||
output.write("Private connection log: " + str(log.path) + "\n")
|
||||
output.flush()
|
||||
started = time.time()
|
||||
process = popen(args, env=env, stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
start_new_session=True, bufsize=0, umask=0o077)
|
||||
env.pop(KEY_ENV, None)
|
||||
log.write("Connection attempt started; tunnel=" + expected_tunnel + "; child_pid=" + str(process.pid))
|
||||
reader = threading.Thread(target=monitor.capture_output,
|
||||
args=(process.stdout, log, reader_errors, stop_reader), daemon=True)
|
||||
reader.start()
|
||||
next_probe = 0.0
|
||||
waiting_since = time.monotonic()
|
||||
instructions_shown = False
|
||||
while True:
|
||||
rc = process.poll()
|
||||
if rc is not None:
|
||||
log.write("Tunnel client exited; code=" + str(rc))
|
||||
if not ready_once:
|
||||
progress.change("error", "[ERROR] The tunnel stopped before it was ready. Check the private connection log.")
|
||||
return rc if rc > 0 else 1
|
||||
progress.change("closed" if rc == 0 else "error",
|
||||
"[CLOSED] The tunnel client stopped." if rc == 0 else
|
||||
"[ERROR] The tunnel connection stopped unexpectedly. Check the private connection log.")
|
||||
return rc if rc >= 0 else 1
|
||||
if reader_errors:
|
||||
progress.change("error", "[ERROR] Private connection logging failed. The tunnel will be stopped.")
|
||||
return 1
|
||||
if time.monotonic() >= next_probe:
|
||||
status, detail = monitor.probe(attempt.health_file, expected_tunnel, expected_mcp, started)
|
||||
if observe is not None:
|
||||
observe(status)
|
||||
next_probe = time.monotonic() + 1.0
|
||||
if detail != last_detail:
|
||||
log.write("Connection check: " + detail)
|
||||
last_detail = detail
|
||||
if process.poll() is not None:
|
||||
continue
|
||||
messages = {
|
||||
"starting": "[1/3] Waiting for the tunnel client to start.",
|
||||
"checking": "[2/3] Verifying the local MCP service.",
|
||||
"access": "[3/3] Verifying tunnel access and authenticated polling.",
|
||||
"ready": "[OK] Secure tunnel connected; local MCP service verified.",
|
||||
"rejected": "[ERROR] Tunnel access was rejected. Press Ctrl+C and check the runtime key and tunnel permissions.",
|
||||
}
|
||||
if status == "ready":
|
||||
ready_once = True
|
||||
progress.change(status, messages[status])
|
||||
if not instructions_shown:
|
||||
output.write("Complete or select this tunnel in ChatGPT. A successful tool call verifies that connection.\n")
|
||||
output.write("Keep this terminal open. Press Ctrl+C to disconnect.\n")
|
||||
output.flush()
|
||||
instructions_shown = True
|
||||
elif ready_once:
|
||||
progress.change("retrying", "[RETRYING] Tunnel readiness was lost. Waiting for verified recovery; see the private log.")
|
||||
elif status == "rejected":
|
||||
progress.change(status, messages[status])
|
||||
elif time.monotonic() - waiting_since >= 90:
|
||||
progress.change("unverified", "[NOT READY] The connection is not verified yet. The client is retrying; check the private log or press Ctrl+C.")
|
||||
else:
|
||||
progress.change(status, messages[status])
|
||||
progress.tick()
|
||||
time.sleep(0.2)
|
||||
except KeyboardInterrupt:
|
||||
progress.change("closed", "[STOPPED] Disconnecting this tunnel client.")
|
||||
return 130
|
||||
except Exception as error:
|
||||
if log is not None:
|
||||
try:
|
||||
log.write("Connection wrapper failed; type=" + type(error).__name__ + "; detail=" + str(error))
|
||||
except Exception:
|
||||
pass
|
||||
progress.change("error", "[ERROR] The connection could not be started or monitored. Check the private connection log and profile permissions.")
|
||||
return 1
|
||||
finally:
|
||||
cleanup_failed = False
|
||||
for sig in previous:
|
||||
try:
|
||||
signal.signal(sig, signal.SIG_IGN)
|
||||
except Exception:
|
||||
cleanup_failed = True
|
||||
try:
|
||||
if process is not None:
|
||||
try:
|
||||
_stop_owned(process)
|
||||
if process.poll() is None:
|
||||
cleanup_failed = True
|
||||
except Exception:
|
||||
cleanup_failed = True
|
||||
finally:
|
||||
stop_reader.set()
|
||||
if reader is not None:
|
||||
reader.join(timeout=3)
|
||||
reader_finished = reader is None or not reader.is_alive()
|
||||
if not reader_finished or reader_errors:
|
||||
cleanup_failed = True
|
||||
try:
|
||||
if process is not None and process.stdout is not None and reader_finished:
|
||||
process.stdout.close()
|
||||
except Exception:
|
||||
cleanup_failed = True
|
||||
try:
|
||||
progress.clear()
|
||||
if attempt is not None:
|
||||
attempt.close()
|
||||
except Exception:
|
||||
cleanup_failed = True
|
||||
finally:
|
||||
try:
|
||||
# Never close descriptors out from under an active writer.
|
||||
if log is not None and reader_finished:
|
||||
log.close()
|
||||
except Exception:
|
||||
cleanup_failed = True
|
||||
finally:
|
||||
env.pop(KEY_ENV, None)
|
||||
secret = None
|
||||
for sig, handler in previous.items():
|
||||
try:
|
||||
signal.signal(sig, handler)
|
||||
except Exception:
|
||||
cleanup_failed = True
|
||||
if cleanup_failed:
|
||||
progress.change("error", "[ERROR] Connection cleanup could not be fully verified. Check this client process before reconnecting.")
|
||||
return 1
|
||||
|
||||
|
||||
def _hidden(prompt):
|
||||
if not sys.stdin.isatty():
|
||||
raise TunnelInputError("Use an interactive terminal for the hidden setup prompts.")
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error", getpass.GetPassWarning)
|
||||
try:
|
||||
return getpass.getpass(prompt)
|
||||
except getpass.GetPassWarning:
|
||||
raise TunnelInputError("A terminal with hidden input is required.") from None
|
||||
|
||||
|
||||
def _public_answer(label, saved=""):
|
||||
suffix = " [" + saved + "]" if saved else ""
|
||||
return input(label + suffix + ": ").strip() or saved
|
||||
|
||||
|
||||
def _wait_managed_connection(state, current, output, *, wait_timeout=95):
|
||||
from . import autostart
|
||||
from .connection_monitor import Progress
|
||||
progress = Progress(output)
|
||||
deadline = time.monotonic() + max(0, wait_timeout)
|
||||
try:
|
||||
while True:
|
||||
if current.get("interrupted_after_commit"):
|
||||
progress.change("closed", "Connection continues in the background.")
|
||||
return 130
|
||||
connection = current.get("connection", {})
|
||||
local = current.get("runtime", {})
|
||||
if (current.get("configured") and current.get("enabled")
|
||||
and current.get("service_active") and current.get("service_state") == "active"
|
||||
and current.get("worker", {}).get("state") != "failed"
|
||||
and local.get("running") and local.get("ready")
|
||||
and connection.get("alive") and connection.get("verified")):
|
||||
progress.change("ready", "[OK] Connected.")
|
||||
return 0
|
||||
if (current.get("service_state") == "failed"
|
||||
or current.get("worker", {}).get("state") == "failed"
|
||||
or connection.get("state") in {"rejected", "error"}):
|
||||
progress.change("error", "[ERROR] Connection failed. Run fb connection status for details.")
|
||||
return 1
|
||||
if not current.get("configured") or not current.get("enabled"):
|
||||
progress.change("error", "[ERROR] Connection setup could not be completed.")
|
||||
return 1
|
||||
if time.monotonic() >= deadline:
|
||||
progress.change("unverified", "[PENDING] Connection is still being established in the background.")
|
||||
return 1
|
||||
progress.tick()
|
||||
time.sleep(0.2)
|
||||
current = autostart.status(state)
|
||||
finally:
|
||||
progress.clear()
|
||||
|
||||
|
||||
def resume_managed_connection(state, resume_connection, *, output=None, wait_timeout=95):
|
||||
output = output or sys.stdout
|
||||
resumed = False
|
||||
try:
|
||||
output.write("Connecting...\n")
|
||||
output.flush()
|
||||
current = resume_connection(state)
|
||||
resumed = bool(current.get("configured") and current.get("enabled"))
|
||||
return _wait_managed_connection(state, current, output, wait_timeout=wait_timeout)
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
output.write("\nConnection continues in the background.\n" if resumed else "\nConnection cancelled.\n")
|
||||
return 130
|
||||
|
||||
|
||||
def _connect_managed(state, public_settings, configure_connection, output, *, wait_timeout=95):
|
||||
if not sys.stdin.isatty():
|
||||
raise TunnelInputError("Use an interactive terminal for connection setup.")
|
||||
runtime_key = None
|
||||
committed = False
|
||||
try:
|
||||
saved_label = public_settings.get("account_label", "")
|
||||
if saved_label and not re.fullmatch(r"[^\s@]+@[^\s@]+\.[^\s@]+", saved_label):
|
||||
saved_label = ""
|
||||
label = _public_answer("ChatGPT account (email label)", saved_label)
|
||||
if label and not re.fullmatch(r"[^\s@]+@[^\s@]+\.[^\s@]+", label):
|
||||
raise TunnelInputError("Enter an email label or leave it empty.")
|
||||
saved_id = public_settings.get("tunnel_id", "")
|
||||
if not _ID.fullmatch(saved_id):
|
||||
saved_id = ""
|
||||
raw = _hidden("Tunnel ID (hidden; Enter reuses saved ID): ")
|
||||
tunnel_id = raw.strip() or saved_id
|
||||
if not _ID.fullmatch(tunnel_id):
|
||||
raise TunnelInputError("Enter only a valid tunnel ID beginning with tunnel_.")
|
||||
spec = TunnelSpec(tunnel_id)
|
||||
reuse_saved = tunnel_id == saved_id
|
||||
raw = None
|
||||
if reuse_saved:
|
||||
spec = TunnelSpec(spec.tunnel_id, public_settings.get("control_plane_organization_id", ""), public_settings.get("control_plane_url_path", ""))
|
||||
runtime_key = _hidden("MCP server access key (hidden): ")
|
||||
if (not runtime_key or not runtime_key.isascii() or len(runtime_key.encode()) > 16384
|
||||
or any(ord(ch) < 33 or ord(ch) == 127 or ch.isspace() for ch in runtime_key)):
|
||||
raise TunnelInputError("Enter a single nonempty MCP server access key.")
|
||||
settings = dict(spec.public_settings(), account_label=label)
|
||||
output.write("Connecting...\n")
|
||||
output.flush()
|
||||
current = configure_connection(state, settings, runtime_key)
|
||||
runtime_key = None
|
||||
committed = bool(current.get("configured") and current.get("enabled"))
|
||||
return _wait_managed_connection(state, current, output, wait_timeout=wait_timeout)
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
output.write("\nConnection continues in the background.\n" if committed else "\nConnection cancelled.\n")
|
||||
return 130
|
||||
finally:
|
||||
runtime_key = None
|
||||
|
||||
|
||||
def connect_chatgpt(binary, state, mcp_url, public_settings=None,
|
||||
save_public: Callable | None = None, *, output=None, foreground=False,
|
||||
configure_connection: Callable | None = None, wait_timeout=95):
|
||||
"""Connect with one hidden key; normal setup uses protected Linux services."""
|
||||
output = output or sys.stdout
|
||||
public_settings = dict(public_settings or {})
|
||||
if configure_connection is not None and not foreground:
|
||||
return _connect_managed(state, public_settings, configure_connection, output, wait_timeout=wait_timeout)
|
||||
from . import connection_background
|
||||
current = connection_background.status(state)
|
||||
if current["alive"]:
|
||||
output.write("This profile already has a background ChatGPT connection. Use framebridge connection status.\n")
|
||||
return 0 if current["verified"] else 1
|
||||
label = public_settings.get("account_label", "")
|
||||
output.write("ChatGPT login happens in your browser. The saved email is only an account label.\n")
|
||||
if label:
|
||||
output.write("An account label is saved for this profile.\n")
|
||||
output.write("Create or select your tunnel manually in OpenAI Platform.\n")
|
||||
output.write("The runtime key needs Tunnels Read + Use. No Admin API key is needed here.\n")
|
||||
output.write("The command is parsed as data; this profile's local MCP server will be used.\n")
|
||||
secret = None
|
||||
try:
|
||||
raw = _hidden("Tunnel ID or full tunnel-client command (hidden; Enter reuses the saved ID): ")
|
||||
reuse_saved = not raw.strip()
|
||||
if reuse_saved:
|
||||
raw = public_settings.get("tunnel_id", "")
|
||||
spec = parse_tunnel_input(raw)
|
||||
if reuse_saved:
|
||||
spec = TunnelSpec(spec.tunnel_id, public_settings.get("control_plane_organization_id", ""), public_settings.get("control_plane_url_path", ""))
|
||||
raw = None
|
||||
if save_public:
|
||||
save_public(spec.public_settings())
|
||||
secret = _hidden("Runtime API key (hidden; kept in memory for this connection): ").strip()
|
||||
if not secret or any(c.isspace() for c in secret) or "\x00" in secret:
|
||||
raise TunnelInputError("Enter a single nonempty runtime key.")
|
||||
command = build_command(binary, state, mcp_url, spec)
|
||||
if foreground:
|
||||
return connection_background.run_foreground(command, secret, state, output=output)
|
||||
return connection_background.start(command, secret, state, output=output)
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
output.write("\nConnection cancelled.\n")
|
||||
return 130
|
||||
finally:
|
||||
secret = None
|
||||
Reference in new issue
Block a user