startup_guard.py
1 # Copyright 2026 Alibaba Group Holding Ltd. 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 15 import logging 16 import os 17 import sys 18 import threading 19 20 logger = logging.getLogger(__name__) 21 22 ALLOW_NO_API_KEY_ENV = "OPENSANDBOX_INSECURE_SERVER" 23 ALLOW_NO_API_KEY_CONFIRMATION = "YES" 24 ANSI_RED = "\033[31m" 25 ANSI_RESET = "\033[0m" 26 API_KEY_CONFIRM_TIMEOUT_SECONDS = 30 27 28 29 class _InputResult: 30 def __init__(self) -> None: 31 self.value: str | None = None 32 self.error: BaseException | None = None 33 34 35 def _read_with_timeout(prompt: str, input_func, timeout_seconds: int) -> str: 36 result = _InputResult() 37 38 def _worker() -> None: 39 try: 40 result.value = input_func(prompt) 41 except BaseException as exc: # pragma: no cover 42 result.error = exc 43 44 t = threading.Thread(target=_worker, daemon=True) 45 t.start() 46 t.join(timeout_seconds) 47 48 if t.is_alive(): 49 raise TimeoutError(f"confirmation input timed out after {timeout_seconds} seconds") 50 if result.error is not None: 51 raise result.error 52 return result.value or "" 53 54 55 def api_key_confirm( 56 *, 57 configured_api_key: str | None, 58 stdin=None, 59 environ=None, 60 input_func=input, 61 ) -> None: 62 """ 63 Enforce explicit confirmation before starting without server.api_key. 64 65 Confirmation sources: 66 1) OPENSANDBOX_INSECURE_SERVER=YES (non-interactive safe path) 67 2) Interactive TTY prompt requiring exact input 'YES' 68 """ 69 if configured_api_key and configured_api_key.strip(): 70 return 71 72 env = environ if environ is not None else os.environ 73 74 if env.get(ALLOW_NO_API_KEY_ENV) == ALLOW_NO_API_KEY_CONFIRMATION: 75 logger.warning( 76 "server.api_key is not configured. Proceeding only because %s=%s.", 77 ALLOW_NO_API_KEY_ENV, 78 ALLOW_NO_API_KEY_CONFIRMATION, 79 ) 80 return 81 82 stdin_stream = stdin if stdin is not None else sys.stdin 83 if stdin_stream is not None and hasattr(stdin_stream, "isatty") and stdin_stream.isatty(): 84 try: 85 confirmation = _read_with_timeout( 86 f"{ANSI_RED}" 87 "SECURITY WARNING: server.api_key is empty; API authentication is disabled. " 88 "Type 'YES' to continue startup without API key. " 89 "Strongly recommend setting server.api_key. " 90 "See: https://github.com/alibaba/OpenSandbox/issues/750 " 91 ": " 92 f"{ANSI_RESET}", 93 input_func, 94 API_KEY_CONFIRM_TIMEOUT_SECONDS, 95 ) 96 except TimeoutError as exc: 97 raise RuntimeError( 98 "Startup aborted: confirmation timed out waiting for YES. " 99 "Strongly recommend setting server.api_key. " 100 "See: https://github.com/alibaba/OpenSandbox/issues/750" 101 ) from exc 102 if confirmation == ALLOW_NO_API_KEY_CONFIRMATION: 103 logger.warning( 104 "server.api_key is not configured. Proceeding after interactive confirmation." 105 ) 106 return 107 raise RuntimeError( 108 "Startup aborted: missing explicit confirmation for empty server.api_key. " 109 "Strongly recommend setting server.api_key. " 110 "See: https://github.com/alibaba/OpenSandbox/issues/750" 111 ) 112 113 raise RuntimeError( 114 "Startup blocked: server.api_key is empty in non-interactive mode. " 115 f"Set {ALLOW_NO_API_KEY_ENV}={ALLOW_NO_API_KEY_CONFIRMATION} to acknowledge the risk. " 116 "Strongly recommend setting server.api_key. " 117 "See: https://github.com/alibaba/OpenSandbox/issues/750" 118 )