Initial commit: agentic chat app for IBM Storage Scale REST API v3
Adds a Python package that wraps the Storage Scale /scalemgmt/v3 management API behind a Claude-driven tool-use loop. Ships a Rich-styled terminal REPL and a Streamlit web UI sharing the same Agent/Dispatcher/GPFSClient stack. Guardrails are env-driven (.env): read-only by default; writes and DELETE gated by GPFS_ALLOW_WRITES / GPFS_ALLOW_DESTRUCTIVE; optional path allow/deny regex; mutating calls require operator confirmation. Free-text GPFS_INSTRUCTIONS (or GPFS_INSTRUCTIONS_FILE) appended to the system prompt. The system prompt also includes a curated v3 endpoint reference compiled from IBM Storage Scale 5.2.3 / 6.0.0 documentation so the agent doesn't have to guess endpoint shapes for common operations. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
commit
8840ba74f7
13 changed files with 959 additions and 0 deletions
106
gpfs_agent/config.py
Normal file
106
gpfs_agent/config.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Optional, Pattern
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
def _bool(name: str, default: bool = False) -> bool:
|
||||
raw = os.getenv(name)
|
||||
if raw is None or raw == "":
|
||||
return default
|
||||
return raw.strip().lower() in {"1", "true", "yes", "on", "y"}
|
||||
|
||||
|
||||
def _required(name: str) -> str:
|
||||
val = os.getenv(name)
|
||||
if not val:
|
||||
raise RuntimeError(
|
||||
f"Missing required env var {name}. Copy .env.example to .env and fill it in."
|
||||
)
|
||||
return val
|
||||
|
||||
|
||||
def _optional_pattern(name: str) -> Optional[Pattern[str]]:
|
||||
raw = os.getenv(name)
|
||||
if not raw:
|
||||
return None
|
||||
try:
|
||||
return re.compile(raw)
|
||||
except re.error as exc:
|
||||
raise RuntimeError(f"Invalid regex in {name}: {exc}") from exc
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Config:
|
||||
anthropic_api_key: str
|
||||
model: str
|
||||
max_tokens: int
|
||||
|
||||
gpfs_base: str
|
||||
gpfs_username: str
|
||||
gpfs_password: str
|
||||
verify_tls: bool | str
|
||||
timeout: float
|
||||
|
||||
allow_writes: bool
|
||||
allow_destructive: bool
|
||||
confirm_mutations: bool
|
||||
|
||||
path_allow: Optional[Pattern[str]] = None
|
||||
path_deny: Optional[Pattern[str]] = None
|
||||
extra_instructions: str = ""
|
||||
|
||||
@property
|
||||
def allowed_methods(self) -> set[str]:
|
||||
methods = {"GET"}
|
||||
if self.allow_writes:
|
||||
methods |= {"POST", "PUT"}
|
||||
if self.allow_writes and self.allow_destructive:
|
||||
methods.add("DELETE")
|
||||
return methods
|
||||
|
||||
|
||||
def load_config(env_file: Optional[Path] = None) -> Config:
|
||||
if env_file is not None:
|
||||
load_dotenv(env_file, override=False)
|
||||
else:
|
||||
load_dotenv(override=False)
|
||||
|
||||
verify_raw = os.getenv("GPFS_VERIFY_TLS", "true").strip().lower()
|
||||
ca_bundle = os.getenv("GPFS_CA_BUNDLE")
|
||||
if ca_bundle:
|
||||
verify: bool | str = ca_bundle
|
||||
elif verify_raw in {"0", "false", "no", "off", "n"}:
|
||||
verify = False
|
||||
else:
|
||||
verify = True
|
||||
|
||||
instructions = os.getenv("GPFS_INSTRUCTIONS", "").strip()
|
||||
instructions_file = os.getenv("GPFS_INSTRUCTIONS_FILE")
|
||||
if instructions_file:
|
||||
path = Path(instructions_file)
|
||||
if path.is_file():
|
||||
file_text = path.read_text(encoding="utf-8").strip()
|
||||
instructions = f"{instructions}\n\n{file_text}".strip() if instructions else file_text
|
||||
|
||||
return Config(
|
||||
anthropic_api_key=_required("ANTHROPIC_API_KEY"),
|
||||
model=os.getenv("ANTHROPIC_MODEL", "claude-sonnet-4-6"),
|
||||
max_tokens=int(os.getenv("ANTHROPIC_MAX_TOKENS", "4096")),
|
||||
gpfs_base=_required("GPFS_API_BASE").rstrip("/"),
|
||||
gpfs_username=_required("GPFS_USERNAME"),
|
||||
gpfs_password=_required("GPFS_PASSWORD"),
|
||||
verify_tls=verify,
|
||||
timeout=float(os.getenv("GPFS_TIMEOUT", "30")),
|
||||
allow_writes=_bool("GPFS_ALLOW_WRITES", False),
|
||||
allow_destructive=_bool("GPFS_ALLOW_DESTRUCTIVE", False),
|
||||
confirm_mutations=_bool("GPFS_CONFIRM_MUTATIONS", True),
|
||||
path_allow=_optional_pattern("GPFS_PATH_ALLOW"),
|
||||
path_deny=_optional_pattern("GPFS_PATH_DENY"),
|
||||
extra_instructions=instructions,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue