Skip to main content

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 WebSocket
  • GET /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}/mode
  • POST /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/health
  • GET /api/sessions
  • GET /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 uses auth.mode = "dev".
  • Production should run auth.mode = "jwt" with:
    • jwt_issuer, jwt_audience
    • jwt_public_key_pem or jwt_jwks_url
    • jwt_algorithms (for example ["RS256"])
    • worker_bearer_token for hosted runtime worker WebSocket authentication

When auth.mode = "jwt", the server fails fast at startup unless:

  • worker_bearer_token is set
  • jwt_algorithms is non-empty
  • at least one key source is configured (jwt_public_key_pem or jwt_jwks_url)

JWT Deployment Runbook

  1. Configure JWT trust:
    • set jwt_issuer, jwt_audience, jwt_algorithms
    • prefer jwt_jwks_url for key rotation without restarts
  2. Configure runtime worker auth:
    • mint a dedicated service JWT (admin role) for worker_bearer_token
    • scope/TTL this token to server runtime usage only
  3. Set session ownership/visibility:
    • use owner + visibility to enforce role + ownership constraints
  4. 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_url mode: rotate signing keys at the IdP, publish new JWKs, then retire old keys after token TTL.
  • jwt_public_key_pem mode: deploy new config and restart server(s) in rolling fashion.
  • Rotate worker_bearer_token independently from user-facing tokens; keep overlap window short.

Failure Behavior

  • Missing/invalid/expired JWT:
    • HTTP routes: 401
    • WebSocket routes: close with policy violation (1008)
  • Authenticated but unauthorized action:
    • HTTP routes: 403
    • Browser WS hijack attempts: explicit error event; no privilege escalation
  • Invalid JWT runtime config:
    • app startup raises ValueError (fail-fast)

For connector_type = "ssh", the session entry can use these auth fields:

  • password for password authentication
  • client_key_path for a private key file path
  • client_key_data for inline PEM private key text
  • client_key for a single AsyncSSH-compatible key value
  • client_keys for multiple keys
  • known_hosts to override host-key verification behavior (null disables 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 via addopts in pyproject.toml.
  • Pre-commit hooks enforce ruff, mypy strict, ty, bandit, and biome on every commit.
  • Security audit via pip-audit and bandit; 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


License

AGPL-3.0-or-later. Copyright (c) 2025-2026 MindTenet LLC.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

undef_terminal-0.3.0.tar.gz (232.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

undef_terminal-0.3.0-py3-none-any.whl (248.4 kB view details)

Uploaded Python 3

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

Hashes for undef_terminal-0.3.0.tar.gz
Algorithm Hash digest
SHA256 84d0750e6a49b2e372c6fe13585fba8c14320da3819a03e1b6048d398ecdb11b
MD5 2d7e3b36c309b0968ecbe39558653484
BLAKE2b-256 ed52372dd2e9471a8f9028ad3d875048cd6bbc76eef59874f11b62fed80dd53b

See more details on using hashes here.

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

Hashes for undef_terminal-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ec4c4a3a9fd8104ce96abbbbc3e2523219b76bb3da9c6c7f063ef85c7c848afb
MD5 03176069e84fb244307e11746e2c731b
BLAKE2b-256 7fc62da80931f695157dc1a6d8403ea1c703ed4651df29bfc1d524f318dadee7

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page