settings refactor
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
# Terminal Plugin Spec
|
||||
|
||||
This document describes the Terminal plugin implementation, architecture, and integration points. It is a reference for continuing work later.
|
||||
|
||||
## Goals
|
||||
|
||||
- Provide a browser terminal (xterm.js) inside Officer.
|
||||
- Support two execution modes:
|
||||
- Host mode: shell runs on the host machine.
|
||||
- Sandbox mode: shell runs inside a Docker sidecar container.
|
||||
- Keep host mode working and optional; sandbox is enabled via admin settings.
|
||||
- Keep all terminal logic self-contained in the plugin.
|
||||
- Allow the terminal UI to be rendered anywhere with a reusable component.
|
||||
|
||||
## File Layout
|
||||
|
||||
- `src/workspaces/plugins/Terminal/index.ts`
|
||||
- Exports plugin metadata, client screen, and server websocket handler.
|
||||
- `src/workspaces/plugins/Terminal/client/TerminalView.tsx`
|
||||
- Reusable terminal component (xterm + websocket bridge).
|
||||
- `src/workspaces/plugins/Terminal/client/Screen/index.tsx`
|
||||
- Page wrapper that renders `TerminalView` inside `DashboardLayout`.
|
||||
- `src/workspaces/plugins/Terminal/client/index.ts`
|
||||
- Exports `Screen` and `TerminalView`.
|
||||
- `src/workspaces/plugins/Terminal/server/websocket.ts`
|
||||
- WebSocket handler for terminal sessions and sidecar orchestration.
|
||||
- `src/workspaces/plugins/Terminal/server/pty-sidecar.mjs`
|
||||
- Node sidecar process that owns `node-pty` and shell spawn.
|
||||
- `src/workspaces/plugins/Terminal/server/Dockerfile.terminal-sidecar`
|
||||
- Docker image for sandbox mode.
|
||||
- `src/workspaces/plugins/Terminal/server/templates/.zshrc`
|
||||
- `src/workspaces/plugins/Terminal/server/templates/starship-officer.toml`
|
||||
- Template configs copied into user home for stable shell UX.
|
||||
|
||||
## Client Architecture
|
||||
|
||||
### TerminalView
|
||||
|
||||
`TerminalView` is the reusable component that can be rendered anywhere.
|
||||
|
||||
Props:
|
||||
|
||||
- `className?: string`
|
||||
- `style?: CSSProperties`
|
||||
- `wsPath?: string` (default: `/api/terminal/ws`)
|
||||
- `fontSize?: number` (default: 14)
|
||||
- `fontFamily?: string`
|
||||
- `theme?: { background?; foreground?; cursor?; selectionBackground? }`
|
||||
- `autoFocus?: boolean` (default: true)
|
||||
- `onReady?: (term: XTerm) => void`
|
||||
- `onExit?: () => void`
|
||||
- `onDisconnect?: () => void`
|
||||
|
||||
Behavior:
|
||||
|
||||
- Initializes xterm after mount for StrictMode compatibility.
|
||||
- Connects to websocket at `wsPath`, appending `token` from localStorage.
|
||||
- Sends `resize` on open, and on ResizeObserver changes.
|
||||
- Forwards terminal input as `{ type: 'input', data }` JSON.
|
||||
- Writes server output on `{ type: 'output', data }`.
|
||||
- Emits `onExit` when `{ type: 'exit' }` is received.
|
||||
- Emits `onDisconnect` when the websocket closes.
|
||||
|
||||
### Screen
|
||||
|
||||
`Screen` is a thin wrapper around `TerminalView` for the full-page route:
|
||||
|
||||
```
|
||||
<DashboardLayout>
|
||||
<TerminalView className="h-full w-full p-2" />
|
||||
</DashboardLayout>
|
||||
```
|
||||
|
||||
This matches plugin conventions used by FileBrowser.
|
||||
|
||||
## Server Architecture
|
||||
|
||||
### WebSocket Flow
|
||||
|
||||
The terminal websocket is mounted in `src/server.tsx`:
|
||||
|
||||
- Path: `/api/terminal/ws`
|
||||
- Auth: `token` query parameter, validated with `verify()`.
|
||||
- Server handler: `terminalWebsocket` from `plugins/Terminal/server`.
|
||||
|
||||
Message formats between client and server:
|
||||
|
||||
- Client -> server:
|
||||
- `{ type: 'input', data: string }`
|
||||
- `{ type: 'resize', cols: number, rows: number }`
|
||||
- Server -> client:
|
||||
- `{ type: 'output', data: string }`
|
||||
- `{ type: 'exit' }`
|
||||
|
||||
### Sidecar Model
|
||||
|
||||
The Bun server does not spawn PTYs directly. Instead it proxies to a Node sidecar
|
||||
that runs `node-pty` for reliability.
|
||||
|
||||
There are two sidecar modes:
|
||||
|
||||
1. Host mode
|
||||
- Spawns a local Node sidecar on the host via Bun.
|
||||
- Sidecar binds to `127.0.0.1`.
|
||||
|
||||
2. Docker sandbox mode
|
||||
- Runs a Docker container per user.
|
||||
- Container binds to `0.0.0.0` and is mapped to a host port.
|
||||
- Container runs the same Node sidecar and shell inside the container.
|
||||
|
||||
### Mode Selection
|
||||
|
||||
- Settings are stored in `~/.config/officer.dev/server-settings.json`.
|
||||
- `terminalSandboxed: true` enables Docker mode.
|
||||
- If `terminalSandboxed` is falsy, host mode is used.
|
||||
|
||||
### Sidecar Handshake
|
||||
|
||||
On websocket open:
|
||||
|
||||
- Determine user home: `getHomeDir(email)`.
|
||||
- Ensure the home directory exists.
|
||||
- Determine mode (host or docker).
|
||||
- Connect to sidecar on a port (host: default 5337; docker: user-specific).
|
||||
- Send init payload to sidecar:
|
||||
|
||||
```
|
||||
{
|
||||
type: 'init',
|
||||
shell: { command, args, name },
|
||||
cwd,
|
||||
homeDir,
|
||||
userLabel
|
||||
}
|
||||
```
|
||||
|
||||
- `userLabel` is the email used by the prompt template.
|
||||
- `cwd` and `homeDir` are `/home/officer` in Docker, or the user home on host.
|
||||
|
||||
### Docker Container Lifecycle
|
||||
|
||||
Containers are named `officer-terminal-${userId}` and stored per user in:
|
||||
|
||||
`data/terminal-containers.json`
|
||||
|
||||
Rules:
|
||||
|
||||
- Reuse an existing container if it exists and is running.
|
||||
- Restart the container if it exists but stopped.
|
||||
- Start a new container if none exists; allocate a port starting at 54000.
|
||||
- Containers are left running for reuse when websocket closes.
|
||||
|
||||
### Docker Image
|
||||
|
||||
- Tag: `officer-terminal-sidecar:v1`
|
||||
- Built from `Dockerfile.terminal-sidecar` when missing.
|
||||
|
||||
Includes:
|
||||
|
||||
- zsh, git, curl, ca-certificates
|
||||
- oh-my-zsh
|
||||
- starship
|
||||
- fortune-mod, cowsay
|
||||
- eza
|
||||
- node, ws, node-pty
|
||||
|
||||
### Shell Configuration
|
||||
|
||||
Templates are copied into the user's home if missing:
|
||||
|
||||
- `.zshrc`
|
||||
- `.config/starship-officer.toml`
|
||||
|
||||
This provides a stable base for all users while allowing per-user customization.
|
||||
|
||||
Prompt requirement:
|
||||
|
||||
- Format: `email@domain:officer.dev`
|
||||
- Email colored in button blue (#0891B2)
|
||||
|
||||
## Plugin Conventions
|
||||
|
||||
- `index.ts` exports `plugin` metadata with `id` matching folder name.
|
||||
- Client exports use `Widget`/`Screen` conventions. Terminal only exports `Screen`
|
||||
and `TerminalView` (no widget).
|
||||
- Server exports a websocket handler (not a router). This is a special-case
|
||||
plugin that is mounted in `src/server.tsx`.
|
||||
|
||||
## Known Constraints
|
||||
|
||||
- Terminal mode is global via server settings; no per-component mode selection.
|
||||
- `TerminalView` builds the websocket URL from `window.location` and localStorage
|
||||
token; custom auth is not supported yet.
|
||||
- Sidecar connection retries are bounded; errors are printed to the terminal.
|
||||
|
||||
## Potential Future Enhancements
|
||||
|
||||
- Allow `TerminalView` to request mode/cwd via websocket init payload.
|
||||
- Add a read-only mode or initial command support.
|
||||
- Add per-component theme presets or allow app-level defaults.
|
||||
- Add better telemetry/logging for sidecar startup failures.
|
||||
|
||||
## API Contract
|
||||
|
||||
This section describes the messages across each layer.
|
||||
|
||||
### Client <-> Server (Bun)
|
||||
|
||||
Client -> server:
|
||||
|
||||
- `input`
|
||||
- `{ type: 'input', data: string }`
|
||||
- `data` is raw terminal input from xterm.
|
||||
- `resize`
|
||||
- `{ type: 'resize', cols: number, rows: number }`
|
||||
|
||||
Server -> client:
|
||||
|
||||
- `output`
|
||||
- `{ type: 'output', data: string }`
|
||||
- `data` is raw PTY output to be written to xterm.
|
||||
- `exit`
|
||||
- `{ type: 'exit' }`
|
||||
- Signals terminal process termination.
|
||||
|
||||
### Server <-> Sidecar (Node)
|
||||
|
||||
Server -> sidecar:
|
||||
|
||||
- `init`
|
||||
- `{ type: 'init', shell, cwd, homeDir, userLabel }`
|
||||
- `shell`: `{ command: string; args: string[]; name: string }`
|
||||
- `cwd`: initial current working directory
|
||||
- `homeDir`: home directory to set in the sidecar
|
||||
- `userLabel`: email for the prompt template
|
||||
- `input`
|
||||
- `{ type: 'input', data: string }`
|
||||
- `resize`
|
||||
- `{ type: 'resize', cols: number, rows: number }`
|
||||
|
||||
Sidecar -> server:
|
||||
|
||||
- `output`
|
||||
- `{ type: 'output', data: string }`
|
||||
- `exit`
|
||||
- `{ type: 'exit' }`
|
||||
|
||||
## Sequence Diagram
|
||||
|
||||
```
|
||||
Client (xterm) Bun Server Sidecar (Node) Shell
|
||||
| | | |
|
||||
| WS connect | | |
|
||||
|---------------> | | |
|
||||
| | connect sidecar | |
|
||||
| |-------------------> | |
|
||||
| | init (shell/cwd) | |
|
||||
| |-------------------> | spawn PTY |
|
||||
| | |----------------->|
|
||||
| input | | |
|
||||
|---------------> | input | |
|
||||
| |-------------------> | write to PTY |
|
||||
| output | | |
|
||||
|<--------------- | output | read from PTY |
|
||||
| |<------------------- | |
|
||||
| resize | | |
|
||||
|---------------> | resize | |
|
||||
| |-------------------> | set PTY size |
|
||||
```
|
||||
|
||||
## Docker Troubleshooting
|
||||
|
||||
Common failure modes and checks:
|
||||
|
||||
- Docker not installed or not in PATH
|
||||
- `ensureDockerImage()` will throw. Install Docker or fix PATH.
|
||||
|
||||
- Image build fails
|
||||
- Build uses `Dockerfile.terminal-sidecar` and tag `officer-terminal-sidecar:v1`.
|
||||
- Check for network access (apt), and that build context is the plugin folder.
|
||||
|
||||
- Container starts but sidecar is unreachable
|
||||
- Container binds to `0.0.0.0`, host maps `127.0.0.1:${port}:${port}`.
|
||||
- Port is user-specific, stored in `data/terminal-containers.json`.
|
||||
- Verify with `docker ps` and `docker logs` for the container name.
|
||||
|
||||
- Permissions inside container
|
||||
- Container runs with host UID/GID derived from the mounted user home.
|
||||
- If home dir ownership is incorrect, zsh history may fail to write.
|
||||
|
||||
- Shell missing
|
||||
- Container must have zsh installed; the Dockerfile includes zsh.
|
||||
- Host mode resolves shell from `$SHELL` or known paths.
|
||||
|
||||
## Per-User Template Rules
|
||||
|
||||
Templates are copied into user home on first launch only.
|
||||
|
||||
Files:
|
||||
|
||||
- `.zshrc`
|
||||
- `.config/starship-officer.toml`
|
||||
|
||||
Rules:
|
||||
|
||||
- If a file already exists, it is not overwritten.
|
||||
- This lets the admin provide a stable default while allowing user customization.
|
||||
- To reset a user to defaults, delete the per-user files and reconnect.
|
||||
Reference in New Issue
Block a user