Initialize repository snapshot
This commit is contained in:
123
scripts/offsec_node_client.py
Normal file
123
scripts/offsec_node_client.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
Thin client for talking to the OffSec Shield Node (offsec-agents MCP backend).
|
||||
|
||||
Usage:
|
||||
|
||||
from scripts.offsec_node_client import OffsecNodeClient
|
||||
|
||||
client = OffsecNodeClient() # uses OFFSEC_NODE_URL env
|
||||
agents = await client.command("agents list")
|
||||
status = await client.command("tem status")
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import aiohttp
|
||||
|
||||
|
||||
DEFAULT_OFFSEC_NODE_URL = "http://shield-vm:8081"
|
||||
|
||||
|
||||
@dataclass
|
||||
class OffsecNodeError(Exception):
|
||||
message: str
|
||||
status: Optional[int] = None
|
||||
details: Optional[Dict[str, Any]] = None
|
||||
|
||||
def __str__(self) -> str:
|
||||
base = self.message
|
||||
if self.status is not None:
|
||||
base += f" (status={self.status})"
|
||||
if self.details:
|
||||
base += f" details={self.details}"
|
||||
return base
|
||||
|
||||
|
||||
@dataclass
|
||||
class OffsecNodeClient:
|
||||
base_url: str = DEFAULT_OFFSEC_NODE_URL
|
||||
timeout_seconds: int = 10
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "OffsecNodeClient":
|
||||
url = os.getenv("OFFSEC_NODE_URL", DEFAULT_OFFSEC_NODE_URL)
|
||||
return cls(base_url=url)
|
||||
|
||||
async def health(self) -> Dict[str, Any]:
|
||||
url = f"{self.base_url.rstrip('/')}/health"
|
||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=self.timeout_seconds)) as session:
|
||||
async with session.get(url) as resp:
|
||||
if resp.status != 200:
|
||||
raise OffsecNodeError("Shield node health check failed", status=resp.status)
|
||||
return await resp.json()
|
||||
|
||||
async def command(
|
||||
self,
|
||||
command: str,
|
||||
session_id: str = "vaultmesh-client",
|
||||
user: str = "vaultmesh",
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Send a command to the Shield Node MCP backend.
|
||||
|
||||
Example commands:
|
||||
"agents list"
|
||||
"status"
|
||||
"shield status"
|
||||
"proof latest"
|
||||
"agent spawn recon"
|
||||
"""
|
||||
url = f"{self.base_url.rstrip('/')}/mcp/command"
|
||||
payload: Dict[str, Any] = {
|
||||
"session_id": session_id,
|
||||
"user": user,
|
||||
"command": command,
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=self.timeout_seconds)) as session:
|
||||
async with session.post(url, json=payload) as resp:
|
||||
text = await resp.text()
|
||||
if resp.status != 200:
|
||||
# Try to parse JSON details if present
|
||||
try:
|
||||
data = json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
data = None
|
||||
raise OffsecNodeError(
|
||||
"Shield node command failed",
|
||||
status=resp.status,
|
||||
details=data or {"raw": text},
|
||||
)
|
||||
try:
|
||||
return json.loads(text)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise OffsecNodeError("Invalid JSON from Shield node", details={"raw": text}) from exc
|
||||
|
||||
|
||||
# Optional: CLI entrypoint for quick manual tests
|
||||
async def _demo() -> None:
|
||||
client = OffsecNodeClient.from_env()
|
||||
print(f"[offsec-node] base_url={client.base_url}")
|
||||
try:
|
||||
health = await client.health()
|
||||
print("Health:", json.dumps(health, indent=2))
|
||||
except OffsecNodeError as e:
|
||||
print("Health check failed:", e)
|
||||
return
|
||||
|
||||
try:
|
||||
agents = await client.command("agents list")
|
||||
print("Agents:", json.dumps(agents, indent=2))
|
||||
except OffsecNodeError as e:
|
||||
print("Command failed:", e)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(_demo())
|
||||
Reference in New Issue
Block a user