Shared terminal I/O primitives for the undef ecosystem
Project description
undef-terminal
Shared terminal I/O primitives and WebSocket proxy infrastructure for the undef ecosystem.
Highlights: WebSocket ↔ telnet/SSH proxy · hijack/observe control plane · browser role system (viewer/operator/admin) · open/shared input mode · quick-connect ephemeral sessions (GET /app/connect, POST /api/connect) · ShellSessionConnector for in-process shell sessions · JWT auth · 1225+ tests at 100% branch coverage
For Cloudflare Workers deployment, see undef-terminal-cloudflare — a companion package that runs the control plane on Durable Objects with CF Access JWT support.
Installation
pip install undef-terminal
Extras
| Extra | Installs | Required for |
|---|---|---|
[websocket] |
fastapi, websockets |
WsTerminalProxy, create_ws_terminal_router, hijack hub |
[emulator] |
pyte |
TerminalEmulator (screen state tracking) |
[ssh] |
asyncssh |
SSH transport, uterm proxy --transport ssh |
[server] |
fastapi, uvicorn, pyjwt |
uterm-server hosted reference server |
[cli] |
fastapi, uvicorn, websockets |
uterm command-line tool |
[all] |
everything above | Full feature set |
pip install 'undef-terminal[all]'
Quick Start
Serve the built-in terminal UI
Mount the bundled terminal.html + terminal.js frontend into any FastAPI app:
from fastapi import FastAPI
from undef.terminal.fastapi import mount_terminal_ui
app = FastAPI()
mount_terminal_ui(app) # serves UndefTerminal at /terminal
mount_terminal_ui(app, path="/t") # custom path
Browser WebSocket → remote telnet proxy
Accept browser WebSocket connections and proxy them to a remote BBS:
from undef.terminal.fastapi import WsTerminalProxy
proxy = WsTerminalProxy("bbs.example.com", 23)
app.include_router(proxy.create_router("/ws/terminal"))
The browser connects to ws://yourhost/ws/terminal; the proxy opens a raw TCP
connection to the BBS for each session.
In-process session handler
Handle terminal sessions in your own async code:
from undef.terminal.fastapi import create_ws_terminal_router
async def my_handler(reader, writer, ws):
writer.write(b"Welcome!\r\n")
await writer.drain()
async for line in reader:
writer.write(line)
await writer.drain()
app.include_router(create_ws_terminal_router(my_handler))
Hijack Widget
The hijack system lets a human operator observe and take over a worker's terminal session in real time.
Backend — TermHub
from undef.terminal.hijack.hub import TermHub
def resolve_browser_role(ws, worker_id):
user = getattr(ws.state, "user", None)
if getattr(user, "is_admin", False):
return "admin"
if getattr(user, "can_operate_terminals", False):
return "operator"
return "viewer"
hub = TermHub(
on_hijack_changed=lambda worker_id, enabled, owner: print(worker_id, enabled),
resolve_browser_role=resolve_browser_role,
)
app.include_router(hub.create_router())
This adds:
GET /ws/browser/{worker_id}/term— browser observer/hijack WebSocketGET /ws/worker/{worker_id}/term— worker WebSocket- REST endpoints for session management
Browser roles are resolved on the server. The browser WebSocket does not accept
a client-selected role parameter; without a resolver, browser sessions default
to read-only (viewer).
If resolve_browser_role raises an exception, the browser WebSocket is rejected
and closed. Resolver failures do not fall back to viewer.
Frontend — UndefHijack
Embed the hijack control widget in any HTML page:
<div id="hijack-container"></div>
<script src="/static/hijack.js"></script>
<script>
new UndefHijack(document.getElementById('hijack-container'), {
workerId: 'myworker', // connects to /ws/browser/myworker/term
mobileKeys: true, // show collapsible special-key toolbar when hijacked
heartbeatInterval: 5000, // ms between heartbeats while owner
});
</script>
Mount the bundled frontend files via FastAPI's StaticFiles or use
mount_terminal_ui() which includes hijack.html, hijack.js, and hijack.css.
Interactive Example Server
The repo also includes an interactive example server for manual testing:
uv run python scripts/example_server.py
Then open:
http://127.0.0.1:8742/hijack/hijack.html?worker=demo-session
The built-in demo session is a general-purpose interactive worker rather than a static screen. It supports:
- exclusive hijack mode (one browser owns input)
- shared input mode (multiple browsers can type)
- free-form text that appends to a live transcript
- built-in commands:
/help,/mode open,/mode hijack,/clear,/status,/nick <name>,/say <text>,/demo,/reset
The demo page includes mode and reset controls backed by example-only HTTP endpoints:
GET /demo/session/{worker_id}POST /demo/session/{worker_id}/modePOST /demo/session/{worker_id}/reset
These demo endpoints exist only for the example server and are not part of the library's public API.
Reference Server
The repo now also includes a standalone reference server application:
uterm-server --config scripts/uterm-server.example.toml
This is the canonical hosted-app example for the library. It demonstrates:
- named sessions above
TermHub - browser session pages and operator pages
- server-side role resolution and policy
- hosted connectors (
shell,telnet,ssh) - session APIs, mode switching, and optional file-backed recording
Key endpoints:
GET /api/healthGET /api/sessionsGET /app/(operator dashboard)GET /app/session/{session_id}(end-user page)GET /app/operator/{session_id}(operator console)GET /app/connect(quick-connect page)POST /api/connect(create ephemeral session)
The example TOML config in scripts/uterm-server.example.toml
shows the intended reference-implementation structure for server config.
For production JWT deployments, start from
scripts/uterm-server.jwt.example.toml.
Auth Runtime Posture
default_server_config()is intentionally local-friendly and usesauth.mode = "dev".- Production should run
auth.mode = "jwt"with:jwt_issuer,jwt_audiencejwt_public_key_pemorjwt_jwks_urljwt_algorithms(for example["RS256"])worker_bearer_tokenfor hosted runtime worker WebSocket authentication
When auth.mode = "jwt", the server fails fast at startup unless:
worker_bearer_tokenis setjwt_algorithmsis non-empty- at least one key source is configured (
jwt_public_key_pemorjwt_jwks_url)
JWT Deployment Runbook
- Configure JWT trust:
- set
jwt_issuer,jwt_audience,jwt_algorithms - prefer
jwt_jwks_urlfor key rotation without restarts
- set
- Configure runtime worker auth:
- mint a dedicated service JWT (admin role) for
worker_bearer_token - scope/TTL this token to server runtime usage only
- mint a dedicated service JWT (admin role) for
- Set session ownership/visibility:
- use
owner+visibilityto enforce role + ownership constraints
- use
- Validate startup:
uterm-server --config scripts/uterm-server.jwt.example.toml- run smoke tests against
/api/health,/api/sessions, and browser WS connect
JWT Browser-UI Caveat
In auth.mode = "jwt", the hosted HTML pages authenticate correctly when the
initial page request carries an Authorization: Bearer ... header. However,
the page routes do not currently bridge that JWT into auth.token_cookie, so
browser follow-up requests to /api/... must still present an authorization
header. If you rely on direct browser navigation to the hosted UI, put the app
behind an auth proxy that injects the header on API requests, or stay on
auth.mode = "dev"/header for local use until token-cookie bridging lands.
Key Rotation
jwt_jwks_urlmode: rotate signing keys at the IdP, publish new JWKs, then retire old keys after token TTL.jwt_public_key_pemmode: deploy new config and restart server(s) in rolling fashion.- Rotate
worker_bearer_tokenindependently from user-facing tokens; keep overlap window short.
Failure Behavior
- Missing/invalid/expired JWT:
- HTTP routes:
401 - WebSocket routes: close with policy violation (
1008)
- HTTP routes:
- Authenticated but unauthorized action:
- HTTP routes:
403 - Browser WS hijack attempts: explicit error event; no privilege escalation
- HTTP routes:
- Invalid JWT runtime config:
- app startup raises
ValueError(fail-fast)
- app startup raises
For connector_type = "ssh", the session entry can use these auth fields:
passwordfor password authenticationclient_key_pathfor a private key file pathclient_key_datafor inline PEM private key textclient_keyfor a single AsyncSSH-compatible key valueclient_keysfor multiple keysknown_hoststo override host-key verification behavior (nulldisables checks for local/dev use)
The SSH connector intentionally skips user SSH config discovery so startup stays
predictable and fast in the hosted server. If you need key-based auth in config
without a file path, prefer client_key_data.
Frontend — UndefTerminal
Standalone terminal widget (no hijack controls):
<div id="term"></div>
<script src="/static/terminal.js"></script>
<script>
new UndefTerminal(document.getElementById('term'), {
wsUrl: '/ws/terminal',
theme: 'crt', // 'crt' | 'bbs' | 'glass'
heartbeatMs: 25000, // keepalive ping interval (ms). 0 disables.
});
</script>
CLI
Install the [cli] extra, then:
uterm proxy — browser WS → telnet/SSH
Accepts browser WebSocket connections and proxies to a remote BBS.
# Basic telnet proxy
uterm proxy bbs.example.com 23
# Custom port and WS path
uterm proxy bbs.example.com 23 --port 9000 --path /ws/term
# SSH proxy (requires [ssh] extra)
uterm proxy bbs.example.com 22 --transport ssh
uterm listen — telnet/SSH client → WebSocket server
Accepts traditional telnet and/or SSH clients and proxies to a remote WebSocket terminal endpoint.
# Telnet listener
uterm listen wss://warp.undef.games/ws/terminal
# With custom ports
uterm listen wss://warp.undef.games/ws/terminal --port 2112 --ssh-port 2222
# With host key (SSH)
uterm listen wss://warp.undef.games/ws/terminal --server-key /etc/host_key
Docker
Pre-built Docker targets are provided for local testing of both backends.
FastAPI reference server
# Build (from repo root)
docker build -f docker/Dockerfile.server -t undef-terminal-server .
# Run — dashboard at http://localhost:27780/app/
docker run --rm -p 27780:27780 undef-terminal-server
# Custom config
docker run --rm -p 27780:27780 \
-v /path/to/my.toml:/config/server.toml:ro \
undef-terminal-server
The default config (docker/server.toml) starts in dev auth mode with one pre-configured shell session. Mount a custom TOML to add JWT, real connectors, or additional sessions — see scripts/uterm-server.jwt.example.toml for a full JWT example.
Cloudflare Worker (pywrangler dev)
# Build (requires Docker Buildx; Node 20 + Python 3.11 image)
docker build -f docker/Dockerfile.cf -t undef-terminal-cf .
# Run — worker at http://localhost:27788/api/health
docker run --rm -p 27788:27788 undef-terminal-cf
Runs pywrangler dev inside the container with AUTH_MODE=dev. Pass -e AUTH_MODE=jwt -e JWT_JWKS_URL=... etc. to test JWT auth. KV/DO state is local (SQLite in /tmp) — not written to Cloudflare.
Both backends together
docker compose -f docker/docker-compose.yml up
FastAPI on :27780, CF worker on :27788.
Quality Guarantees
- Test gate runs at 100% branch coverage (
--cov-branch), enforced viaaddoptsinpyproject.toml. - Pre-commit hooks enforce ruff, mypy strict, ty, bandit, and biome on every commit.
- Security audit via
pip-auditandbandit; timing-safe token comparison in auth paths. - All input size limits enforced at boundaries; fail-closed auth on misconfiguration.
Documentation Ownership
- README: installation, quick-start, and API overview.
- Operations: runbook, SLOs, and production readiness gates.
- Protocol: backend capability matrix and client contract.
- Release: governance, tagging, and publishing workflow.
Docs
- Operations Runbook
- Service SLOs
- Protocol Matrix
- Production Readiness Gates
- Release Governance
- Cloudflare Companion Package
License
AGPL-3.0-or-later. Copyright (c) 2025-2026 MindTenet LLC.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file undef_terminal-0.3.0.tar.gz.
File metadata
- Download URL: undef_terminal-0.3.0.tar.gz
- Upload date:
- Size: 232.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.10 {"installer":{"name":"uv","version":"0.10.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
84d0750e6a49b2e372c6fe13585fba8c14320da3819a03e1b6048d398ecdb11b
|
|
| MD5 |
2d7e3b36c309b0968ecbe39558653484
|
|
| BLAKE2b-256 |
ed52372dd2e9471a8f9028ad3d875048cd6bbc76eef59874f11b62fed80dd53b
|
File details
Details for the file undef_terminal-0.3.0-py3-none-any.whl.
File metadata
- Download URL: undef_terminal-0.3.0-py3-none-any.whl
- Upload date:
- Size: 248.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.10 {"installer":{"name":"uv","version":"0.10.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ec4c4a3a9fd8104ce96abbbbc3e2523219b76bb3da9c6c7f063ef85c7c848afb
|
|
| MD5 |
03176069e84fb244307e11746e2c731b
|
|
| BLAKE2b-256 |
7fc62da80931f695157dc1a6d8403ea1c703ed4651df29bfc1d524f318dadee7
|