mirror of
https://github.com/open-webui/open-terminal.git
synced 2026-11-04 08:10:53 +00:00
feat: add TOML configuration file support
Load settings from /etc/open-terminal/config.toml (system) and $XDG_CONFIG_HOME/open-terminal/config.toml (user). Supports host, port, api_key, cors_allowed_origins, log_dir, and binary_mime_prefixes. CLI flags and environment variables still take precedence. A --config flag allows pointing to a custom config file. Closes #11
This commit is contained in:
1 parent
c8d3285abb
commit
f94fa3cec6
6 files changed
+203
-16
No files matched your search
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## [0.6.0] - 2026-03-02
|
||||
|
||||
### Added
|
||||
|
||||
- 📄 **Configuration file support** — settings can now be loaded from TOML config files at /etc/open-terminal/config.toml (system-wide) and $XDG_CONFIG_HOME/open-terminal/config.toml (per-user, defaults to ~/.config/open-terminal/config.toml). Supports host, port, api_key, cors_allowed_origins, log_dir, and binary_mime_prefixes. CLI flags and environment variables still take precedence. Use --config to point to a custom config file. This keeps the API key out of ps / htop output.
|
||||
|
||||
## [0.5.0] - 2026-03-02
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -50,6 +50,36 @@ open-terminal run --host 0.0.0.0 --port 8000 --api-key your-secret-key
|
||||
> On bare metal, commands run directly on your machine with your user's permissions. Use Docker if you want sandboxed execution.
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
Open Terminal can be configured via a TOML config file, environment variables, and CLI flags. Settings are resolved in this order (highest priority wins):
|
||||
|
||||
1. **CLI flags** (`--host`, `--port`, `--api-key`, etc.)
|
||||
2. **Environment variables** (`OPEN_TERMINAL_API_KEY`, etc.)
|
||||
3. **User config** — `$XDG_CONFIG_HOME/open-terminal/config.toml` (defaults to `~/.config/open-terminal/config.toml`)
|
||||
4. **System config** — `/etc/open-terminal/config.toml`
|
||||
5. **Built-in defaults**
|
||||
|
||||
Create a config file at either location with any of these keys (all optional):
|
||||
|
||||
```toml
|
||||
host = "0.0.0.0"
|
||||
port = 8000
|
||||
api_key = "sk-my-secret-key"
|
||||
cors_allowed_origins = "*"
|
||||
log_dir = "/var/log/open-terminal"
|
||||
binary_mime_prefixes = "image,audio"
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> Use the system config at `/etc/open-terminal/config.toml` to set site-wide defaults for host and port, and the user config for personal settings like the API key — this keeps the key out of `ps` / `htop`.
|
||||
|
||||
You can also point to a specific config file:
|
||||
|
||||
```bash
|
||||
open-terminal run --config /path/to/my-config.toml
|
||||
```
|
||||
|
||||
## Using with Open WebUI
|
||||
|
||||
Open Terminal integrates with [Open WebUI](https://github.com/open-webui/open-webui), giving your AI assistants the ability to run commands, manage files, and interact with a terminal right from the AI interface. Make sure to add it under **Open Terminal** in the integrations settings, not as a tool server. Adding it as an Open Terminal connection gives you a built-in file navigation sidebar where you can browse directories, upload, download, and edit files. There are two ways to connect:
|
||||
|
||||
+55
-6
@@ -23,8 +23,15 @@ BANNER = r"""
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option("--host", default="0.0.0.0", help="Bind host")
|
||||
@click.option("--port", default=8000, type=int, help="Bind port")
|
||||
@click.option("--host", default=None, help="Bind host (default: 0.0.0.0)")
|
||||
@click.option("--port", default=None, type=int, help="Bind port (default: 8000)")
|
||||
@click.option(
|
||||
"--config",
|
||||
"config_path",
|
||||
default=None,
|
||||
type=click.Path(exists=True, dir_okay=False, resolve_path=True),
|
||||
help="Path to a TOML config file (overrides user-level config location).",
|
||||
)
|
||||
@click.option(
|
||||
"--cwd",
|
||||
type=click.Path(exists=True, file_okay=False, resolve_path=True, path_type=str),
|
||||
@@ -43,10 +50,26 @@ BANNER = r"""
|
||||
envvar="OPEN_TERMINAL_CORS_ALLOWED_ORIGINS",
|
||||
help="Allowed CORS origins, comma-separated (default: * for all)",
|
||||
)
|
||||
def run(host: str, port: int, cwd: str | None, api_key: str, cors_allowed_origins: str):
|
||||
def run(
|
||||
host: str | None,
|
||||
port: int | None,
|
||||
config_path: str | None,
|
||||
cwd: str | None,
|
||||
api_key: str,
|
||||
cors_allowed_origins: str,
|
||||
):
|
||||
"""Start the sandbox API server."""
|
||||
import secrets
|
||||
|
||||
from open_terminal import config
|
||||
|
||||
# Load config files before resolving other settings.
|
||||
cfg = config.init(config_path)
|
||||
|
||||
# Resolve host/port: CLI flag > config file > built-in default
|
||||
host = host or cfg.get("host", "0.0.0.0")
|
||||
port = port if port is not None else cfg.get("port", 8000)
|
||||
|
||||
if cwd:
|
||||
os.chdir(cwd)
|
||||
|
||||
@@ -57,6 +80,10 @@ def run(host: str, port: int, cwd: str | None, api_key: str, cors_allowed_origin
|
||||
with open(file_path) as f:
|
||||
api_key = f.read().strip()
|
||||
|
||||
# Fall back to config file value
|
||||
if not api_key:
|
||||
api_key = cfg.get("api_key", "")
|
||||
|
||||
generated = not api_key
|
||||
if not api_key:
|
||||
api_key = secrets.token_urlsafe(24)
|
||||
@@ -81,16 +108,38 @@ def run(host: str, port: int, cwd: str | None, api_key: str, cors_allowed_origin
|
||||
type=click.Choice(["stdio", "streamable-http"]),
|
||||
help="MCP transport (default: stdio)",
|
||||
)
|
||||
@click.option("--host", default="0.0.0.0", help="Bind host (streamable-http only)")
|
||||
@click.option("--port", default=8000, type=int, help="Bind port (streamable-http only)")
|
||||
@click.option("--host", default=None, help="Bind host (streamable-http only)")
|
||||
@click.option(
|
||||
"--port", default=None, type=int, help="Bind port (streamable-http only)"
|
||||
)
|
||||
@click.option(
|
||||
"--config",
|
||||
"config_path",
|
||||
default=None,
|
||||
type=click.Path(exists=True, dir_okay=False, resolve_path=True),
|
||||
help="Path to a TOML config file (overrides user-level config location).",
|
||||
)
|
||||
@click.option(
|
||||
"--cwd",
|
||||
type=click.Path(exists=True, file_okay=False, resolve_path=True, path_type=str),
|
||||
default=None,
|
||||
help="Working directory for the server process.",
|
||||
)
|
||||
def mcp(transport: str, host: str, port: int, cwd: str | None):
|
||||
def mcp(
|
||||
transport: str,
|
||||
host: str | None,
|
||||
port: int | None,
|
||||
config_path: str | None,
|
||||
cwd: str | None,
|
||||
):
|
||||
"""Start the MCP server (requires 'pip install open-terminal[mcp]')."""
|
||||
from open_terminal import config
|
||||
|
||||
cfg = config.init(config_path)
|
||||
|
||||
host = host or cfg.get("host", "0.0.0.0")
|
||||
port = port if port is not None else cfg.get("port", 8000)
|
||||
|
||||
if cwd:
|
||||
os.chdir(cwd)
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
"""TOML configuration file support.
|
||||
|
||||
Settings are resolved with this precedence (highest wins):
|
||||
|
||||
1. CLI flags
|
||||
2. Environment variables / Docker-secrets ``_FILE`` variants
|
||||
3. User config — ``$XDG_CONFIG_HOME/open-terminal/config.toml``
|
||||
(defaults to ``~/.config/open-terminal/config.toml``)
|
||||
4. System config — ``/etc/open-terminal/config.toml``
|
||||
5. Built-in defaults
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _default_user_config_path() -> Path:
|
||||
"""Return the XDG-compliant user config file path."""
|
||||
xdg = os.environ.get("XDG_CONFIG_HOME") or os.path.join(
|
||||
os.path.expanduser("~"), ".config"
|
||||
)
|
||||
return Path(xdg) / "open-terminal" / "config.toml"
|
||||
|
||||
|
||||
_SYSTEM_CONFIG_PATH = Path("/etc/open-terminal/config.toml")
|
||||
|
||||
|
||||
def load_config(explicit_path: str | None = None) -> dict:
|
||||
"""Load and merge TOML configuration files.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
explicit_path:
|
||||
If given, this file replaces the *user-level* lookup. The
|
||||
system-level config is still loaded underneath.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
Merged configuration dictionary. System values are overridden
|
||||
by user (or explicit) values.
|
||||
"""
|
||||
merged: dict = {}
|
||||
|
||||
# 1. System config (lowest priority of the two files)
|
||||
if _SYSTEM_CONFIG_PATH.is_file():
|
||||
try:
|
||||
merged.update(tomllib.loads(_SYSTEM_CONFIG_PATH.read_text("utf-8")))
|
||||
except Exception as exc:
|
||||
print(
|
||||
f"Warning: failed to read {_SYSTEM_CONFIG_PATH}: {exc}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
# 2. User / explicit config (overrides system)
|
||||
user_path = Path(explicit_path) if explicit_path else _default_user_config_path()
|
||||
if user_path.is_file():
|
||||
try:
|
||||
merged.update(tomllib.loads(user_path.read_text("utf-8")))
|
||||
except Exception as exc:
|
||||
# If the user explicitly asked for this file, treat errors as fatal.
|
||||
if explicit_path:
|
||||
raise SystemExit(f"Error: failed to read {user_path}: {exc}") from exc
|
||||
print(
|
||||
f"Warning: failed to read {user_path}: {exc}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
return merged
|
||||
|
||||
|
||||
# Module-level merged config, lazily populated by ``init()``.
|
||||
_config: dict = {}
|
||||
|
||||
|
||||
def init(explicit_path: str | None = None) -> dict:
|
||||
"""Load config files and cache the result module-wide.
|
||||
|
||||
This should be called once, early in startup (e.g. from the CLI
|
||||
entry-point), *before* ``env.py`` constants are evaluated.
|
||||
"""
|
||||
global _config
|
||||
_config = load_config(explicit_path)
|
||||
return _config
|
||||
|
||||
|
||||
def get(key: str, default=None):
|
||||
"""Look up a value from the loaded config."""
|
||||
return _config.get(key, default)
|
||||
+20
-9
@@ -1,5 +1,7 @@
|
||||
import os
|
||||
|
||||
from open_terminal import config
|
||||
|
||||
|
||||
def _resolve_file_env(var: str, default: str = "") -> str:
|
||||
"""Resolve an environment variable with Docker-secrets ``_FILE`` support.
|
||||
@@ -26,17 +28,23 @@ def _resolve_file_env(var: str, default: str = "") -> str:
|
||||
return value or default
|
||||
|
||||
|
||||
API_KEY = _resolve_file_env("OPEN_TERMINAL_API_KEY")
|
||||
CORS_ALLOWED_ORIGINS = os.environ.get("OPEN_TERMINAL_CORS_ALLOWED_ORIGINS", "*")
|
||||
API_KEY = _resolve_file_env("OPEN_TERMINAL_API_KEY", config.get("api_key", ""))
|
||||
CORS_ALLOWED_ORIGINS = os.environ.get(
|
||||
"OPEN_TERMINAL_CORS_ALLOWED_ORIGINS",
|
||||
config.get("cors_allowed_origins", "*"),
|
||||
)
|
||||
LOG_DIR = os.environ.get(
|
||||
"OPEN_TERMINAL_LOG_DIR",
|
||||
os.path.join(
|
||||
os.environ.get(
|
||||
"XDG_STATE_HOME",
|
||||
os.path.join(os.path.expanduser("~"), ".local", "state"),
|
||||
config.get(
|
||||
"log_dir",
|
||||
os.path.join(
|
||||
os.environ.get(
|
||||
"XDG_STATE_HOME",
|
||||
os.path.join(os.path.expanduser("~"), ".local", "state"),
|
||||
),
|
||||
"open-terminal",
|
||||
"logs",
|
||||
),
|
||||
"open-terminal",
|
||||
"logs",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -44,6 +52,9 @@ LOG_DIR = os.environ.get(
|
||||
# as raw binary responses (e.g. "image,audio" or "image/png,image/jpeg").
|
||||
BINARY_FILE_MIME_PREFIXES = [
|
||||
p.strip()
|
||||
for p in os.environ.get("OPEN_TERMINAL_BINARY_MIME_PREFIXES", "image").split(",")
|
||||
for p in os.environ.get(
|
||||
"OPEN_TERMINAL_BINARY_MIME_PREFIXES",
|
||||
config.get("binary_mime_prefixes", "image"),
|
||||
).split(",")
|
||||
if p.strip()
|
||||
]
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "open-terminal"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
description = "A remote terminal API."
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
|
||||
Reference in new issue
Block a user