diff --git a/README.md b/README.md index c6552ae..08a34d1 100644 --- a/README.md +++ b/README.md @@ -1,119 +1,157 @@ # cli-mcp-server -MCP server that provides AI agents with a sandboxed bash shell inside per-investigation Kubernetes pods. +MCP server that gives AI agents a **sandboxed bash shell** inside per-session Kubernetes pods. -## Architecture +Agents call a single `bash` tool — pipes, redirects, chaining, and whatever CLIs are installed in the sandbox image. The MCP server is a stateless control plane: it creates and routes to sandbox pods, then proxies commands. Session state (working directory, env vars, files) lives in the pod’s persistent bash process. -Two binaries in one repo: +Designed to run against a Kubernetes (or OpenShift) cluster — the server manages sandbox pods via the API. It is not a local-only shell MCP. -- **cli-mcp-server** (`cmd/server/`) — Control plane. Manages sandbox pod lifecycle via client-go, exposes the `bash` MCP tool, proxies commands to sandbox agents. -- **sandbox-agent** (`cmd/agent/`) — Data plane. Runs inside each sandbox pod, manages a persistent bash process with `oc`/`kubectl`/`jq`/`yq`/`curl`. +## Features -## Project Structure +- **Persistent bash per session** — cwd, environment, and `/workspace` files survive across tool calls +- **Full shell, not a command allowlist** — security comes from pod isolation, RBAC, and network policy +- **Per-session agent auth** — MCP server → sandbox `/exec` uses an HMAC-derived bearer token (defense in depth beyond NetworkPolicy) +- **Customizable CLIs** — available tools are whatever is in the sandbox agent image (the default image includes `oc`/`kubectl`) +- **Stateless, multi-replica ready** — any server replica can handle any request; Kubernetes is the source of truth +- **Optional warm pool** — pre-warmed pods cut cold-start latency when enabled -``` -cli-mcp-server/ -├── cmd/ -│ ├── server/main.go # MCP server entry point -│ └── agent/main.go # Sandbox agent entry point -├── pkg/ -│ ├── session/ # Pod lifecycle, warm pool, cache -│ ├── agent/ # HTTP client + shared types -│ ├── sandbox/ # Bash session + HTTP handlers -│ ├── server/ # MCP server setup -│ ├── tools/ # MCP tool handlers (bash) -│ └── version/ # Build version info -├── docs/ -│ ├── proposals/ # Design documents -│ └── implementation/ # Implementation plan and Jira stories -├── Makefile -└── go.mod -``` +## Usage -## Build +Works with any MCP client that can call tools over HTTP (or stdio) and send an `X-Session-ID` header. Over HTTP this is straightforward; over stdio it depends on whether the client/SDK can attach request headers. -```bash -make build # Build both server and agent -make build-server # Build only the MCP server -make build-agent # Build only the sandbox agent -make test # Run tests -make lint # Run linter -make build-prod # Production build (static, CGO disabled) +### MCP tool: `bash` + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `command` | string | yes | Shell command (full bash — pipes, redirects, chaining) | +| `timeout` | int | no | Max execution time in seconds (default 60, max 300) | + +Example calls: + +```json +{"command": "kubectl get pods -n kube-system --context=prod"} +{"command": "kubectl get pods -o json | jq '.items[].metadata.name'", "timeout": 120} ``` -## Session & Pod Lifecycle +Returns `stdout`, `stderr`, `exit_code`, and `duration_ms`. Non-zero exit codes are tool results (not transport errors) so the agent can use failure output. -Each investigation gets its own sandbox pod. The MCP server is stateless — session -state lives in Kubernetes (pod labels/annotations) and is cached in memory for performance. +### Session routing -### Request Flow +Every request must include: +```http +X-Session-ID: ``` -TARSy (LLM) - │ MCP tool call + X-Session-ID header - ▼ -MCP Server (stateless, multi-replica) - │ HTTP POST /exec + Bearer HMAC token - ▼ -Sandbox Pod (per-session, persistent bash) - │ stdout / stderr / exit_code - ▼ -MCP Server → TARSy -``` -### GetOrCreatePod — Three-Tier Lookup +The server only uses this value to find or create a sandbox pod. Session IDs must be RFC 1123 DNS labels (`^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$`); invalid IDs fail at pod create. + +Typical client practice (agent harness / orchestrator — not the LLM): + +1. Generate a new session ID when starting work that should share one sandbox +2. Send that same ID on every follow-up `bash` call to reuse the persistent shell and `/workspace` +3. Call `DELETE /sessions/{id}` when finished (HTTP transport only) — removes the sandbox pod, auth secret, and cache entry -When a command arrives for a session, `SessionManager.GetOrCreatePod` resolves the -target pod using a three-tier strategy: +If the client never deletes the session, the sandbox is garbage-collected after `--idle-timeout` (default 30m). -| Tier | Method | Latency | Description | -|------|--------|---------|-------------| -| 1 | **PodCache** | ~0ms | In-memory map of session ID → pod IP with 30s TTL | -| 2 | **Label discovery** | ~50-100ms | `List` pods by `tarsy.redhat.com/session-id` label | -| 3 | **Create** | ~3-8s | Create auth Secret + Pod, wait for readiness probe | +## Configuration -Cache misses fall through to tier 2; if no pod exists, tier 3 creates one -idempotently (handles `AlreadyExists`). +### Server flags -### Warm Pool +| Flag | Default | Description | +|------|---------|-------------| +| `--transport` | `stdio` | `stdio` or `http` (`http` requires `--stateless`) | +| `--address` | `localhost:8080` | Listen address (HTTP; must be loopback) | +| `--stateless` | `false` | Required for HTTP / multi-replica | +| `--namespace` | `tarsy` | Namespace for sandbox pods | +| `--sandbox-image` | _(required)_ | Container image for sandbox pods | +| `--hmac-key-file` | _(required)_ | Path to shared HMAC secret | +| `--kubeconfig` | _(in-cluster)_ | Kubeconfig for managing sandbox pods | +| `--idle-timeout` | `30m` | Delete idle sandbox pods after this duration | +| `--warm-pool-size` | `0` | Pre-warmed pods (`0` = create on demand) | -When `--warm-pool-size N` is set, N pre-warmed pods are maintained in a ready state. -New sessions are assigned a warm pod instantly (~0ms) instead of waiting for a cold -start. Disabled by default (`N=0`). +### Sandbox image (available CLIs) -### Pod Spec +CLIs available to the agent are determined by the **sandbox agent image**, not by server code. -Each sandbox pod runs with a hardened security context: +The default image (`Containerfile.agent`) is based on `oc-client-base-minimal` and includes `oc`, `kubectl`, plus utilities such as `jq`, `yq`, and `curl`. -- **Non-root**: UID/GID 1001, `allowPrivilegeEscalation: false`, all capabilities dropped -- **Resources**: 100m/500m CPU, 128Mi/512Mi memory -- **Readiness**: HTTP GET `/health` on port 8090 -- **Volumes**: `kubeconfig` (read-only Secret mount), `workspace` (emptyDir) -- **Auth**: HMAC-SHA256(key, sessionID) → bearer token delivered via Secret → env var +To add other CLIs (for example `helm` or `virtctl`): -### Cleanup +1. Extend `Containerfile.agent` (or build a custom image from it) +2. Build and push the image +3. Point the MCP server at it with `--sandbox-image` -| Method | Trigger | Description | -|--------|---------|-------------| -| **Explicit delete** | `DELETE /sessions/{id}` | TARSy calls when investigation ends. Deletes pod, Secret, and cache entry | -| **Stale cleanup** | Background ticker | Deletes pods idle > 30 minutes (via `last-activity` annotation) | +No MCP server code changes are required. Tell the LLM what is available via your client’s server instructions (or equivalent); the `bash` tool description stays generic. + +## Architecture + +```mermaid +flowchart TB + Client[MCP Client] + Server["cli-mcp-server
stateless · N replicas"] + Sandbox["Sandbox pods
assigned sessions · optional warm pool"] + Target["Target infrastructure
e.g. Kubernetes API"] + + Client -->|"bash + X-Session-ID"| Server + Server -->|"POST /exec + HMAC token"| Sandbox + Sandbox -->|"CLIs from sandbox image / config"| Target +``` -If a sandbox pod crashes mid-investigation, shell state (env vars, working directory, -files in emptyDir) is lost. The MCP server detects the transport error, invalidates -the cache, and creates a new pod on the next call. TARSy retains the investigation -context. +Bash commands run in the sandbox pods. What they can reach (for example a Kubernetes API via `kubectl`) depends on the sandbox image and the kubeconfig/RBAC mounted into those pods — not on the MCP server binary. -## Design Documents +### Scalability -- [Sketch](docs/proposals/cli-mcp-server-sketch.md) — Problem statement and approach -- [Design Overview](docs/proposals/cli-mcp-server-design-overview.md) — Architecture overview -- [Detailed Design](docs/proposals/cli-mcp-server-design.md) — Go types, flows, YAML specs +The MCP server holds no durable session state. Pod identity is stored in Kubernetes labels; an in-memory cache speeds up routing. Any replica can serve any request, so you can scale the Deployment horizontally behind a load balancer with no sticky sessions. + +Optional `--warm-pool-size` keeps ready pods on hand so new sessions skip cold start (image pull + container boot). + +### Sandboxing and security + +Each session gets its own pod. That pod is the security boundary: + +- **Isolation** — non-root (runAsNonRoot), no privilege escalation, all capabilities dropped, resource limits +- **Credentials** — read-only kubeconfig mounted from a dedicated investigation ServiceAccount (typically view/read-only RBAC) +- **Network** — NetworkPolicy can restrict ingress to the MCP server and egress to intended APIs +- **Agent auth** — per-session HMAC bearer token; unauthenticated `/exec` calls are rejected +- **Ephemeral workspace** — `/workspace` is an `emptyDir`; destroyed with the pod + +The server does not filter shell commands. Capability is controlled by what is in the image and what RBAC allows. + +### Components + +| Component | Role | +|-----------|------| +| **cli-mcp-server** | Control plane: MCP `bash` tool, pod lifecycle, command proxy | +| **sandbox-agent** | Data plane inside each pod: persistent bash over HTTP (`/exec`, `/health`, `/assign`) | + +## Development + +```bash +make build # Build both server and agent +make build-server # Build only the MCP server +make build-agent # Build only the sandbox agent +make test # Run tests +make lint # Run linter +make build-prod # Production build (static, CGO disabled) +``` + +``` +cli-mcp-server/ +├── cmd/server/ # MCP server entry point +├── cmd/agent/ # Sandbox agent entry point +├── pkg/session/ # Pod lifecycle, warm pool, cache +├── pkg/sandbox/ # Bash session + agent HTTP handlers +├── pkg/tools/ # MCP tool handlers +├── pkg/server/ # MCP server + HTTP mux +└── docs/ # Design and implementation docs +``` -## Implementation +### Further reading -- [Implementation Plan](docs/implementation/implementation-plan.md) — Phased plan -- [Jira Stories](docs/implementation/stories.md) — Epic SANDBOX-1803 breakdown +- [Sketch](docs/proposals/cli-mcp-server-sketch.md) — problem statement and approach +- [Design Overview](docs/proposals/cli-mcp-server-design-overview.md) — architecture overview +- [Detailed Design](docs/proposals/cli-mcp-server-design.md) — types, flows, deployment specs ## License -The code is available under the [Apache License 2.0](LICENSE). \ No newline at end of file +[Apache License 2.0](LICENSE)