diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d1f7982 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +.git +.gitignore +.env +.env.* +!.env.example +.venv +venv +__pycache__ +*.pyc +*.pyo +.pytest_cache +.mypy_cache +.ruff_cache +.idea +.vscode +README.md +Dockerfile +docker-compose.yml +run.sh diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..29855e6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +* text=auto eol=lf +*.sh text eol=lf +Dockerfile text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.py text eol=lf +*.md text eol=lf +*.txt text eol=lf +.env.example text eol=lf diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f1699b5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +FROM python:3.12-slim AS base + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PIP_NO_CACHE_DIR=1 \ + STREAMLIT_SERVER_ADDRESS=0.0.0.0 \ + STREAMLIT_SERVER_PORT=8501 \ + STREAMLIT_SERVER_HEADLESS=true \ + STREAMLIT_BROWSER_GATHER_USAGE_STATS=false + +WORKDIR /app + +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt ./ +RUN pip install -r requirements.txt + +COPY gpfs_agent ./gpfs_agent + +RUN useradd --create-home --uid 10001 app && chown -R app:app /app +USER app + +EXPOSE 8501 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ + CMD curl -fsS http://localhost:8501/_stcore/health || exit 1 + +CMD ["streamlit", "run", "gpfs_agent/web.py"] diff --git a/README.md b/README.md index 82c6878..764f960 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,70 @@ # gpfs-agent -Agentic chat front-end for the IBM Spectrum Scale (GPFS) REST API, driven by +Agentic chat front-end for the IBM Storage Scale (GPFS) REST API v3, driven by Claude with tool-use. Read-only by default; writes and destructive operations are gated behind explicit env flags. Ships with both a terminal REPL and a -pure-Python web UI (Streamlit). +pure-Python web UI (Streamlit). Runs natively or in Docker. ## Quick start -```powershell -cd D:\Projects\gpfs-agent -python -m venv .venv -.\.venv\Scripts\Activate.ps1 -pip install -r requirements.txt +### 1. Configure -Copy-Item .env.example .env +```bash +cp .env.example .env # edit .env: ANTHROPIC_API_KEY, GPFS_API_BASE, GPFS_USERNAME, GPFS_PASSWORD ``` -### Web UI +### 2a. Docker (recommended for Linux) -```powershell -streamlit run gpfs_agent\web.py +```bash +docker compose up -d --build +# open http://:8501 ``` -Opens http://localhost:8501 with a chat panel, a sidebar showing the mode and -system prompt, a tool-call log, and (when writes are enabled in `.env`) a -session-scoped "Auto-approve mutations" toggle. +Stop / view / update: -### Terminal REPL - -```powershell -python -m gpfs_agent +```bash +docker compose logs -f +docker compose pull && docker compose up -d +docker compose down ``` -Commands inside the REPL: `/reset`, `/system`, `/quit`. +Override the host port with `HOST_PORT=9000 docker compose up -d`. If your GPFS +GUI uses a private CA, drop the cert into `./certs/ca.pem` and uncomment the +volume + `GPFS_CA_BUNDLE` lines in `docker-compose.yml`. + +### 2b. Native Linux (no Docker) + +```bash +./run.sh # web UI on 0.0.0.0:8501 +./run.sh cli # terminal REPL +HOST=127.0.0.1 PORT=8080 ./run.sh # bind override +``` + +`run.sh` creates a `.venv` on first run and installs dependencies. + +### 2c. Windows (PowerShell) + +```powershell +python -m venv .venv +.\.venv\Scripts\Activate.ps1 +pip install -r requirements.txt +streamlit run gpfs_agent\web.py # web UI +python -m gpfs_agent # REPL +``` + +## Web UI + +The Streamlit app lives at `/` on the configured port. It shows: + +- A chat panel (uses Claude with tool-use against the GPFS REST API) +- A sidebar with model, API base, mode, the live system prompt, and a + tool-call log (last 25 calls with status, reason and payload) +- A "Reset conversation" button +- When `GPFS_ALLOW_WRITES=true`: a session-scoped *"Auto-approve mutations"* + checkbox. Mutating calls are denied unless this is on. + +REPL commands: `/reset`, `/system`, `/quit`. ## How the guardrails work @@ -54,15 +85,9 @@ Guardrails are enforced in three layers: `cfg.allowed_methods`, so it cannot ask for `DELETE` when disabled. 2. **Dispatcher** — `tools.py` re-checks method and allow/deny regex and triggers the confirm hook before any mutating HTTP call. -3. **System prompt** — `agent.py` injects natural-language guardrails so the - model behaves sensibly even when a guardrail is loose. - -### Mutation confirmation - -- **REPL**: prints the planned call and waits for `y` at the terminal. -- **Web UI**: mutations are denied unless the sidebar checkbox - *"Auto-approve mutations this session"* is on. The checkbox only appears - when `GPFS_ALLOW_WRITES=true`. +3. **System prompt** — `agent.py` injects natural-language guardrails plus a + curated v3 endpoint reference (see `endpoints.py`) so the agent has + accurate context without having to guess. ## Project layout @@ -73,17 +98,24 @@ gpfs_agent/ ├── agent.py # Anthropic tool-use loop ├── cli.py # Rich REPL ├── config.py # .env loading & validation +├── endpoints.py # curated /scalemgmt/v3 reference (in-context) ├── gpfs_client.py # httpx wrapper around the GPFS REST API ├── tools.py # Tool schemas + guarded dispatcher └── web.py # Streamlit chat app +Dockerfile # python:3.12-slim + streamlit +docker-compose.yml # one-command deploy +run.sh # bash helper for native Linux ``` ## Notes on the GPFS REST API - `GPFS_API_BASE` should include the version prefix, e.g. - `https://gui:443/scalemgmt/v3`. Shipping Spectrum Scale builds currently - serve `/v2`; set whichever your cluster exposes. -- Auth is HTTP Basic against the Spectrum Scale GUI user. Use an account - scoped to the operations you intend to allow. + `https://gui:46443/scalemgmt/v3`. v3 is the current native REST API on + Storage Scale 5.2.x / 6.0.x; the typical port is 443 (CNSA / container + deploys) or 46443 (native install). +- Auth is HTTP Basic against a Storage Scale GUI user. Use an account scoped + to the operations you intend to allow. - For self-signed lab gear, set `GPFS_VERIFY_TLS=false` or point - `GPFS_CA_BUNDLE` at a PEM file. + `GPFS_CA_BUNDLE` at a PEM file (mount as a volume when running in Docker). +- The interactive Swagger UI lives at `https:///openapi/` — useful for + discovering endpoints your specific release ships. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..18aa438 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +services: + gpfs-agent: + build: . + image: gpfs-agent:latest + container_name: gpfs-agent + restart: unless-stopped + ports: + - "${HOST_PORT:-8501}:8501" + env_file: + - .env + # Uncomment to mount a custom CA bundle and tell the app to use it. + # volumes: + # - ./certs:/certs:ro + # environment: + # GPFS_CA_BUNDLE: /certs/ca.pem diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..bfc8ed0 --- /dev/null +++ b/run.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" + +if [[ ! -f .env ]]; then + echo "No .env found. Copy .env.example to .env and edit it first." >&2 + exit 1 +fi + +if [[ ! -d .venv ]]; then + echo "Creating virtualenv at .venv..." + python3 -m venv .venv + .venv/bin/pip install -q --upgrade pip + .venv/bin/pip install -q -r requirements.txt +fi + +MODE="${1:-web}" +HOST="${HOST:-0.0.0.0}" +PORT="${PORT:-8501}" + +case "$MODE" in + web) + exec .venv/bin/streamlit run gpfs_agent/web.py \ + --server.address "$HOST" \ + --server.port "$PORT" \ + --server.headless true \ + --browser.gatherUsageStats false + ;; + cli|repl) + exec .venv/bin/python -m gpfs_agent + ;; + *) + echo "Usage: $0 [web|cli]" >&2 + exit 2 + ;; +esac