feat: custom Oracle AI robot logo

This commit is contained in:
nogueiraguh
2026-02-27 20:20:22 -03:00
parent 3b5b0c7c2a
commit 3c1d87dde3
6 changed files with 393 additions and 170 deletions

BIN
.DS_Store vendored

Binary file not shown.

3
.idea/misc.xml generated
View File

@@ -1,4 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.10 (oci-cis-agent)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (oci-cis-agent)" project-jdk-type="Python SDK" />
</project>

View File

@@ -1,10 +1,11 @@
"""
OCI CIS AI Agent - Backend API v2
FastAPI with JWT auth, TOTP MFA, RBAC, OCI GenAI integration (Cohere/Meta/xAI/Google),
MCP Server registry, Autonomous DB vector storage, CIS reports, chat agent, audit log.
OCI CIS AI Agent - Backend API v1.1
FastAPI with JWT auth, TOTP MFA, RBAC, OCI GenAI (exact SDK pattern),
OCI Account Explorer, MCP Server registry with VectorDB tool integration,
Autonomous DB vector storage, CIS reports, chat agent, audit log.
"""
import os, json, uuid, hashlib, hmac, time, base64, struct, secrets, subprocess
import shutil, asyncio, sqlite3, logging, socket, re, importlib, sys
import shutil, asyncio, sqlite3, logging, socket, re
from datetime import datetime, timedelta
from pathlib import Path
from typing import Optional, List, Dict, Any
@@ -30,6 +31,7 @@ OCI_DIR = DATA / "oci_configs"
REPORTS = DATA / "reports"
MCP_DIR = DATA / "mcp_servers"
WALLET_DIR = DATA / "wallets"
VERSION = "1.1"
for d in [DATA, OCI_DIR, REPORTS, MCP_DIR, WALLET_DIR]:
d.mkdir(parents=True, exist_ok=True)
@@ -37,7 +39,7 @@ for d in [DATA, OCI_DIR, REPORTS, MCP_DIR, WALLET_DIR]:
logging.basicConfig(level=logging.INFO)
log = logging.getLogger("agent")
app = FastAPI(title="OCI CIS AI Agent", version="2.0.0")
app = FastAPI(title="OCI CIS AI Agent", version=VERSION)
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True,
allow_methods=["*"], allow_headers=["*"])
security = HTTPBearer()
@@ -104,16 +106,21 @@ def init_db():
);
CREATE TABLE IF NOT EXISTS genai_configs (
id TEXT PRIMARY KEY, user_id TEXT NOT NULL,
name TEXT NOT NULL DEFAULT 'default',
oci_config_id TEXT NOT NULL,
model_id TEXT NOT NULL,
model_ocid TEXT,
compartment_id TEXT NOT NULL,
genai_region TEXT NOT NULL,
endpoint TEXT NOT NULL,
serving_type TEXT DEFAULT 'ON_DEMAND',
endpoint_id TEXT,
temperature REAL DEFAULT 0.7,
max_tokens INTEGER DEFAULT 2048,
top_p REAL DEFAULT 0.9,
top_k INTEGER DEFAULT -1,
dedicated_endpoint_id TEXT,
temperature REAL DEFAULT 1,
max_tokens INTEGER DEFAULT 6000,
top_p REAL DEFAULT 0.95,
top_k INTEGER DEFAULT 1,
frequency_penalty REAL DEFAULT 0,
presence_penalty REAL DEFAULT 0,
is_default INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (oci_config_id) REFERENCES oci_configs(id)
@@ -146,6 +153,8 @@ def init_db():
server_type TEXT NOT NULL DEFAULT 'stdio',
command TEXT, args TEXT, env_vars TEXT,
url TEXT, module_path TEXT,
tools TEXT,
linked_adb_id TEXT,
is_active INTEGER DEFAULT 1,
created_at TEXT DEFAULT (datetime('now'))
);
@@ -187,6 +196,12 @@ def _make_token(uid,role,sid):
def _enc(v): return base64.b64encode(v.encode()).decode()
def _dec(v): return base64.b64decode(v.encode()).decode()
# ── OCI SDK Client Helper ─────────────────────────────────────────────────────
def _get_oci_config(oci_config_id: str) -> dict:
import oci
config_path = str(OCI_DIR / oci_config_id / "config")
return oci.config.from_file(config_path, "DEFAULT")
# ── Auth deps ─────────────────────────────────────────────────────────────────
async def current_user(cred: HTTPAuthorizationCredentials = Depends(security)):
try: p = pyjwt.decode(cred.credentials, APP_SECRET, algorithms=[JWT_ALG])
@@ -221,13 +236,17 @@ class ChangePwReq(BaseModel):
class UserUpdateReq(BaseModel):
email: Optional[str] = None; role: Optional[str] = None; is_active: Optional[bool] = None
class ChatMsg(BaseModel):
message: str; session_id: Optional[str] = None; model_id: Optional[str] = None; genai_config_id: Optional[str] = None
message: str; session_id: Optional[str] = None; genai_config_id: Optional[str] = None
class RunReportReq(BaseModel):
config_id: str; mcp_server_id: Optional[str] = None; regions: Optional[List[str]] = None
class GenAIConfigReq(BaseModel):
oci_config_id: str; model_id: str; compartment_id: str; genai_region: str
serving_type: str = "ON_DEMAND"; endpoint_id: Optional[str] = None
temperature: float = 0.7; max_tokens: int = 2048; top_p: float = 0.9; top_k: int = -1
name: str = "default"
oci_config_id: str; model_id: str; model_ocid: Optional[str] = None
compartment_id: str; genai_region: str
endpoint: Optional[str] = None
serving_type: str = "ON_DEMAND"; dedicated_endpoint_id: Optional[str] = None
temperature: float = 1; max_tokens: int = 6000; top_p: float = 0.95
top_k: int = 1; frequency_penalty: float = 0; presence_penalty: float = 0
is_default: bool = False
class ADBVectorReq(BaseModel):
config_name: str; dsn: str; username: str; password: str
@@ -235,7 +254,9 @@ class ADBVectorReq(BaseModel):
class MCPServerReq(BaseModel):
name: str; description: Optional[str] = None; server_type: str = "stdio"
command: Optional[str] = None; args: Optional[List[str]] = None
env_vars: Optional[Dict[str,str]] = None; url: Optional[str] = None; module_path: Optional[str] = None
env_vars: Optional[Dict[str,str]] = None; url: Optional[str] = None
module_path: Optional[str] = None; tools: Optional[List[str]] = None
linked_adb_id: Optional[str] = None
# ── Auth endpoints ────────────────────────────────────────────────────────────
@app.post("/api/auth/login")
@@ -314,9 +335,9 @@ async def update_user(uid: str, req: UserUpdateReq, adm=Depends(require("admin")
if req.role not in ("admin","user","viewer"): raise HTTPException(400, "Role inválida")
sets.append("role=?"); vals.append(req.role)
if req.is_active is not None: sets.append("is_active=?"); vals.append(int(req.is_active))
if sets: vals.append(uid); c_ = db()
with db() as c:
if sets: c.execute(f"UPDATE users SET {','.join(sets)} WHERE id=?", vals)
if sets:
vals.append(uid)
with db() as c: c.execute(f"UPDATE users SET {','.join(sets)} WHERE id=?", vals)
_audit(adm["id"], adm["username"], "update_user", uid)
return {"ok": True}
@@ -340,9 +361,11 @@ async def save_oci(
kp = cdir / "oci_api_key.pem"
kp.write_bytes(await private_key.read()); kp.chmod(0o600)
if public_key: (cdir / "oci_api_key_public.pem").write_bytes(await public_key.read())
(cdir / "config").write_text(
cfg_file = cdir / "config"
cfg_file.write_text(
f"[DEFAULT]\nuser={user_ocid}\nfingerprint={fingerprint}\n"
f"tenancy={tenancy_ocid}\nregion={region}\nkey_file={kp}\n")
cfg_file.chmod(0o600)
with db() as c:
c.execute("INSERT INTO oci_configs (id,user_id,tenancy_name,tenancy_ocid,user_ocid,fingerprint,region,key_file_path,compartment_id) VALUES (?,?,?,?,?,?,?,?,?)",
(cid, u["id"], tenancy_name, tenancy_ocid, user_ocid, fingerprint, region, str(kp), compartment_id or None))
@@ -375,11 +398,98 @@ async def test_oci(cid: str, u=Depends(require("admin","user"))):
r = subprocess.run(["oci","iam","region","list","--config-file",str(cp),"--output","json"], capture_output=True, text=True, timeout=30)
if r.returncode == 0: return {"status":"success","message":"Conexão OK","data":json.loads(r.stdout)}
return {"status":"error","message":r.stderr[:500]}
except FileNotFoundError: return {"status":"error","message":"OCI CLI não instalado. Instale via admin panel."}
except FileNotFoundError: return {"status":"error","message":"OCI CLI não disponível no container."}
except subprocess.TimeoutExpired: return {"status":"error","message":"Timeout na conexão"}
except Exception as e: return {"status":"error","message":str(e)}
# ── OCI GenAI Config ──────────────────────────────────────────────────────────
# ── OCI Account Explorer ──────────────────────────────────────────────────────
@app.get("/api/oci/explore/{cid}/compartments")
async def explore_compartments(cid: str, u=Depends(current_user)):
try:
import oci
config = _get_oci_config(cid)
identity = oci.identity.IdentityClient(config)
with db() as c:
cfg = c.execute("SELECT tenancy_ocid,compartment_id FROM oci_configs WHERE id=?", (cid,)).fetchone()
root = cfg["compartment_id"] or cfg["tenancy_ocid"]
comps = identity.list_compartments(root, compartment_id_in_subtree=True).data
return [{"id":cp.id,"name":cp.name,"lifecycle_state":cp.lifecycle_state,"description":cp.description or ""} for cp in comps if cp.lifecycle_state == "ACTIVE"]
except Exception as e:
return {"error": str(e)[:500]}
@app.get("/api/oci/explore/{cid}/regions")
async def explore_regions(cid: str, u=Depends(current_user)):
try:
import oci
config = _get_oci_config(cid)
identity = oci.identity.IdentityClient(config)
with db() as c:
cfg = c.execute("SELECT tenancy_ocid FROM oci_configs WHERE id=?", (cid,)).fetchone()
regions = identity.list_region_subscriptions(cfg["tenancy_ocid"]).data
return [{"name":r.region_name,"key":r.region_key,"status":r.status,"is_home":r.is_home_region} for r in regions]
except Exception as e:
return {"error": str(e)[:500]}
@app.get("/api/oci/explore/{cid}/vcns")
async def explore_vcns(cid: str, compartment_id: str = Query(None), u=Depends(current_user)):
try:
import oci
config = _get_oci_config(cid)
vn = oci.core.VirtualNetworkClient(config)
with db() as c:
cfg = c.execute("SELECT tenancy_ocid,compartment_id FROM oci_configs WHERE id=?", (cid,)).fetchone()
comp = compartment_id or cfg["compartment_id"] or cfg["tenancy_ocid"]
vcns = vn.list_vcns(comp).data
return [{"id":v.id,"display_name":v.display_name,"cidr_blocks":v.cidr_blocks,"lifecycle_state":v.lifecycle_state} for v in vcns]
except Exception as e:
return {"error": str(e)[:500]}
@app.get("/api/oci/explore/{cid}/instances")
async def explore_instances(cid: str, compartment_id: str = Query(None), u=Depends(current_user)):
try:
import oci
config = _get_oci_config(cid)
compute = oci.core.ComputeClient(config)
with db() as c:
cfg = c.execute("SELECT tenancy_ocid,compartment_id FROM oci_configs WHERE id=?", (cid,)).fetchone()
comp = compartment_id or cfg["compartment_id"] or cfg["tenancy_ocid"]
insts = compute.list_instances(comp).data
return [{"id":i.id,"display_name":i.display_name,"shape":i.shape,"lifecycle_state":i.lifecycle_state,"region":i.region,"time_created":str(i.time_created)} for i in insts]
except Exception as e:
return {"error": str(e)[:500]}
@app.get("/api/oci/explore/{cid}/databases")
async def explore_databases(cid: str, compartment_id: str = Query(None), u=Depends(current_user)):
try:
import oci
config = _get_oci_config(cid)
db_client = oci.database.DatabaseClient(config)
with db() as c:
cfg = c.execute("SELECT tenancy_ocid,compartment_id FROM oci_configs WHERE id=?", (cid,)).fetchone()
comp = compartment_id or cfg["compartment_id"] or cfg["tenancy_ocid"]
adbs = db_client.list_autonomous_databases(comp).data
return [{"id":a.id,"display_name":a.display_name,"db_name":a.db_name,"lifecycle_state":a.lifecycle_state,
"cpu_core_count":a.cpu_core_count,"data_storage_size_in_tbs":a.data_storage_size_in_tbs,
"is_free_tier":a.is_free_tier} for a in adbs]
except Exception as e:
return {"error": str(e)[:500]}
@app.get("/api/oci/explore/{cid}/buckets")
async def explore_buckets(cid: str, compartment_id: str = Query(None), u=Depends(current_user)):
try:
import oci
config = _get_oci_config(cid)
os_client = oci.object_storage.ObjectStorageClient(config)
namespace = os_client.get_namespace().data
with db() as c:
cfg = c.execute("SELECT tenancy_ocid,compartment_id FROM oci_configs WHERE id=?", (cid,)).fetchone()
comp = compartment_id or cfg["compartment_id"] or cfg["tenancy_ocid"]
buckets = os_client.list_buckets(namespace, comp).data
return [{"name":b.name,"namespace":b.namespace,"time_created":str(b.time_created)} for b in buckets]
except Exception as e:
return {"error": str(e)[:500]}
# ── OCI GenAI Config & Chat ───────────────────────────────────────────────────
@app.get("/api/genai/models")
async def list_genai_models(u=Depends(current_user)):
return {"models": GENAI_MODELS, "regions": GENAI_REGIONS}
@@ -387,15 +497,20 @@ async def list_genai_models(u=Depends(current_user)):
@app.post("/api/genai/config")
async def save_genai(req: GenAIConfigReq, u=Depends(require("admin","user"))):
gid = str(uuid.uuid4())
ep = req.endpoint or f"https://inference.generativeai.{req.genai_region}.oci.oraclecloud.com"
with db() as c:
if req.is_default:
c.execute("UPDATE genai_configs SET is_default=0 WHERE user_id=?", (u["id"],))
c.execute(
"INSERT INTO genai_configs (id,user_id,oci_config_id,model_id,compartment_id,genai_region,serving_type,endpoint_id,temperature,max_tokens,top_p,top_k,is_default) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
(gid, u["id"], req.oci_config_id, req.model_id, req.compartment_id, req.genai_region,
req.serving_type, req.endpoint_id, req.temperature, req.max_tokens, req.top_p, req.top_k, int(req.is_default)))
"""INSERT INTO genai_configs (id,user_id,name,oci_config_id,model_id,model_ocid,compartment_id,
genai_region,endpoint,serving_type,dedicated_endpoint_id,temperature,max_tokens,top_p,top_k,
frequency_penalty,presence_penalty,is_default) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(gid, u["id"], req.name, req.oci_config_id, req.model_id, req.model_ocid,
req.compartment_id, req.genai_region, ep, req.serving_type, req.dedicated_endpoint_id,
req.temperature, req.max_tokens, req.top_p, req.top_k,
req.frequency_penalty, req.presence_penalty, int(req.is_default)))
_audit(u["id"], u["username"], "save_genai_config", gid, req.model_id)
return {"id": gid, "model_id": req.model_id}
return {"id": gid, "model_id": req.model_id, "endpoint": ep}
@app.get("/api/genai/configs")
async def list_genai(u=Depends(current_user)):
@@ -411,79 +526,123 @@ async def del_genai(gid: str, u=Depends(require("admin","user"))):
@app.post("/api/genai/test/{gid}")
async def test_genai(gid: str, u=Depends(require("admin","user"))):
"""Test GenAI connection by sending a simple prompt"""
with db() as c:
gc = c.execute("SELECT g.*,o.key_file_path,o.tenancy_ocid,o.user_ocid,o.fingerprint FROM genai_configs g JOIN oci_configs o ON g.oci_config_id=o.id WHERE g.id=?",(gid,)).fetchone()
gc = c.execute("SELECT * FROM genai_configs WHERE id=?",(gid,)).fetchone()
if not gc: raise HTTPException(404)
try:
resp = await _call_genai(dict(gc), "Say 'connection successful' in one sentence.")
return {"status":"success","message":"GenAI OK","response":resp[:200]}
resp = _call_genai(dict(gc), "Say 'connection successful' in one short sentence.")
return {"status":"success","message":"GenAI OK","response":resp[:300]}
except Exception as e:
return {"status":"error","message":str(e)[:500]}
async def _call_genai(gc: dict, message: str, history: list = None) -> str:
"""Call OCI Generative AI chat endpoint using OCI SDK"""
try:
import oci
except ImportError:
return "❌ OCI SDK não instalado. Execute: pip install oci"
def _call_genai(gc: dict, message: str, history: list = None) -> str:
"""
Call OCI Generative AI using the exact SDK pattern from chat_demo.py.
Uses oci.generative_ai_inference with proper Message/TextContent objects.
"""
import oci
config_path = str(Path(gc["key_file_path"]).parent / "config")
try:
config = oci.config.from_file(config_path, "DEFAULT")
except Exception:
config = {
"user": gc["user_ocid"], "tenancy": gc["tenancy_ocid"],
"fingerprint": gc["fingerprint"], "key_file": gc["key_file_path"],
"region": gc["genai_region"]
}
# Load OCI config from stored credentials (same as ~/.oci/config)
config_path = str(OCI_DIR / gc["oci_config_id"] / "config")
config = oci.config.from_file(config_path, "DEFAULT")
endpoint = f"https://inference.generativeai.{gc['genai_region']}.oci.oraclecloud.com"
client = oci.generative_ai_inference.GenerativeAiInferenceClient(config, service_endpoint=endpoint)
# Service endpoint - built from region
endpoint = gc["endpoint"]
# Create inference client with retry strategy and timeout
generative_ai_inference_client = oci.generative_ai_inference.GenerativeAiInferenceClient(
config=config,
service_endpoint=endpoint,
retry_strategy=oci.retry.NoneRetryStrategy(),
timeout=(10, 240)
)
# Build ChatDetails
chat_detail = oci.generative_ai_inference.models.ChatDetails()
# Determine API format from model catalog
model_info = GENAI_MODELS.get(gc["model_id"], {})
api_format = model_info.get("api_format", "GENERIC")
if gc["serving_type"] == "DEDICATED" and gc.get("endpoint_id"):
serving_mode = oci.generative_ai_inference.models.DedicatedServingMode(endpoint_id=gc["endpoint_id"])
else:
serving_mode = oci.generative_ai_inference.models.OnDemandServingMode(model_id=gc["model_id"])
if api_format == "COHERE":
chat_request = oci.generative_ai_inference.models.CohereChatRequest(
message=message, max_tokens=gc.get("max_tokens", 2048),
temperature=gc.get("temperature", 0.7), top_p=gc.get("top_p", 0.9),
api_format="COHERE"
)
# ── Cohere models (CohereChatRequest) ──
chat_request = oci.generative_ai_inference.models.CohereChatRequest()
chat_request.api_format = oci.generative_ai_inference.models.BaseChatRequest.API_FORMAT_COHERE
chat_request.message = message
chat_request.max_tokens = int(gc.get("max_tokens", 6000))
chat_request.temperature = float(gc.get("temperature", 1))
chat_request.frequency_penalty = float(gc.get("frequency_penalty", 0))
chat_request.presence_penalty = float(gc.get("presence_penalty", 0))
chat_request.top_p = float(gc.get("top_p", 0.95))
chat_request.top_k = int(gc.get("top_k", 1))
if history:
chat_history = []
for h in history:
entry = oci.generative_ai_inference.models.CohereMessage()
entry.role = "USER" if h["role"] == "user" else "CHATBOT"
entry.message = h["content"]
chat_history.append(entry)
chat_request.chat_history = chat_history
else:
msgs = []
# ── Generic format (Meta Llama, Google, xAI) - exact chat_demo.py pattern ──
chat_request = oci.generative_ai_inference.models.GenericChatRequest()
chat_request.api_format = oci.generative_ai_inference.models.BaseChatRequest.API_FORMAT_GENERIC
chat_request.max_tokens = int(gc.get("max_tokens", 6000))
chat_request.temperature = float(gc.get("temperature", 1))
chat_request.frequency_penalty = float(gc.get("frequency_penalty", 0))
chat_request.presence_penalty = float(gc.get("presence_penalty", 0))
chat_request.top_p = float(gc.get("top_p", 0.95))
chat_request.top_k = int(gc.get("top_k", 1))
messages = []
if history:
for h in history:
role = "USER" if h["role"] == "user" else "ASSISTANT"
msgs.append(oci.generative_ai_inference.models.GenericChatMessage(
role=role, content=[oci.generative_ai_inference.models.TextContent(text=h["content"])]
))
msgs.append(oci.generative_ai_inference.models.GenericChatMessage(
role="USER", content=[oci.generative_ai_inference.models.TextContent(text=message)]
))
chat_request = oci.generative_ai_inference.models.GenericChatRequest(
messages=msgs, max_tokens=gc.get("max_tokens", 2048),
temperature=gc.get("temperature", 0.7), top_p=gc.get("top_p", 0.9),
api_format="GENERIC"
content = oci.generative_ai_inference.models.TextContent()
content.text = h["content"]
msg = oci.generative_ai_inference.models.Message()
msg.role = "USER" if h["role"] == "user" else "ASSISTANT"
msg.content = [content]
messages.append(msg)
# Current user message
content = oci.generative_ai_inference.models.TextContent()
content.text = message
user_message = oci.generative_ai_inference.models.Message()
user_message.role = "USER"
user_message.content = [content]
messages.append(user_message)
chat_request.messages = messages
# Serving mode - supports model_id or model_ocid
model_ref = gc.get("model_ocid") or gc["model_id"]
if gc.get("serving_type") == "DEDICATED" and gc.get("dedicated_endpoint_id"):
chat_detail.serving_mode = oci.generative_ai_inference.models.DedicatedServingMode(
endpoint_id=gc["dedicated_endpoint_id"]
)
else:
chat_detail.serving_mode = oci.generative_ai_inference.models.OnDemandServingMode(
model_id=model_ref
)
chat_detail = oci.generative_ai_inference.models.ChatDetails(
compartment_id=gc["compartment_id"], serving_mode=serving_mode, chat_request=chat_request
)
response = client.chat(chat_detail)
chat_response = response.data.chat_response
chat_detail.chat_request = chat_request
chat_detail.compartment_id = gc["compartment_id"]
# Execute
chat_response = generative_ai_inference_client.chat(chat_detail)
# Extract text from response
resp = chat_response.data.chat_response
if api_format == "COHERE":
return chat_response.text if hasattr(chat_response, 'text') else str(chat_response)
return resp.text if hasattr(resp, 'text') else str(resp)
else:
if hasattr(chat_response, 'choices') and chat_response.choices:
return chat_response.choices[0].message.content[0].text if chat_response.choices[0].message.content else ""
return str(chat_response)
if hasattr(resp, 'choices') and resp.choices:
choice = resp.choices[0]
if hasattr(choice, 'message') and choice.message and hasattr(choice.message, 'content'):
contents = choice.message.content
if contents and len(contents) > 0:
return contents[0].text
return str(resp)
# ── MCP Servers ───────────────────────────────────────────────────────────────
@app.post("/api/mcp/servers")
@@ -491,10 +650,11 @@ async def register_mcp(req: MCPServerReq, u=Depends(require("admin","user"))):
mid = str(uuid.uuid4())
with db() as c:
c.execute(
"INSERT INTO mcp_servers (id,user_id,name,description,server_type,command,args,env_vars,url,module_path) VALUES (?,?,?,?,?,?,?,?,?,?)",
"INSERT INTO mcp_servers (id,user_id,name,description,server_type,command,args,env_vars,url,module_path,tools,linked_adb_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
(mid, u["id"], req.name, req.description, req.server_type,
req.command, json.dumps(req.args) if req.args else None,
json.dumps(req.env_vars) if req.env_vars else None, req.url, req.module_path))
json.dumps(req.env_vars) if req.env_vars else None, req.url, req.module_path,
json.dumps(req.tools) if req.tools else None, req.linked_adb_id))
_audit(u["id"], u["username"], "register_mcp", mid, req.name)
return {"id": mid, "name": req.name, "server_type": req.server_type}
@@ -506,8 +666,10 @@ async def list_mcp(u=Depends(current_user)):
res = []
for r in rows:
d = dict(r)
if d.get("args"): d["args"] = json.loads(d["args"])
if d.get("env_vars"): d["env_vars"] = json.loads(d["env_vars"])
for k in ("args","env_vars","tools"):
if d.get(k):
try: d[k] = json.loads(d[k])
except: pass
res.append(d)
return res
@@ -526,14 +688,17 @@ async def toggle_mcp(mid: str, u=Depends(require("admin","user"))):
@app.post("/api/mcp/servers/{mid}/upload")
async def upload_mcp_file(mid: str, file: UploadFile = File(...), u=Depends(require("admin","user"))):
"""Upload a Python MCP server script"""
sdir = MCP_DIR / mid; sdir.mkdir(parents=True, exist_ok=True)
fp = sdir / file.filename
fp.write_bytes(await file.read())
with db() as c:
c.execute("UPDATE mcp_servers SET module_path=? WHERE id=?", (str(fp), mid))
with db() as c: c.execute("UPDATE mcp_servers SET module_path=? WHERE id=?", (str(fp), mid))
return {"ok": True, "path": str(fp)}
@app.put("/api/mcp/servers/{mid}/link-adb")
async def link_mcp_adb(mid: str, adb_id: str = Query(...), u=Depends(require("admin","user"))):
with db() as c: c.execute("UPDATE mcp_servers SET linked_adb_id=? WHERE id=?", (adb_id, mid))
return {"ok": True, "mcp_id": mid, "linked_adb_id": adb_id}
# ── ADB Vector DB Config ─────────────────────────────────────────────────────
@app.post("/api/adb/config")
async def save_adb(req: ADBVectorReq, u=Depends(require("admin","user"))):
@@ -580,7 +745,7 @@ async def test_adb(vid: str, u=Depends(require("admin","user"))):
cur = conn.cursor(); cur.execute("SELECT 1 FROM DUAL"); cur.close(); conn.close()
return {"status":"success","message":"Conexão Autonomous DB OK!"}
except ImportError:
return {"status":"error","message":"python-oracledb não instalado. Execute: pip install oracledb"}
return {"status":"error","message":"python-oracledb não disponível no container."}
except Exception as e:
return {"status":"error","message":str(e)[:500]}
@@ -614,7 +779,13 @@ async def _exec_report(rid, cfg, regions, mcp_server_id):
if mcp_server_id:
with db() as c:
mcp = c.execute("SELECT * FROM mcp_servers WHERE id=?",(mcp_server_id,)).fetchone()
if mcp and mcp["module_path"]: cmd += ["--mcp-module", mcp["module_path"]]
if mcp:
if mcp["module_path"]: cmd += ["--mcp-module", mcp["module_path"]]
if mcp.get("linked_adb_id"):
adb_cfg = c.execute("SELECT * FROM adb_vector_configs WHERE id=?",(mcp["linked_adb_id"],)).fetchone()
if adb_cfg:
cmd += ["--adb-dsn", adb_cfg["dsn"], "--adb-user", adb_cfg["username"]]
if adb_cfg.get("wallet_dir"): cmd += ["--adb-wallet", adb_cfg["wallet_dir"]]
proc = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
stdout, stderr = await proc.communicate()
jp = rdir / "report.json"; hp = rdir / "report.html"
@@ -668,36 +839,30 @@ async def chat(msg: ChatMsg, u=Depends(current_user)):
sid = msg.session_id or str(uuid.uuid4())
with db() as c:
c.execute("INSERT INTO chat_messages (id,session_id,user_id,role,content,model_id) VALUES (?,?,?,?,?,?)",
(str(uuid.uuid4()), sid, u["id"], "user", msg.message, msg.model_id))
(str(uuid.uuid4()), sid, u["id"], "user", msg.message, None))
genai_cfg = None
if msg.genai_config_id:
with db() as c:
genai_cfg = c.execute(
"SELECT g.*,o.key_file_path,o.tenancy_ocid,o.user_ocid,o.fingerprint FROM genai_configs g JOIN oci_configs o ON g.oci_config_id=o.id WHERE g.id=?",
(msg.genai_config_id,)).fetchone()
elif msg.model_id:
with db() as c:
genai_cfg = c.execute(
"SELECT g.*,o.key_file_path,o.tenancy_ocid,o.user_ocid,o.fingerprint FROM genai_configs g JOIN oci_configs o ON g.oci_config_id=o.id WHERE g.user_id=? AND g.model_id=? LIMIT 1",
(u["id"], msg.model_id)).fetchone()
genai_cfg = c.execute("SELECT * FROM genai_configs WHERE id=?", (msg.genai_config_id,)).fetchone()
if genai_cfg:
try:
history = []
with db() as c:
prev = c.execute("SELECT role,content FROM chat_messages WHERE session_id=? ORDER BY created_at ASC", (sid,)).fetchall()
prev = c.execute("SELECT role,content FROM chat_messages WHERE session_id=? AND role IN ('user','assistant') ORDER BY created_at ASC", (sid,)).fetchall()
history = [{"role":r["role"],"content":r["content"]} for r in prev]
resp = await _call_genai(dict(genai_cfg), msg.message, history[:-1] if history else None)
resp = _call_genai(dict(genai_cfg), msg.message, history[:-1] if len(history) > 1 else None)
except Exception as e:
resp = f"❌ Erro GenAI: {str(e)[:300]}"
resp = f"❌ Erro GenAI: {str(e)[:400]}"
else:
resp = _agent_respond(msg.message, u)
mid = genai_cfg["model_id"] if genai_cfg else None
with db() as c:
c.execute("INSERT INTO chat_messages (id,session_id,user_id,role,content,model_id) VALUES (?,?,?,?,?,?)",
(str(uuid.uuid4()), sid, u["id"], "assistant", resp, msg.model_id))
return {"session_id": sid, "response": resp, "model_id": msg.model_id}
(str(uuid.uuid4()), sid, u["id"], "assistant", resp, mid))
return {"session_id": sid, "response": resp, "model_id": mid}
def _agent_respond(msg, user):
m = msg.lower().strip()
@@ -709,7 +874,7 @@ def _agent_respond(msg, user):
nu=c.execute("SELECT COUNT(*) n FROM users WHERE is_active=1").fetchone()["n"]
nm=c.execute("SELECT COUNT(*) n FROM mcp_servers WHERE is_active=1").fetchone()["n"]
ng=c.execute("SELECT COUNT(*) n FROM genai_configs").fetchone()["n"]
return (f"📊 **Status do Sistema**\n\n• OCI CLI: {cli}\n• Configs OCI: {nc}\n• GenAI Models: {ng}\n• MCP Servers: {nm}\n• Relatórios: {nr}\n• Usuários ativos: {nu}\n• Servidor: ✅ Online")
return (f"📊 **Status do Sistema**\n\n• OCI CLI: {cli}\n• Configs OCI: {nc}\n• GenAI Models: {ng}\n• MCP Servers: {nm}\n• Relatórios: {nr}\n• Usuários ativos: {nu}\n• Servidor: ✅ Online\n• Versão: v{VERSION}")
if any(k in m for k in ["ajuda","help","comandos"]):
return ("🤖 **Comandos disponíveis:**\n\n• `status` — Status do sistema\n• `listar configs` — Configurações OCI\n• `verificar cis` — Checks CIS 3.0\n• `modelos` — Modelos GenAI disponíveis\n• `ajuda` — Esta mensagem\n\n💡 Configure um modelo GenAI para chat com IA.")
if any(k in m for k in ["modelo","modelos","genai"]):
@@ -720,7 +885,7 @@ def _agent_respond(msg, user):
return "\n".join(lines)
if any(k in m for k in ["cis","benchmark","checks","verificar"]):
return ("🔒 **CIS OCI Foundations Benchmark 3.0**\n\n54 controles em 8 domínios:\n• IAM: 17 controles\n• Networking: 8 controles\n• Compute: 3 controles\n• Logging & Monitoring: 18 controles\n• Storage: 6 controles\n• Asset Management: 2 controles\n\nConfigure OCI e execute um relatório na aba **Report**.")
return ("Sou o **OCI CIS AI Agent**. Sem modelo GenAI configurado, uso respostas locais.\n\n"
return ("Sou o **OCI CIS AI Agent v" + VERSION + "**. Sem modelo GenAI configurado, uso respostas locais.\n\n"
"Para chat com IA:\n1. Configure **OCI Credentials**\n2. Configure **GenAI** com modelo e região\n3. Selecione o modelo no chat\n\nDigite **ajuda** para ver os comandos.")
@app.delete("/api/chat/{sid}")
@@ -758,12 +923,12 @@ async def audit_log(limit:int=Query(100,le=500), u=Depends(require("admin"))):
# ── Health ────────────────────────────────────────────────────────────────────
@app.get("/api/health")
async def health():
return {"status":"ok","ts":datetime.utcnow().isoformat(),"version":"2.0.0"}
return {"status":"ok","ts":datetime.utcnow().isoformat(),"version":VERSION}
@app.on_event("startup")
async def startup():
init_db()
log.info("OCI CIS AI Agent v2 API started")
log.info(f"OCI CIS AI Agent v{VERSION} API started")
if __name__ == "__main__":
import uvicorn

View File

@@ -9,6 +9,7 @@ services:
- APP_SECRET=${APP_SECRET:-change-me-in-production-use-a-long-random-string}
- JWT_EXPIRY_HOURS=${JWT_EXPIRY_HOURS:-12}
- DATA_DIR=/data
- OCI_CLI_SUPPRESS_FILE_PERMISSIONS_WARNING=True
volumes:
- agent-data:/data
ports:

View File

@@ -14,7 +14,6 @@
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:var(--fs);background:var(--bg0);color:var(--t1);min-height:100vh}
.app{display:flex;min-height:100vh}
/* ── Sidebar ── */
.sb{width:250px;background:var(--bg1);border-right:1px solid var(--bd);display:flex;flex-direction:column;position:fixed;top:0;left:0;bottom:0;z-index:100}
.sb-h{padding:1.15rem 1rem;border-bottom:1px solid var(--bd);background:linear-gradient(135deg,#c74634 0%,#a53727 100%)}
.sb-h h1{font-size:.88rem;font-weight:700;color:#fff;font-family:var(--fm);letter-spacing:-.02em;display:flex;align-items:center;gap:.4rem}
@@ -28,66 +27,59 @@ body{font-family:var(--fs);background:var(--bg0);color:var(--t1);min-height:100v
.ui{display:flex;align-items:center;gap:.5rem}
.ua{width:28px;height:28px;background:var(--ac);border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:.7rem;color:#fff}
.un{font-weight:600;color:var(--t1);font-size:.78rem}.ur{color:var(--tm);font-size:.63rem;font-family:var(--fm)}
/* ── Main ── */
.mc{flex:1;margin-left:250px;min-height:100vh}
.tb{height:50px;background:var(--bg1);border-bottom:1px solid var(--bd);display:flex;align-items:center;justify-content:space-between;padding:0 1.15rem;position:sticky;top:0;z-index:50}
.tb-t{font-weight:600;font-size:.88rem;color:var(--t1)}.tb-a{display:flex;align-items:center;gap:.5rem}
.pc{padding:1.15rem}
.cd{background:var(--bg1);border:1px solid var(--bd);border-radius:var(--rl);padding:1.15rem;margin-bottom:.65rem;box-shadow:var(--sh)}
.ct{font-weight:600;font-size:.88rem;margin-bottom:.65rem;display:flex;align-items:center;gap:.35rem;color:var(--t1)}
/* ── Buttons ── */
.btn{display:inline-flex;align-items:center;gap:.35rem;padding:.42rem .8rem;border-radius:var(--r);font-size:.76rem;font-weight:600;cursor:pointer;border:1px solid transparent;transition:all .15s;font-family:var(--fs)}
.bp{background:var(--ac);color:#fff;border-color:var(--ac)}.bp:hover{background:var(--ah)}
.bs{background:var(--bg1);color:var(--t2);border-color:var(--bd)}.bs:hover{border-color:var(--tm);color:var(--t1)}
.bd{background:var(--rdl);color:var(--rd);border-color:rgba(199,70,52,.2)}.bd:hover{background:var(--rd);color:#fff}
.bsm{padding:.28rem .55rem;font-size:.7rem}
.btn:disabled{opacity:.5;cursor:not-allowed}
/* ── Form ── */
.ig{margin-bottom:.65rem}.ig label{display:block;font-size:.76rem;font-weight:600;color:var(--t2);margin-bottom:.2rem}
.ig .ht{font-size:.65rem;color:var(--tm);margin-bottom:.15rem}
input[type=text],input[type=password],input[type=email],input[type=number],select,textarea{width:100%;padding:.45rem .6rem;background:var(--bg1);border:1px solid var(--bd);border-radius:var(--r);color:var(--t1);font-size:.78rem;font-family:var(--fm);transition:border .15s}
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--ac);box-shadow:0 0 0 2px rgba(199,70,52,.12)}
input[type=file]{font-family:var(--fm);font-size:.73rem;color:var(--t2)}
/* ── Badges ── */
.badge{padding:.15rem .5rem;border-radius:12px;font-size:.66rem;font-weight:600;font-family:var(--fm)}
.b-pass{background:var(--gnl);color:var(--gn)}.b-fail{background:var(--rdl);color:var(--rd)}
.b-pend{background:var(--yll);color:var(--yl)}.b-rev{background:var(--bll);color:var(--bl)}
.b-admin{background:var(--ppl);color:var(--pp)}.b-user{background:var(--bll);color:var(--bl)}.b-viewer{background:var(--bg3);color:var(--tm)}
.b-on{background:var(--gnl);color:var(--gn)}.b-off{background:var(--bg3);color:var(--tm)}
/* ── Table ── */
table{width:100%;border-collapse:collapse;font-size:.76rem}
th{text-align:left;padding:.5rem .6rem;font-size:.65rem;text-transform:uppercase;letter-spacing:.05em;color:var(--tm);border-bottom:2px solid var(--bd);font-weight:700;background:var(--bg2)}
td{padding:.5rem .6rem;border-bottom:1px solid var(--bg3);color:var(--t2)}
tr:hover td{background:var(--acl)}
.g2{display:grid;grid-template-columns:1fr 1fr;gap:.65rem}
.g3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:.65rem}
/* ── Chat ── */
.ch-c{display:flex;flex-direction:column;height:calc(100vh - 50px - 2.3rem)}
.ch-m{flex:1;overflow-y:auto;padding:.65rem;background:var(--bg2);border:1px solid var(--bd);border-radius:var(--rl) var(--rl) 0 0}
.cm{margin-bottom:.65rem;display:flex;gap:.5rem}
.cm-u{justify-content:flex-end}
.cm-u .cb{background:var(--ac);color:#fff;border-radius:var(--rl) var(--rl) 4px var(--rl);border-color:var(--ac)}
.cm{margin-bottom:.65rem;display:flex;gap:.5rem}.cm-user{justify-content:flex-end}
.cm-user .cb{background:var(--ac);color:#fff;border-radius:var(--rl) var(--rl) 4px var(--rl);border-color:var(--ac)}
.cb{max-width:75%;padding:.55rem .75rem;border-radius:var(--rl) var(--rl) var(--rl) 4px;background:var(--bg1);font-size:.8rem;line-height:1.55;border:1px solid var(--bd);white-space:pre-wrap;box-shadow:var(--sh)}
.cb code{font-family:var(--fm);font-size:.73rem;background:var(--bg3);padding:.1rem .2rem;border-radius:3px}
.cb strong{color:var(--ac)}
.ch-i{display:flex;gap:.35rem;padding:.55rem;background:var(--bg1);border:1px solid var(--bd);border-top:none;border-radius:0 0 var(--rl) var(--rl)}
.ch-i input{flex:1}
.ch-t{display:flex;gap:.35rem;align-items:center;padding:.35rem .55rem;background:var(--bg2);border:1px solid var(--bd);border-bottom:none;border-radius:var(--rl) var(--rl) 0 0;font-size:.7rem}
.ch-t{display:flex;gap:.35rem;align-items:center;padding:.35rem .55rem;background:var(--bg2);border:1px solid var(--bd);border-bottom:none;border-radius:var(--rl) var(--rl) 0 0;font-size:.7rem;flex-wrap:wrap}
.ch-t select{max-width:240px;font-size:.72rem;padding:.25rem .4rem}
.ch-t label{font-weight:600;color:var(--tm);font-size:.68rem}
/* ── Report iframe ── */
.rif{width:100%;height:calc(100vh - 50px - 5.5rem);border:1px solid var(--bd);border-radius:var(--rl);background:#fff}
/* ── Login ── */
.lp{min-height:100vh;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#fef2f0 0%,#f0f2f5 50%,#e6f1fa 100%)}
.lc{background:var(--bg1);border:1px solid var(--bd);border-radius:var(--rl);padding:2rem;width:100%;max-width:370px;box-shadow:0 4px 24px rgba(0,0,0,.08)}
.lc h2{font-size:1.1rem;font-family:var(--fm);color:var(--ac)}.lc .st{color:var(--tm);font-size:.76rem;margin-bottom:1.15rem}
/* ── Alerts ── */
.al{padding:.55rem .75rem;border-radius:var(--r);font-size:.76rem;margin-bottom:.65rem}
.al-e{background:var(--rdl);color:var(--rd);border:1px solid rgba(199,70,52,.2)}
.al-s{background:var(--gnl);color:var(--gn);border:1px solid rgba(13,138,65,.2)}
.al-i{background:var(--bll);color:var(--bl);border:1px solid rgba(11,114,185,.2)}
.tag{display:inline-block;padding:.1rem .35rem;background:var(--acl);color:var(--ac);border-radius:4px;font-size:.65rem;font-family:var(--fm);font-weight:500}
.emp{text-align:center;padding:2rem;color:var(--tm)}.emp .eic{font-size:2rem;margin-bottom:.4rem}
.exp-r{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:.5rem;margin-top:.5rem}
.exp-c{background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r);padding:.65rem;font-size:.76rem}
.exp-c strong{color:var(--t1);font-size:.8rem}.exp-c .em{font-family:var(--fm);font-size:.63rem;color:var(--tm);word-break:break-all}
@keyframes fi{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.fi{animation:fi .2s ease}
@keyframes pu{0%,100%{opacity:1}50%{opacity:.5}}.ld{animation:pu 1.5s infinite;color:var(--tm);font-size:.8rem;text-align:center;padding:1.2rem}
::-webkit-scrollbar{width:5px}::-webkit-scrollbar-track{background:var(--bg0)}::-webkit-scrollbar-thumb{background:var(--bd);border-radius:3px}
@@ -97,7 +89,8 @@ tr:hover td{background:var(--acl)}
<body>
<div id="app"></div>
<script>
const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],genaiCfg:[],adbCfg:[],mcpSvr:[],users:[],auditLogs:[],models:{},regions:[],selModel:'',selGenai:''};
const V='1.1';
const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],genaiCfg:[],adbCfg:[],mcpSvr:[],users:[],auditLogs:[],models:{},regions:[],selGenai:'',expData:null};
const API='/api';
async function $api(p,o={}){const h={...(o.headers||{})};if(S.token)h['Authorization']='Bearer '+S.token;
if(o.body&&!(o.body instanceof FormData)){h['Content-Type']='application/json';o.body=JSON.stringify(o.body)}
@@ -112,14 +105,14 @@ async function loadData(){try{
[S.ociCfg,S.reports,S.genaiCfg,S.mcpSvr]=await Promise.all([$api('/oci/configs'),$api('/reports'),$api('/genai/configs'),$api('/mcp/servers')]);
try{const m=await $api('/genai/models');S.models=m.models;S.regions=m.regions}catch(e){}
try{S.adbCfg=await $api('/adb/configs')}catch(e){S.adbCfg=[]}
if(S.user.role==='admin'){S.users=await $api('/users')}
if(S.user.role==='admin'){try{S.users=await $api('/users')}catch(e){}}
}catch(e){console.error(e)}}
function R(){document.getElementById('app').innerHTML=S.user?rApp():rLogin();bind()}
function switchTab(t){S.tab=t;R();if(t==='audit'&&S.user.role==='admin')loadAudit();if(t==='downloads')refreshDl()}
function switchTab(t){S.tab=t;S.expData=null;R();if(t==='audit'&&S.user.role==='admin')loadAudit();if(t==='downloads')refreshDl()}
function rLogin(){return`<div class="lp"><div class="lc fi">
<div style="text-align:center;margin-bottom:1.15rem"><div style="font-size:1.6rem;margin-bottom:.3rem">🛡️</div>
<h2>OCI CIS Agent</h2><div class="st">Oracle Cloud Infrastructure · Security</div></div>
<div style="text-align:center;margin-bottom:1.15rem"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="48" height="48"><rect x="2" y="5" width="32" height="26" rx="13" ry="13" fill="rgba(199,70,52,0.1)" stroke="#C74634" stroke-width="2"/><rect x="11" y="11" width="14" height="14" rx="4" fill="#fff" stroke="#C74634" stroke-width="0.5"/><circle cx="15" cy="17" r="2" fill="#C74634"/><circle cx="21" cy="17" r="2" fill="#C74634"/><circle cx="15" cy="16.7" r="0.7" fill="#fff"/><circle cx="21" cy="16.7" r="0.7" fill="#fff"/><rect x="14" y="21" width="8" height="1.5" rx="0.75" fill="#C74634" opacity="0.6"/><line x1="18" y1="11" x2="18" y2="6" stroke="#C74634" stroke-width="1.2" stroke-linecap="round"/><circle cx="18" cy="5.5" r="1.5" fill="#C74634" opacity="0.3" stroke="#C74634" stroke-width="0.8"/><line x1="11" y1="18" x2="6" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.5"/><line x1="25" y1="18" x2="30" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.5"/><circle cx="5.5" cy="18" r="1" fill="#C74634" opacity="0.4"/><circle cx="30.5" cy="18" r="1" fill="#C74634" opacity="0.4"/></svg>
<h2>OCI CIS Agent</h2><div class="st">Oracle Cloud Infrastructure · Security · v${V}</div></div>
<div id="le"></div>
<div id="mfa-s" style="display:none"><div class="al al-i">MFA obrigatório. Insira o código do autenticador.</div>
<div class="ig"><label>Código MFA</label><input type="text" id="mfa-c" placeholder="000000" maxlength="6" style="text-align:center;font-size:1rem;letter-spacing:.25em"></div>
@@ -131,11 +124,11 @@ function rLogin(){return`<div class="lp"><div class="lc fi">
function rApp(){return`<div class="app">${rSb()}<div class="mc">${rTb()}<div class="pc fi" id="pg">${rPg()}</div></div></div>`}
function rSb(){
const tabs=[['chat','💬','Chat Agent'],['report','📊','Report'],['downloads','📁','Downloads']];
const tabs=[['chat','💬','Chat Agent'],['explorer','🔍','OCI Explorer'],['report','📊','Report'],['downloads','📁','Downloads']];
const ctabs=[['oci-config','☁️','OCI Credentials'],['genai','🧠','GenAI Config'],['mcp','🔌','MCP Servers'],['adb','🗄️','ADB Vector']];
const atabs=[['users','👥','Usuários'],['mfa','🔐','MFA'],['audit','📋','Audit Log']];
const i=(S.user?.username||'?')[0].toUpperCase();
return`<div class="sb"><div class="sb-h"><h1>🛡️ OCI CIS Agent</h1><div class="st">Infrastructure & Security Engineer</div></div>
return`<div class="sb"><div class="sb-h"><h1><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="28" height="28" style="vertical-align:middle;flex-shrink:0"><rect x="2" y="5" width="32" height="26" rx="13" ry="13" fill="rgba(255,255,255,0.15)" stroke="#fff" stroke-width="2"/><rect x="11" y="11" width="14" height="14" rx="4" fill="#fff" opacity="0.95"/><circle cx="15" cy="17" r="2" fill="#C74634"/><circle cx="21" cy="17" r="2" fill="#C74634"/><circle cx="15" cy="16.7" r="0.7" fill="#fff"/><circle cx="21" cy="16.7" r="0.7" fill="#fff"/><rect x="14" y="21" width="8" height="1.5" rx="0.75" fill="#C74634" opacity="0.6"/><line x1="18" y1="11" x2="18" y2="6" stroke="#fff" stroke-width="1.2" stroke-linecap="round"/><circle cx="18" cy="5.5" r="1.5" fill="#fff" opacity="0.9"/><line x1="11" y1="18" x2="6" y2="18" stroke="#fff" stroke-width="1" stroke-linecap="round" opacity="0.7"/><line x1="25" y1="18" x2="30" y2="18" stroke="#fff" stroke-width="1" stroke-linecap="round" opacity="0.7"/><circle cx="5.5" cy="18" r="1" fill="#fff" opacity="0.7"/><circle cx="30.5" cy="18" r="1" fill="#fff" opacity="0.7"/></svg> OCI CIS Agent</h1><div class="st">Infrastructure & Security · v${V}</div></div>
<div class="nav"><div class="nl">Principal</div>
${tabs.map(t=>`<div class="ni ${S.tab===t[0]?'on':''}" onclick="switchTab('${t[0]}')"><span class="ic">${t[1]}</span>${t[2]}</div>`).join('')}
<div class="nl" style="margin-top:.5rem">Configuração</div>
@@ -145,16 +138,16 @@ ${atabs.map(t=>`<div class="ni ${S.tab===t[0]?'on':''}" onclick="switchTab('${t[
<div class="sb-f"><div class="ui"><div class="ua">${i}</div><div><div class="un">${S.user?.username}</div><div class="ur">${S.user?.role}</div></div>
<button class="btn bs bsm" style="margin-left:auto" onclick="doLogout()" title="Sair">⏻</button></div></div></div>`}
function rTb(){const t={'chat':'💬 AI Agent Chat','report':'📊 Compliance Report','downloads':'📁 Downloads','oci-config':'☁️ OCI Credentials','genai':'🧠 OCI Generative AI','mcp':'🔌 MCP Servers','adb':'🗄️ Autonomous DB Vector','users':'👥 Usuários','mfa':'🔐 MFA','audit':'📋 Audit Log'};
return`<div class="tb"><div class="tb-t">${t[S.tab]||''}</div><div class="tb-a"><span class="tag">v2.0</span></div></div>`}
function rTb(){const t={'chat':'💬 AI Agent Chat','explorer':'🔍 OCI Account Explorer','report':'📊 Compliance Report','downloads':'📁 Downloads','oci-config':'☁️ OCI Credentials','genai':'🧠 OCI Generative AI','mcp':'🔌 MCP Servers','adb':'🗄️ Autonomous DB Vector','users':'👥 Usuários','mfa':'🔐 MFA','audit':'📋 Audit Log'};
return`<div class="tb"><div class="tb-t">${t[S.tab]||''}</div><div class="tb-a"><span class="tag">v${V}</span></div></div>`}
function rPg(){switch(S.tab){case'chat':return rChat();case'report':return rReport();case'downloads':return rDl();case'oci-config':return rOci();case'genai':return rGenAI();case'mcp':return rMCP();case'adb':return rADB();case'users':return rUsers();case'mfa':return rMfa();case'audit':return rAudit();default:return''}}
function rPg(){switch(S.tab){case'chat':return rChat();case'explorer':return rExplorer();case'report':return rReport();case'downloads':return rDl();case'oci-config':return rOci();case'genai':return rGenAI();case'mcp':return rMCP();case'adb':return rADB();case'users':return rUsers();case'mfa':return rMfa();case'audit':return rAudit();default:return''}}
// ── Chat ──
function rChat(){
const ms=S.msgs.length===0?'<div class="emp"><div class="eic">🤖</div><p>Envie uma mensagem para iniciar.</p><p style="font-size:.72rem;margin-top:.3rem">Selecione um modelo GenAI acima ou use o agente local.</p></div>'
:S.msgs.map(m=>`<div class="cm cm-${m.r}"><div class="cb">${fm(m.c)}</div></div>`).join('');
const gcOpts=S.genaiCfg.map(g=>{const mi=S.models[g.model_id];return`<option value="${g.id}" ${S.selGenai===g.id?'selected':''}>${mi?mi.name:g.model_id} (${g.genai_region})</option>`}).join('');
const gcOpts=S.genaiCfg.map(g=>{const mi=S.models[g.model_id];return`<option value="${g.id}" ${S.selGenai===g.id?'selected':''}>${g.name||mi?.name||g.model_id} (${g.genai_region})</option>`}).join('');
return`<div class="ch-c">
<div class="ch-t"><label>🧠 Modelo:</label><select id="gsel" onchange="S.selGenai=this.value"><option value="">Agente Local (sem IA)</option>${gcOpts}</select>
<div style="flex:1"></div><button class="btn bd bsm" onclick="clrChat()">Limpar</button></div>
@@ -170,20 +163,40 @@ async function sChat(){const el=document.getElementById('chi');const m=el.value.
function scCh(){setTimeout(()=>{const e=document.getElementById('chm');if(e)e.scrollTop=e.scrollHeight},50)}
async function clrChat(){if(S.sid)try{await $api('/chat/'+S.sid,{method:'DELETE'})}catch(e){}S.msgs=[];S.sid=null;R()}
// ── OCI Explorer ──
function rExplorer(){
const cfgSel=S.ociCfg.map(c=>`<option value="${c.id}">${c.tenancy_name} (${c.region})</option>`).join('');
if(!S.ociCfg.length)return`<div class="cd"><div class="emp"><div class="eic">☁️</div><p>Nenhuma credencial OCI configurada.</p><p style="font-size:.72rem;margin-top:.3rem">Vá em <strong>OCI Credentials</strong> para adicionar.</p></div></div>`;
return`<div class="cd"><div class="ct">🔍 Explorar Conta OCI</div>
<div class="g3"><div class="ig"><label>Conexão OCI</label><select id="exCfg">${cfgSel}</select></div>
<div class="ig"><label>Recurso</label><select id="exRes"><option value="compartments">Compartments</option><option value="regions">Regions</option><option value="instances">Compute Instances</option><option value="vcns">VCNs</option><option value="databases">Autonomous DBs</option><option value="buckets">Object Storage Buckets</option></select></div>
<div class="ig"><label>&nbsp;</label><button class="btn bp" onclick="doExplore()">🔍 Explorar</button></div></div></div>
<div class="cd"><div class="ct" id="exTitle">Resultados</div><div id="exOut">${S.expData?renderExpData(S.expData):'<div class="emp"><p>Selecione uma conexão e recurso acima.</p></div>'}</div></div>`}
async function doExplore(){const cid=document.getElementById('exCfg').value;const res=document.getElementById('exRes').value;
document.getElementById('exOut').innerHTML='<div class="ld">Consultando OCI...</div>';
try{const d=await $api('/oci/explore/'+cid+'/'+res);S.expData=d;
document.getElementById('exTitle').textContent='Resultados: '+res;
document.getElementById('exOut').innerHTML=renderExpData(d)}catch(e){document.getElementById('exOut').innerHTML='<div class="al al-e">'+e.message+'</div>'}}
function renderExpData(d){if(d.error)return`<div class="al al-e">${d.error}</div>`;if(!Array.isArray(d)||!d.length)return'<div class="emp"><p>Nenhum resultado.</p></div>';
return`<div style="font-size:.72rem;color:var(--tm);margin-bottom:.4rem">${d.length} item(ns)</div><div class="exp-r">${d.map(i=>{
const keys=Object.keys(i).filter(k=>k!=='id');
const name=i.display_name||i.name||i.db_name||'—';
return`<div class="exp-c"><strong>${name}</strong>${i.lifecycle_state?` <span class="badge ${i.lifecycle_state==='RUNNING'||i.lifecycle_state==='ACTIVE'||i.lifecycle_state==='AVAILABLE'?'b-pass':'b-pend'}">${i.lifecycle_state}</span>`:''}<br>
${keys.filter(k=>!['display_name','name','db_name','lifecycle_state'].includes(k)).map(k=>`<span style="color:var(--tm);font-size:.65rem">${k}:</span> <span class="em">${typeof i[k]==='object'?JSON.stringify(i[k]):i[k]}</span>`).join('<br>')}</div>`}).join('')}</div>`}
// ── Report ──
function rReport(){const comp=S.reports.filter(r=>r.status==='completed');
if(!comp.length)return`<div class="cd"><div class="ct">📊 Report</div><div class="emp"><div class="eic">📄</div><p>Nenhum relatório.</p></div></div>${rRunRpt()}`;
const lt=comp[0];
return`<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.4rem"><span style="font-size:.76rem;color:var(--tm)">Report HTML — ${lt.tenancy_name}</span>
<div style="display:flex;gap:.35rem"><select id="rsel" onchange="ldRpt(this.value)" style="max-width:260px">
${comp.map(r=>`<option value="${r.id}">${r.tenancy_name}${r.created_at}</option>`).join('')}</select></div></div>
<select id="rsel" onchange="ldRpt(this.value)" style="max-width:260px">${comp.map(r=>`<option value="${r.id}">${r.tenancy_name}${r.created_at}</option>`).join('')}</select></div>
<iframe class="rif" id="rfr" src="${API}/reports/${lt.id}/html"></iframe>${rRunRpt()}`}
function rRunRpt(){if(S.user?.role==='viewer')return'';
return`<div class="cd" style="margin-top:.65rem"><div class="ct">🚀 Executar Relatório</div>
<div class="g3"><div class="ig"><label>Config OCI</label><select id="rc"><option value="">Selecione...</option>
${S.ociCfg.map(c=>`<option value="${c.id}">${c.tenancy_name} (${c.region})</option>`).join('')}</select></div>
<div class="ig"><label>MCP Server (opcional)</label><select id="rmcp"><option value="">Padrão (stub)</option>
${S.mcpSvr.filter(m=>m.is_active).map(m=>`<option value="${m.id}">${m.name}</option>`).join('')}</select></div>
${S.mcpSvr.filter(m=>m.is_active).map(m=>`<option value="${m.id}">${m.name}${m.linked_adb_id?' 🗄️':''}</option>`).join('')}</select></div>
<div class="ig"><label>Regiões (opcional)</label><input type="text" id="rr" placeholder="sa-saopaulo-1, us-ashburn-1"></div></div>
<button class="btn bp" onclick="runRpt()">▶ Executar CIS Compliance</button></div>`}
function ldRpt(id){const f=document.getElementById('rfr');if(f)f.src=API+'/reports/'+id+'/html'}
@@ -196,17 +209,15 @@ async function runRpt(){const c=document.getElementById('rc').value;if(!c)return
// ── Downloads ──
function rDl(){return`<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.4rem">
<span style="font-size:.76rem;color:var(--tm)">${S.reports.length} relatório(s)</span>
<div style="display:flex;gap:.35rem"><input type="text" id="dlf" placeholder="Filtrar..." style="max-width:240px" oninput="fDl()">
<button class="btn bp bsm" onclick="refreshDl()">🔄</button></div></div>
<button class="btn bp bsm" onclick="refreshDl()">🔄</button></div>
<div class="cd"><table><thead><tr><th>Tenancy</th><th>Status</th><th>Criado</th><th>Concluído</th><th>Ações</th></tr></thead>
<tbody id="dlt">${S.reports.map(r=>`<tr data-n="${r.tenancy_name.toLowerCase()}"><td><strong>${r.tenancy_name}</strong></td>
<tbody>${S.reports.map(r=>`<tr><td><strong>${r.tenancy_name}</strong></td>
<td><span class="badge ${r.status==='completed'?'b-pass':r.status==='failed'?'b-fail':'b-pend'}">${r.status}</span></td>
<td>${r.created_at||'—'}</td><td>${r.completed_at||'—'}</td>
<td>${r.status==='completed'?`<button class="btn bs bsm" onclick="dlRpt('${r.id}','json')">JSON</button> <button class="btn bs bsm" onclick="dlRpt('${r.id}','html')">HTML</button>`:'—'}</td></tr>`).join('')}</tbody></table>
${!S.reports.length?'<div class="emp"><div class="eic">📂</div><p>Nenhum relatório.</p></div>':''}</div>`}
function dlRpt(id,f){window.open(API+'/reports/'+id+'/download?fmt='+f,'_blank')}
async function refreshDl(){S.reports=await $api('/reports');R()}
function fDl(){const q=document.getElementById('dlf').value.toLowerCase();document.querySelectorAll('#dlt tr').forEach(tr=>{tr.style.display=tr.dataset.n?.includes(q)?'':'none'})}
// ── OCI Config ──
function rOci(){return`<div class="cd"><div class="ct">☁️ Credenciais OCI</div>
@@ -234,69 +245,92 @@ async function sOci(){const fd=new FormData();fd.append('tenancy_name',document.
try{await $api('/oci/config',{method:'POST',body:fd,headers:{}});S.ociCfg=await $api('/oci/configs');sm('om','✅ Salvo!','s');R()}catch(e){sm('om',e.message,'e')}}
async function tOci(id){try{const d=await $api('/oci/test/'+id,{method:'POST'});alert(d.status==='success'?'✅ Conexão OK!':'❌ '+d.message)}catch(e){alert(e.message)}}
async function dOci(id){if(!confirm('Excluir?'))return;await $api('/oci/configs/'+id,{method:'DELETE'});S.ociCfg=await $api('/oci/configs');R()}
// ── GenAI Config ──
function rGenAI(){const mOpts=Object.entries(S.models).map(([k,v])=>`<option value="${k}">${v.name} (${v.provider})</option>`).join('');
const rOpts=S.regions.map(r=>`<option value="${r}">${r}</option>`).join('');
const rOpts=S.regions.map(r=>`<option value="${r}" ${r==='us-ashburn-1'?'selected':''}>${r}</option>`).join('');
return`<div class="cd"><div class="ct">🧠 Configurações GenAI</div>
<table><thead><tr><th>Modelo</th><th>Provider</th><th>Região</th><th>Serving</th><th>Default</th><th>Ações</th></tr></thead><tbody>
${S.genaiCfg.map(g=>{const mi=S.models[g.model_id]||{};return`<tr><td><strong>${mi.name||g.model_id}</strong><br><span style="font-size:.63rem;color:var(--tm)">${g.model_id}</span></td>
<td><span class="tag">${mi.provider||'?'}</span></td><td>${g.genai_region}</td><td>${g.serving_type}</td>
<td>${g.is_default?'⭐':'—'}</td>
<table><thead><tr><th>Nome</th><th>Modelo</th><th>Endpoint</th><th>Temp</th><th>Max Tokens</th><th>Ações</th></tr></thead><tbody>
${S.genaiCfg.map(g=>{const mi=S.models[g.model_id]||{};return`<tr><td><strong>${g.name||'—'}</strong>${g.is_default?' ':''}<br><span class="tag">${mi.provider||'?'}</span></td>
<td>${mi.name||g.model_id}<br><span style="font-size:.6rem;color:var(--tm)">${g.model_ocid||g.model_id}</span></td>
<td style="font-size:.63rem;font-family:var(--fm)">${g.endpoint||'—'}</td>
<td>${g.temperature}</td><td>${g.max_tokens}</td>
<td><button class="btn bs bsm" onclick="tGenai('${g.id}')">Testar</button> <button class="btn bd bsm" onclick="dGenai('${g.id}')">Excluir</button></td></tr>`}).join('')}</tbody></table>
${!S.genaiCfg.length?'<div class="emp" style="padding:.65rem"><p>Nenhum modelo configurado. Adicione abaixo.</p></div>':''}</div>
<div class="cd"><div class="ct"> Novo Modelo GenAI</div><div id="gm"></div>
<div class="g3"><div class="ig"><label>Config OCI (credenciais)</label><select id="goci"><option value="">Selecione...</option>
${!S.genaiCfg.length?'<div class="emp" style="padding:.65rem"><p>Nenhum modelo configurado.</p></div>':''}</div>
<div class="cd"><div class="ct"> Novo Modelo GenAI</div>
<p style="font-size:.72rem;color:var(--tm);margin-bottom:.65rem">Configura conexão via <strong>OCI SDK</strong> com <code>GenerativeAiInferenceClient</code>. Endpoint é gerado automaticamente pela região.</p>
<div id="gm"></div>
<div class="g3"><div class="ig"><label>Nome da Config</label><input type="text" id="gname" placeholder="Llama Produção"></div>
<div class="ig"><label>Credencial OCI</label><select id="goci"><option value="">Selecione...</option>
${S.ociCfg.map(c=>`<option value="${c.id}">${c.tenancy_name}</option>`).join('')}</select></div>
<div class="ig"><label>Modelo</label><select id="gmod">${mOpts}</select></div>
<div class="ig"><label>Região GenAI</label><select id="greg">${rOpts}</select></div></div>
<div class="g3"><div class="ig"><label>Compartment OCID</label><input type="text" id="gcmp" placeholder="ocid1.compartment.oc1.."></div>
<div class="ig"><label>Serving Type</label><select id="gsrv"><option value="ON_DEMAND">On-Demand</option><option value="DEDICATED">Dedicated</option></select></div>
<div class="ig"><label>Endpoint OCID (dedicado)</label><input type="text" id="gep" placeholder="(opcional)"></div></div>
<div class="g3"><div class="ig"><label>Temperature</label><input type="number" id="gtemp" value="0.7" step="0.1" min="0" max="2"></div>
<div class="ig"><label>Max Tokens</label><input type="number" id="gmt" value="2048" min="1" max="128000"></div>
<div class="ig"><label>Top P</label><input type="number" id="gtp" value="0.9" step="0.05" min="0" max="1"></div></div>
<div class="ig"><label>Modelo (catálogo)</label><select id="gmod">${mOpts}</select></div></div>
<div class="g3"><div class="ig"><label>Região GenAI</label><select id="greg">${rOpts}</select></div>
<div class="ig"><label>Compartment OCID</label><input type="text" id="gcmp" placeholder="ocid1.compartment.oc1.."></div>
<div class="ig"><label>Model OCID (opcional)</label><div class="ht">Se diferente do catálogo (ex: modelo dedicado)</div><input type="text" id="gocid" placeholder="ocid1.generativeaimodel.oc1..."></div></div>
<div style="font-size:.72rem;font-weight:600;color:var(--t2);margin:.5rem 0 .3rem">Parâmetros do Chat</div>
<div class="g3"><div class="ig"><label>Temperature</label><input type="number" id="gtemp" value="1" step="0.1" min="0" max="2"></div>
<div class="ig"><label>Max Tokens</label><input type="number" id="gmt" value="6000" min="1" max="128000"></div>
<div class="ig"><label>Top P</label><input type="number" id="gtp" value="0.95" step="0.05" min="0" max="1"></div></div>
<div class="g3"><div class="ig"><label>Top K</label><input type="number" id="gtk" value="1" min="-1" max="500"></div>
<div class="ig"><label>Frequency Penalty</label><input type="number" id="gfp" value="0" step="0.1" min="0" max="2"></div>
<div class="ig"><label>Presence Penalty</label><input type="number" id="gpp" value="0" step="0.1" min="0" max="2"></div></div>
<button class="btn bp" onclick="sGenai()">Salvar Modelo</button></div>`}
async function sGenai(){try{await $api('/genai/config',{method:'POST',body:{
name:document.getElementById('gname').value||'default',
oci_config_id:document.getElementById('goci').value,model_id:document.getElementById('gmod').value,
model_ocid:document.getElementById('gocid').value||null,
compartment_id:document.getElementById('gcmp').value,genai_region:document.getElementById('greg').value,
serving_type:document.getElementById('gsrv').value,endpoint_id:document.getElementById('gep').value||null,
endpoint:null,
serving_type:'ON_DEMAND',dedicated_endpoint_id:null,
temperature:parseFloat(document.getElementById('gtemp').value),max_tokens:parseInt(document.getElementById('gmt').value),
top_p:parseFloat(document.getElementById('gtp').value),is_default:S.genaiCfg.length===0}});
top_p:parseFloat(document.getElementById('gtp').value),top_k:parseInt(document.getElementById('gtk').value),
frequency_penalty:parseFloat(document.getElementById('gfp').value),presence_penalty:parseFloat(document.getElementById('gpp').value),
is_default:S.genaiCfg.length===0}});
S.genaiCfg=await $api('/genai/configs');sm('gm','✅ Modelo salvo!','s');R()}catch(e){sm('gm',e.message,'e')}}
async function tGenai(id){try{const d=await $api('/genai/test/'+id,{method:'POST'});alert(d.status==='success'?'✅ '+d.message+'\n'+d.response:'❌ '+d.message)}catch(e){alert(e.message)}}
async function dGenai(id){if(!confirm('Excluir?'))return;await $api('/genai/configs/'+id,{method:'DELETE'});S.genaiCfg=await $api('/genai/configs');R()}
// ── MCP Servers ──
function rMCP(){return`<div class="cd"><div class="ct">🔌 MCP Servers Registrados</div>
<table><thead><tr><th>Nome</th><th>Tipo</th><th>Comando/URL</th><th>Status</th><th>Ações</th></tr></thead><tbody>
${S.mcpSvr.map(m=>`<tr><td><strong>${m.name}</strong><br><span style="font-size:.63rem;color:var(--tm)">${m.description||''}</span></td>
function rMCP(){const adbOpts=S.adbCfg.map(a=>`<option value="${a.id}">${a.config_name} (${a.dsn})</option>`).join('');
return`<div class="cd"><div class="ct">🔌 MCP Servers</div>
<table><thead><tr><th>Nome</th><th>Tipo</th><th>Cmd/URL</th><th>ADB Link</th><th>Status</th><th>Ações</th></tr></thead><tbody>
${S.mcpSvr.map(m=>{const adb=m.linked_adb_id?S.adbCfg.find(a=>a.id===m.linked_adb_id):null;
return`<tr><td><strong>${m.name}</strong><br><span style="font-size:.63rem;color:var(--tm)">${m.description||''}</span></td>
<td><span class="tag">${m.server_type}</span></td>
<td style="font-family:var(--fm);font-size:.65rem">${m.command||m.url||m.module_path||'—'}</td>
<td>${adb?`<span class="badge b-pass">${adb.config_name}</span>`:'<span style="color:var(--tm);font-size:.65rem">—</span>'}</td>
<td><span class="badge ${m.is_active?'b-on':'b-off'}">${m.is_active?'Ativo':'Inativo'}</span></td>
<td><button class="btn bs bsm" onclick="tgMcp('${m.id}')">${m.is_active?'Desativar':'Ativar'}</button>
<button class="btn bd bsm" onclick="dMcp('${m.id}')">Excluir</button></td></tr>`).join('')}</tbody></table>
${!S.mcpSvr.length?'<div class="emp" style="padding:.65rem"><p>Nenhum MCP server. Registre abaixo para usar em relatórios.</p></div>':''}</div>
<td><button class="btn bs bsm" onclick="tgMcp('${m.id}')">${m.is_active?'Off':'On'}</button>
<button class="btn bd bsm" onclick="dMcp('${m.id}')">Del</button></td></tr>`}).join('')}</tbody></table>
${!S.mcpSvr.length?'<div class="emp" style="padding:.65rem"><p>Nenhum MCP server.</p></div>':''}</div>
<div class="cd"><div class="ct"> Registrar MCP Server</div><div id="mcm"></div>
<div class="g2"><div class="ig"><label>Nome</label><input type="text" id="mn" placeholder="CIS Benchmark Server"></div>
<div class="ig"><label>Descrição</label><input type="text" id="md" placeholder="Executa checks CIS 3.0"></div></div>
<div class="g3"><div class="ig"><label>Tipo</label><select id="mt"><option value="stdio">stdio</option><option value="sse">SSE (HTTP)</option><option value="module">Python Module</option></select></div>
<div class="ig"><label>Comando (stdio)</label><input type="text" id="mcmd" placeholder="python3 server.py"></div>
<div class="ig"><label>URL (SSE)</label><input type="text" id="murl" placeholder="http://localhost:8001/sse"></div></div>
<div class="ig"><label>Args (JSON array)</label><input type="text" id="margs" placeholder='["--config", "/path"]'></div>
<div class="g2"><div class="ig"><label>🗄️ Vincular ADB Vector (tool)</label><div class="ht">MCP server usará este banco como ferramenta</div><select id="madb"><option value="">Nenhum</option>${adbOpts}</select></div>
<div class="ig"><label>Args (JSON array)</label><input type="text" id="margs" placeholder='["--config", "/path"]'></div></div>
<button class="btn bp" onclick="sMcp()">Registrar Server</button></div>
<div class="cd"><div class="ct">📤 Upload Script MCP</div>
<div class="g2"><div class="ig"><label>MCP Server</label><select id="mup">${S.mcpSvr.map(m=>`<option value="${m.id}">${m.name}</option>`).join('')}</select></div>
<div class="ig"><label>Arquivo .py</label><input type="file" id="muf" accept=".py"></div></div>
<div class="cd"><div class="ct">📤 Upload Script / 🔗 Link ADB</div>
<div class="g3"><div class="ig"><label>MCP Server</label><select id="mup">${S.mcpSvr.map(m=>`<option value="${m.id}">${m.name}</option>`).join('')}</select></div>
<div class="ig"><label>Arquivo .py</label><input type="file" id="muf" accept=".py"></div>
<div class="ig"><label>Vincular ADB</label><select id="mlnk"><option value="">Nenhum</option>${adbOpts}</select>
<button class="btn bs bsm" style="margin-top:.3rem" onclick="lnkMcp()">Vincular</button></div></div>
<button class="btn bs" onclick="uMcp()">Upload Script</button></div>`}
async function sMcp(){try{let args=null;const a=document.getElementById('margs').value;if(a)try{args=JSON.parse(a)}catch(e){}
await $api('/mcp/servers',{method:'POST',body:{name:document.getElementById('mn').value,description:document.getElementById('md').value,
server_type:document.getElementById('mt').value,command:document.getElementById('mcmd').value||null,url:document.getElementById('murl').value||null,args}});
server_type:document.getElementById('mt').value,command:document.getElementById('mcmd').value||null,url:document.getElementById('murl').value||null,args,
linked_adb_id:document.getElementById('madb').value||null}});
S.mcpSvr=await $api('/mcp/servers');sm('mcm','✅ Registrado!','s');R()}catch(e){sm('mcm',e.message,'e')}}
async function tgMcp(id){await $api('/mcp/servers/'+id+'/toggle',{method:'PUT'});S.mcpSvr=await $api('/mcp/servers');R()}
async function dMcp(id){if(!confirm('Excluir?'))return;await $api('/mcp/servers/'+id,{method:'DELETE'});S.mcpSvr=await $api('/mcp/servers');R()}
async function uMcp(){const fd=new FormData();const f=document.getElementById('muf').files[0];if(!f)return alert('Selecione um arquivo');
fd.append('file',f);const id=document.getElementById('mup').value;if(!id)return alert('Selecione um server');
try{await $api('/mcp/servers/'+id+'/upload',{method:'POST',body:fd,headers:{}});alert('Upload OK!')}catch(e){alert(e.message)}}
async function lnkMcp(){const mid=document.getElementById('mup').value;const aid=document.getElementById('mlnk').value;if(!mid)return alert('Selecione MCP');
try{await $api('/mcp/servers/'+mid+'/link-adb?adb_id='+(aid||''),{method:'PUT'});S.mcpSvr=await $api('/mcp/servers');alert('Vinculado!');R()}catch(e){alert(e.message)}}
// ── ADB Vector ──
function rADB(){return`<div class="cd"><div class="ct">🗄️ Conexões Autonomous Database (Vector)</div>
@@ -307,13 +341,12 @@ ${S.adbCfg.map(c=>`<tr><td><strong>${c.config_name}</strong></td><td style="font
<td><button class="btn bs bsm" onclick="tAdb('${c.id}')">Testar</button> <button class="btn bd bsm" onclick="dAdb('${c.id}')">Excluir</button></td></tr>`).join('')}</tbody></table>
${!S.adbCfg.length?'<div class="emp" style="padding:.65rem"><p>Nenhuma conexão ADB.</p></div>':''}</div>
<div class="cd"><div class="ct"> Nova Conexão Autonomous DB</div>
<p style="font-size:.76rem;color:var(--tm);margin-bottom:.65rem">Conexão via <strong>python-oracledb</strong> (Thin mode) com Wallet (mTLS) para Oracle Autonomous Database.</p>
<div id="am"></div>
<p style="font-size:.72rem;color:var(--tm);margin-bottom:.65rem">Conexão via <strong>python-oracledb</strong> Thin mode com Wallet (mTLS) para Oracle Autonomous Database.</p><div id="am"></div>
<div class="g2"><div class="ig"><label>Nome da Conexão</label><input type="text" id="an" placeholder="Produção ADB"></div>
<div class="ig"><label>DSN (TNS Name)</label><div class="ht">Ex: myatp_high, myatp_medium (do tnsnames.ora)</div><input type="text" id="adsn" placeholder="myatp_high"></div>
<div class="ig"><label>DSN (TNS Name)</label><div class="ht">Ex: myatp_high, myatp_medium</div><input type="text" id="adsn" placeholder="myatp_high"></div>
<div class="ig"><label>Username</label><input type="text" id="auser" placeholder="ADMIN"></div>
<div class="ig"><label>Password</label><input type="password" id="apw" placeholder="••••••••"></div>
<div class="ig"><label>Wallet Password</label><div class="ht">Senha do ewallet.pem (definida ao baixar wallet)</div><input type="password" id="awpw" placeholder="(opcional)"></div>
<div class="ig"><label>Password</label><input type="password" id="apw"></div>
<div class="ig"><label>Wallet Password</label><input type="password" id="awpw" placeholder="(opcional)"></div>
<div class="ig"><label>Tabela de Embeddings</label><input type="text" id="atbl" value="CIS_EMBEDDINGS"></div></div>
<button class="btn bp" onclick="sAdb()">Salvar Conexão</button></div>
<div class="cd"><div class="ct">📤 Upload Wallet (.zip)</div>

21
logo.svg Normal file
View File

@@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="128" height="128">
<!-- Oracle-style rounded rectangle -->
<rect x="2" y="5" width="32" height="26" rx="13" ry="13" fill="rgba(199,70,52,0.08)" stroke="#C74634" stroke-width="2"/>
<!-- Robot head -->
<rect x="11" y="11" width="14" height="14" rx="4" fill="#fff" stroke="#C74634" stroke-width="0.5"/>
<!-- Eyes -->
<circle cx="15" cy="17" r="2" fill="#C74634"/>
<circle cx="21" cy="17" r="2" fill="#C74634"/>
<circle cx="15" cy="16.7" r="0.7" fill="#fff"/>
<circle cx="21" cy="16.7" r="0.7" fill="#fff"/>
<!-- Mouth -->
<rect x="14" y="21" width="8" height="1.5" rx="0.75" fill="#C74634" opacity="0.6"/>
<!-- Antenna -->
<line x1="18" y1="11" x2="18" y2="6" stroke="#C74634" stroke-width="1.2" stroke-linecap="round"/>
<circle cx="18" cy="5.5" r="1.5" fill="#C74634" opacity="0.3" stroke="#C74634" stroke-width="0.8"/>
<!-- Ears/signal -->
<line x1="11" y1="18" x2="6" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.5"/>
<line x1="25" y1="18" x2="30" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.5"/>
<circle cx="5.5" cy="18" r="1" fill="#C74634" opacity="0.4"/>
<circle cx="30.5" cy="18" r="1" fill="#C74634" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB