- Increase auto-generated API key entropy from 192-bit to 384-bit
- Use hmac.compare_digest() for constant-time key comparison (HTTP + WS)
- Refuse to start without OPEN_TERMINAL_API_KEY configured
- Add prominent CORS wildcard warning at startup
- Bump version to 0.11.30
_chown() transferred file ownership to the provisioned user but didn't
set group-write permission, leaving files at 644. The server process
(a group member) could create new files but couldn't overwrite them on
subsequent saves, returning PermissionError. Now sets chmod g+w after
chown, matching the 2770 treatment already applied to directories.
Fixes#93
Add a single POST /files/archive endpoint that bundles one or more
files and directories into a ZIP archive for download.
- Accepts a list of paths (files and/or directories)
- Directories are recursively included with relative paths preserved
- Multi-user access control enforced via UserFS
- Cross-platform ZIP format (Windows, macOS, Linux)
- Archive name derived from input (single item = item name, multiple = download.zip)
Closes#92
- GET /system returns a structured system prompt grounding the LLM in
the environment (OS, hostname, user, shell, Python version) with
directives for tool usage
- Gated by OPEN_TERMINAL_ENABLE_SYSTEM_PROMPT env var (default: true)
- Advertised via features.system in GET /api/config for consumer discovery
- OPEN_TERMINAL_SYSTEM_PROMPT env var for full prompt override
- Endpoint excluded from OpenAPI schema, auth-protected
Moved all document text extraction logic from read_file() into a
dedicated utils/documents.py module with an EXTRACTORS registry.
The read_file endpoint now iterates the registry in a 15-line loop
instead of inlining ~330 lines of per-format parsing.
Adding a new format requires only a single extract_* function and
a one-line registry entry. No behaviour changes.
Extended document text extraction to seven more formats:
- .rtf: strips formatting to plain text (striprtf, BSD)
- .xls: legacy Excel, renders sheets as tab-separated values (xlrd, BSD)
- .odt: OpenDocument text, extracts paragraphs (stdlib + lxml, BSD)
- .ods: OpenDocument spreadsheet, extracts all sheets (stdlib + lxml, BSD)
- .odp: OpenDocument presentation, extracts slide text (stdlib + lxml, BSD)
- .epub: extracts body text in spine reading order (stdlib + lxml, BSD)
- .eml: extracts headers and body with HTML stripping (stdlib + lxml, BSD)
All support start_line/end_line range selection.
Office documents are now automatically converted to text and returned
in the standard JSON format, making them readable by LLMs.
- .docx: extracts paragraphs and table contents (python-docx, MIT)
- .xlsx: renders all sheets as tab-separated values (openpyxl, MIT)
- .pptx: extracts text from all slides (python-pptx, MIT)
Supports start_line/end_line range selection, consistent with the
existing PDF handler.
The _FILE mutual-exclusivity guard silently passed when the plain env
var was set to an empty string, because empty strings are falsy.
Python: changed `if value and file_path` to
`if value is not None and file_path is not None`, and the fallback
return from `value or default` to
`value if value is not None else default`.
entrypoint.sh: changed ${!var:-} to ${!var+set} to test whether the
variable is defined at all, not just non-empty.
entrypoint-slim.sh: changed -n tests to ${var+set} via eval, matching
the bash entrypoint semantics.
list_ports triggered full user provisioning (useradd) just to filter
ports by UID. On runtimes that reject useradd (e.g. Azure Container
Apps), this crashed with an unhandled CalledProcessError.
Now catches provisioning failures and returns an empty port list —
an unprovisioned user has no listening ports to show.
setcap cap_setgid+ep on the system Python binary made all Python
processes non-dumpable, blocking /proc/[pid]/fd/ access needed by
_pid_from_inode() to resolve socket inodes to PIDs.
Fix: copy the Python binary to python3-ot and setcap only the copy.
The open-terminal server uses python3-ot (has CAP_SETGID for
multi-user os.setgroups()), while user-spawned python3 stays
capability-free and dumpable.
Slim/Alpine: removed setcap entirely (multi-user mode requires sudo,
which only the full image has). Kept libcap packages installed.
README: corrected Image Variants table — multi-user mode is
full-image only.
ensure_os_user() no longer unconditionally requires sudo; when the
process is already root (e.g. user: '0:0' in Docker Compose), user
provisioning commands run directly. check_environment() now only
requires sudo when not running as root.
Directories created by run_command (as the provisioned user via sudo -u)
get default 755 permissions, leaving the server process without group-write.
Both makedirs (creating subdirs) and aiofiles.open (creating files) fail
with EPERM.
UserFS._ensure_parents now:
- Creates directories as the provisioned user (sudo -u mkdir -p) to bypass
755 restrictions on existing parent directories
- Sets 2770 (setgid + group rwx) on all intermediate directories up to the
user's home, matching the home directory's permissions
- Falls through to plain makedirs in single-user mode (no behavior change)
Closes#70
Add OPEN_TERMINAL_LOG_FLUSH_INTERVAL and OPEN_TERMINAL_LOG_FLUSH_BUFFER
env vars to control how frequently process output is flushed to disk.
Default 0 preserves existing per-chunk flush behaviour.
Setting FLUSH_INTERVAL=1 reduces fsyncs from ~250/sec to ~1/sec for
high-output commands, preventing I/O storms that can make ARM/eMMC
systems unresponsive.
- Centralize flush control in BoundedLogWriter
- Remove per-chunk flush() from PtyRunner, PipeRunner, WinPtyRunner
- Add explicit final flush before writing process end marker
- Bump version to 0.11.18
When OPEN_TERMINAL_MULTI_USER=true, OPEN_TERMINAL_PIP_PACKAGES now uses
sudo pip install to install to the system-wide site-packages directory
instead of /home/user/.local/. This ensures all provisioned users share
the same packages without needing to reinstall individually.
Closes#68
The experimental url query parameter was never used by any consumer.
Open WebUI uses multipart file uploads exclusively. The endpoint now
only accepts direct file uploads, and the file parameter is required
instead of optional.
- Display Local and Network URLs at startup (Vite-style)
- Auto-detect LAN IP when binding to 0.0.0.0
- Show generated API key inline with bold label
- Print yellow warning when listening on all interfaces
- Bump to 0.11.15
- get_system_info() no longer includes 'as user user' when MULTI_USER is
active, removing the hint that caused smaller LLMs to hardcode /home/user
- resolve_path() now rewrites /home/usr → actual user home, matching the
existing /home/user rewrite
Closes#57
Per Docker best practices, security patching should come from rebuilding
with updated base image tags, not from apt-get upgrade inside the
Dockerfile. Pinning to a specific patch version ensures reproducible
builds. Bump the version and rebuild to pick up new patches.
Applies all available security patches at build time. Fixes ~14 CVEs
with upstream patches available (ruby, git, node). The remaining ~194
unfixable HIGHs are in Debian base packages (linux-libc-dev, imagemagick,
systemd, vim) with no upstream fix yet.
Add OPEN_TERMINAL_ALLOWED_DOMAINS env var for domain-level network
restriction. Uses dnsmasq as a local DNS resolver (NXDOMAIN for
non-whitelisted domains), ipset for dynamic IP tracking (dnsmasq
auto-populates resolved IPs), iptables to block external DNS and
non-whitelisted IPs, and capsh to permanently drop CAP_NET_ADMIN.
Supports wildcards (*.github.com), live DNS, fails closed.
Set to empty string to block all; omit for full access.
Gracefully skips on bare metal if iptables is unavailable.
Bump version 0.11.11 → 0.11.12
- Add log rotation: when a process log exceeds MAX_PROCESS_LOG_SIZE (default
50 MB), the oldest half is discarded and writing continues so the most
recent output is always available. Prevents unbounded memory growth that
caused ~26 GB OOM kills.
- Add tail-optimized log reads: _read_log with tail parameter now reads from
the end of the file instead of loading the entire file into memory.
- Add OPEN_TERMINAL_MAX_LOG_SIZE (default 50 MB) and
OPEN_TERMINAL_LOG_RETENTION (default 7 days) configuration.
- Use timestamp-prefixed process IDs (YYYYMMDD-HHMMSS-<random>) so log files
sort chronologically in the filesystem.
- Extract log management into utils/log.py to reduce main.py size.
Added is_path_allowed() to UserFS and wired it into all directory
traversal code paths. In multi-user mode, entries belonging to other
users' home directories are now filtered out during os.walk/os.listdir,
preventing file listing leaks when searching parent directories like
/home. Single-user mode is unaffected (early return on username=None).
- Add resolve_path() to UserFS that resolves relative paths against
self.home instead of os.getcwd()
- Replace all os.path.abspath() calls in route handlers with
fs.resolve_path()
- Auto-swap /home/user → provisioned user home for hardcoded paths
- Add fs dependency to grep_search and glob_search endpoints
- Wrap multi-user terminals with 'script -qc' for proper PTY
- chown home dir after useradd for stale ownership from prior runs
- README: accurate multi-user description with production warning
- Bump version to 0.11.5
When a home directory pre-exists from a previous container run with
different UID assignments, useradd -m doesn't fix ownership. Adding
an explicit chown ensures the dir always belongs to the correct user.
Also adds per-user port visibility filtering — /ports endpoint now
filters by socket UID in multi-user mode so each user only sees
their own listening ports.
Replace all subprocess-based writes (sudo tee, mkdir -p, rm -rf, mv)
with native Python I/O. The only subprocess left is 'sudo chown' for
ownership fixup after writes.
- chmod 2770 (setgid + group rwx) on home dirs so native writes work
- write/write_bytes: aiofiles.open + chown
- mkdir: os.makedirs + chown
- remove: shutil.rmtree / os.remove (no chown needed)
- move: shutil.move + chown
- UserFS._check_path blocks access to other users' /home/<user>/ paths
- PermissionError → 403 Forbidden (global exception handler)
- Terminal spawn uses 'sudo -i -u' with cwd=fs.home so the shell
starts in the user's own home directory
- System paths (/etc, /usr, etc.) remain accessible to all users