diff --git a/README.md b/README.md index d979ed0..8f3f746 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@
-
+
@@ -32,7 +32,7 @@ The platform combines security compliance scanning, AI-powered chat with **RAG (
### 🤖 AI Chat Agent with RAG
- **OCI Generative AI** integration via official SDK (`oci.generative_ai_inference`)
- **RAG (Retrieval-Augmented Generation)**: automatically queries ADB vector store for relevant context before generating responses
-- 12 models across 4 providers: **Cohere** (Command A/R/R+), **Meta** (Llama 4/3.3/3.2/3.1), **Google** (Gemini 2.5), **xAI** (Grok 3/4)
+- 14 models across 5 providers: **Cohere** (Command A/R/R+), **Meta** (Llama 4/3.3/3.2/3.1), **Google** (Gemini 2.5), **xAI** (Grok 3/4), **OpenAI** (GPT-oss 120B/20B)
- 16 OCI regions supported with auto-generated endpoints
- Full parameter control: temperature, max_tokens, top_p, top_k, frequency/presence penalty
- Conversation history with session management
@@ -309,6 +309,7 @@ oci-cis-agent/
|--------|----------|-------------|
| POST | `/api/oci/config` | Save OCI credentials (multipart) |
| GET | `/api/oci/configs` | List OCI credentials |
+| PUT | `/api/oci/configs/{id}` | Update OCI credential (multipart) |
| POST | `/api/oci/test/{id}` | Test OCI connection |
| DELETE | `/api/oci/configs/{id}` | Delete OCI credential |
@@ -330,6 +331,7 @@ oci-cis-agent/
| GET | `/api/genai/models` | List available models and regions |
| POST | `/api/genai/config` | Save GenAI configuration |
| GET | `/api/genai/configs` | List GenAI configurations |
+| PUT | `/api/genai/configs/{id}` | Update GenAI configuration |
| POST | `/api/genai/test/{id}` | Test GenAI connection |
| DELETE | `/api/genai/configs/{id}` | Delete GenAI config |
@@ -339,6 +341,7 @@ oci-cis-agent/
|--------|----------|-------------|
| POST | `/api/mcp/servers` | Register MCP server |
| GET | `/api/mcp/servers` | List MCP servers |
+| PUT | `/api/mcp/servers/{id}` | Update MCP server |
| PUT | `/api/mcp/servers/{id}/toggle` | Activate/deactivate |
| POST | `/api/mcp/servers/{id}/upload` | Upload script file |
| PUT | `/api/mcp/servers/{id}/link-adb` | Link to ADB Vector |
@@ -350,6 +353,8 @@ oci-cis-agent/
|--------|----------|-------------|
| POST | `/api/adb/config` | Save ADB connection (with GenAI config + embedding model) |
| GET | `/api/adb/configs` | List ADB connections |
+| PUT | `/api/adb/configs/{id}` | Update ADB connection (multipart) |
+| POST | `/api/adb/parse-wallet` | Parse wallet ZIP and extract DSN names |
| POST | `/api/adb/{id}/upload-wallet` | Upload wallet ZIP |
| POST | `/api/adb/test/{id}` | Test ADB connection |
| POST | `/api/adb/{id}/ensure-table` | Create embeddings table in ADB |
@@ -410,6 +415,8 @@ oci-cis-agent/
| Google | Gemini 2.5 Flash | GENERIC |
| xAI | Grok 4 | GENERIC |
| xAI | Grok 3 | GENERIC |
+| OpenAI | GPT-oss (120B) | GENERIC |
+| OpenAI | GPT-oss (20B) | GENERIC |
### Embedding Models
@@ -419,6 +426,7 @@ oci-cis-agent/
| Cohere | Embed Multilingual v3.0 | 1024 |
| Cohere | Embed English Light v3.0 | 384 |
| Cohere | Embed Multilingual Light v3.0 | 384 |
+| Cohere | Embed v4.0 (Multimodal) | 1536 |
### GenAI Regions
@@ -446,6 +454,7 @@ oci-cis-agent/
| Version | Date | Changes |
|---------|------|---------|
+| **v1.4** | 2026-03 | In-place config editing (OCI, GenAI, ADB, MCP), OpenAI GPT-oss models (120B/20B), Cohere Embed v4.0 multimodal, wallet auto-parse with DSN extraction |
| **v1.3** | 2026-03 | Persistent config logs per tab, GenAI auto-fill from OCI credentials, inline UX feedback with loading spinners, MCP type-switch fix, encrypted key passphrase support, Docker stdin hang fix |
| **v1.2** | 2026-03 | RAG pipeline (OCI GenAI embeddings + ADB vector search), dedicated Embeddings tab, CIS report chunking, file upload embedding |
| **v1.1** | 2025-02 | OCI SDK GenAI (exact pattern), OCI Account Explorer, MCP↔ADB linking, full chat parameters |
diff --git a/backend/app.py b/backend/app.py
index 7d05291..534f582 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -58,6 +58,8 @@ GENAI_MODELS = {
"google.gemini-2.5-flash": {"provider":"google","name":"Google Gemini 2.5 Flash","api_format":"GENERIC"},
"xai.grok-4": {"provider":"xai","name":"xAI Grok 4","api_format":"GENERIC"},
"xai.grok-3": {"provider":"xai","name":"xAI Grok 3","api_format":"GENERIC"},
+ "openai.gpt-oss-120b": {"provider":"openai","name":"OpenAI GPT-oss (120B)","api_format":"GENERIC"},
+ "openai.gpt-oss-20b": {"provider":"openai","name":"OpenAI GPT-oss (20B)","api_format":"GENERIC"},
}
EMBEDDING_MODELS = {
@@ -65,6 +67,7 @@ EMBEDDING_MODELS = {
"cohere.embed-multilingual-v3.0": {"name":"Cohere Embed Multilingual v3.0","dims":1024},
"cohere.embed-english-light-v3.0": {"name":"Cohere Embed English Light v3.0","dims":384},
"cohere.embed-multilingual-light-v3.0": {"name":"Cohere Embed Multilingual Light v3.0","dims":384},
+ "cohere.embed-v4.0": {"name":"Cohere Embed v4.0 (Multimodal)","dims":1536},
}
GENAI_REGIONS = [
@@ -426,8 +429,9 @@ async def save_oci(
@app.get("/api/oci/configs")
async def list_oci(u=Depends(current_user)):
with db() as c:
- if u["role"]=="admin": rows=c.execute("SELECT id,user_id,tenancy_name,tenancy_ocid,region,compartment_id,created_at FROM oci_configs").fetchall()
- else: rows=c.execute("SELECT id,user_id,tenancy_name,tenancy_ocid,region,compartment_id,created_at FROM oci_configs WHERE user_id=?",(u["id"],)).fetchall()
+ cols = "id,user_id,tenancy_name,tenancy_ocid,user_ocid,fingerprint,region,compartment_id,created_at"
+ if u["role"]=="admin": rows=c.execute(f"SELECT {cols} FROM oci_configs").fetchall()
+ else: rows=c.execute(f"SELECT {cols} FROM oci_configs WHERE user_id=?",(u["id"],)).fetchall()
return [dict(r) for r in rows]
@app.delete("/api/oci/configs/{cid}")
@@ -441,6 +445,43 @@ async def del_oci(cid: str, u=Depends(require("admin","user"))):
if d.exists(): shutil.rmtree(d)
return {"ok": True}
+@app.put("/api/oci/configs/{cid}")
+async def update_oci(
+ cid: str,
+ tenancy_name: str = Form(...), tenancy_ocid: str = Form(...),
+ user_ocid: str = Form(...), fingerprint: str = Form(...),
+ region: str = Form(...), compartment_id: str = Form(""),
+ key_passphrase: str = Form(""),
+ private_key: Optional[UploadFile] = File(None), public_key: Optional[UploadFile] = File(None),
+ u = Depends(require("admin","user"))
+):
+ with db() as c:
+ existing = c.execute("SELECT * FROM oci_configs WHERE id=?", (cid,)).fetchone()
+ if not existing: raise HTTPException(404)
+ if u["role"] != "admin" and existing["user_id"] != u["id"]: raise HTTPException(403)
+ cdir = OCI_DIR / cid; cdir.mkdir(parents=True, exist_ok=True)
+ kp = cdir / "oci_api_key.pem"
+ if private_key and private_key.filename:
+ key_bytes = await private_key.read()
+ kp.write_bytes(key_bytes); kp.chmod(0o600)
+ key_text = key_bytes.decode("utf-8", errors="ignore")
+ if ("ENCRYPTED" in key_text or "Proc-Type: 4,ENCRYPTED" in key_text) and not key_passphrase:
+ raise HTTPException(400, "A chave privada está criptografada (ENCRYPTED). Informe a Key Passphrase.")
+ if public_key and public_key.filename:
+ (cdir / "oci_api_key_public.pem").write_bytes(await public_key.read())
+ cfg_file = cdir / "config"
+ cfg_content = (f"[DEFAULT]\nuser={user_ocid}\nfingerprint={fingerprint}\n"
+ f"tenancy={tenancy_ocid}\nregion={region}\nkey_file={kp}\n")
+ if key_passphrase:
+ cfg_content += f"pass_phrase={key_passphrase}\n"
+ cfg_file.write_text(cfg_content); cfg_file.chmod(0o600)
+ with db() as c:
+ c.execute("UPDATE oci_configs SET tenancy_name=?,tenancy_ocid=?,user_ocid=?,fingerprint=?,region=?,compartment_id=? WHERE id=?",
+ (tenancy_name, tenancy_ocid, user_ocid, fingerprint, region, compartment_id or None, cid))
+ _audit(u["id"], u["username"], "update_oci_config", cid, f"tenancy={tenancy_name}")
+ _config_log("oci", cid, tenancy_name, "success", "save", f"Credencial atualizada: {tenancy_name} ({region})", u["id"], u["username"])
+ return {"id": cid, "tenancy_name": tenancy_name, "region": region}
+
@app.post("/api/oci/test/{cid}")
async def test_oci(cid: str, u=Depends(require("admin","user"))):
cp = OCI_DIR / cid / "config"
@@ -595,6 +636,28 @@ async def del_genai(gid: str, u=Depends(require("admin","user"))):
with db() as c: c.execute("DELETE FROM genai_configs WHERE id=?", (gid,))
return {"ok": True}
+@app.put("/api/genai/configs/{gid}")
+async def update_genai(gid: str, req: GenAIConfigReq, u=Depends(require("admin","user"))):
+ with db() as c:
+ existing = c.execute("SELECT * FROM genai_configs WHERE id=?", (gid,)).fetchone()
+ if not existing: raise HTTPException(404)
+ if u["role"] != "admin" and existing["user_id"] != u["id"]: raise HTTPException(403)
+ 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(
+ """UPDATE genai_configs SET 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=? WHERE 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), gid))
+ _audit(u["id"], u["username"], "update_genai_config", gid, req.model_id)
+ _config_log("genai", gid, req.name, "success", "save", f"Modelo atualizado: {req.model_id} ({req.genai_region})", u["id"], u["username"])
+ return {"id": gid, "model_id": req.model_id, "endpoint": ep}
+
@app.post("/api/genai/test/{gid}")
async def test_genai(gid: str, u=Depends(require("admin","user"))):
with db() as c:
@@ -878,6 +941,23 @@ async def del_mcp(mid: str, u=Depends(require("admin","user"))):
with db() as c: c.execute("DELETE FROM mcp_servers WHERE id=?", (mid,))
return {"ok": True}
+@app.put("/api/mcp/servers/{mid}")
+async def update_mcp(mid: str, req: MCPServerReq, u=Depends(require("admin","user"))):
+ with db() as c:
+ existing = c.execute("SELECT * FROM mcp_servers WHERE id=?", (mid,)).fetchone()
+ if not existing: raise HTTPException(404)
+ if u["role"] != "admin" and existing["user_id"] != u["id"]: raise HTTPException(403)
+ with db() as c:
+ c.execute(
+ "UPDATE mcp_servers SET name=?,description=?,server_type=?,command=?,args=?,env_vars=?,url=?,tools=?,linked_adb_id=? WHERE 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,
+ json.dumps(req.tools) if req.tools else None, req.linked_adb_id, mid))
+ _audit(u["id"], u["username"], "update_mcp", mid, req.name)
+ _config_log("mcp", mid, req.name, "success", "save", f"MCP atualizado: {req.name} ({req.server_type})", u["id"], u["username"])
+ return {"id": mid, "name": req.name, "server_type": req.server_type}
+
@app.put("/api/mcp/servers/{mid}/toggle")
async def toggle_mcp(mid: str, u=Depends(require("admin","user"))):
with db() as c:
@@ -900,18 +980,57 @@ async def link_mcp_adb(mid: str, adb_id: str = Query(...), u=Depends(require("ad
return {"ok": True, "mcp_id": mid, "linked_adb_id": adb_id}
# ── ADB Vector DB Config ─────────────────────────────────────────────────────
+@app.post("/api/adb/parse-wallet")
+async def parse_wallet(wallet: UploadFile = File(...), u=Depends(require("admin","user"))):
+ """Extract DSN names from tnsnames.ora inside wallet ZIP (temporary parse, no save)."""
+ import zipfile, tempfile
+ try:
+ data = await wallet.read()
+ with tempfile.TemporaryDirectory() as tmp:
+ zp = Path(tmp) / "wallet.zip"
+ zp.write_bytes(data)
+ with zipfile.ZipFile(str(zp), 'r') as z:
+ z.extractall(tmp)
+ tns = Path(tmp) / "tnsnames.ora"
+ if not tns.exists():
+ raise HTTPException(400, "Wallet ZIP não contém tnsnames.ora")
+ tns_text = tns.read_text(errors="ignore")
+ dsn_names = re.findall(r'^(\w[\w\-.]*)\s*=', tns_text, re.MULTILINE)
+ if not dsn_names:
+ raise HTTPException(400, "Nenhum DSN encontrado no tnsnames.ora")
+ return {"dsn_names": dsn_names, "files": [n for n in os.listdir(tmp) if n != "wallet.zip"]}
+ except zipfile.BadZipFile:
+ raise HTTPException(400, "Arquivo não é um ZIP válido")
+
@app.post("/api/adb/config")
-async def save_adb(req: ADBVectorReq, u=Depends(require("admin","user"))):
+async def save_adb(
+ config_name: str = Form(...), dsn: str = Form(...), username: str = Form(...),
+ password: str = Form(...), wallet_password: str = Form(""),
+ table_name: str = Form("CIS_EMBEDDINGS"), use_mtls: str = Form("true"),
+ genai_config_id: str = Form(""), embedding_model_id: str = Form("cohere.embed-multilingual-v3.0"),
+ wallet: Optional[UploadFile] = File(None),
+ u=Depends(require("admin","user"))
+):
vid = str(uuid.uuid4())
+ use_mtls_bool = use_mtls.lower() in ("true", "1", "yes")
with db() as c:
c.execute(
"INSERT INTO adb_vector_configs (id,user_id,config_name,dsn,username,password_enc,wallet_password_enc,table_name,use_mtls,genai_config_id,embedding_model_id) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
- (vid, u["id"], req.config_name, req.dsn, req.username, _enc(req.password),
- _enc(req.wallet_password) if req.wallet_password else None, req.table_name, int(req.use_mtls),
- req.genai_config_id, req.embedding_model_id))
- _audit(u["id"], u["username"], "save_adb_config", vid, req.config_name)
- _config_log("adb", vid, req.config_name, "success", "save", f"Conexão salva: {req.config_name} ({req.dsn})", u["id"], u["username"])
- return {"id": vid, "config_name": req.config_name}
+ (vid, u["id"], config_name, dsn, username, _enc(password),
+ _enc(wallet_password) if wallet_password else None, table_name, int(use_mtls_bool),
+ genai_config_id or None, embedding_model_id))
+ # Auto-save wallet if provided
+ if wallet and wallet.filename:
+ import zipfile
+ wdir = WALLET_DIR / vid; wdir.mkdir(parents=True, exist_ok=True)
+ zp = wdir / "wallet.zip"
+ zp.write_bytes(await wallet.read())
+ with zipfile.ZipFile(str(zp), 'r') as z: z.extractall(str(wdir))
+ with db() as c: c.execute("UPDATE adb_vector_configs SET wallet_dir=? WHERE id=?", (str(wdir), vid))
+ _config_log("adb", vid, config_name, "success", "upload", f"Wallet enviada com a conexão", u["id"], u["username"])
+ _audit(u["id"], u["username"], "save_adb_config", vid, config_name)
+ _config_log("adb", vid, config_name, "success", "save", f"Conexão salva: {config_name} ({dsn})", u["id"], u["username"])
+ return {"id": vid, "config_name": config_name}
@app.post("/api/adb/{vid}/upload-wallet")
async def upload_wallet(vid: str, wallet: UploadFile = File(...), u=Depends(require("admin","user"))):
@@ -967,6 +1086,44 @@ async def del_adb(vid: str, u=Depends(require("admin","user"))):
if d.exists(): shutil.rmtree(d)
return {"ok": True}
+@app.put("/api/adb/configs/{vid}")
+async def update_adb(
+ vid: str,
+ config_name: str = Form(...), dsn: str = Form(...), username: str = Form(...),
+ password: str = Form(""), wallet_password: str = Form(""),
+ table_name: str = Form("CIS_EMBEDDINGS"), use_mtls: str = Form("true"),
+ genai_config_id: str = Form(""), embedding_model_id: str = Form("cohere.embed-multilingual-v3.0"),
+ wallet: Optional[UploadFile] = File(None),
+ u=Depends(require("admin","user"))
+):
+ with db() as c:
+ existing = c.execute("SELECT * FROM adb_vector_configs WHERE id=?", (vid,)).fetchone()
+ if not existing: raise HTTPException(404)
+ if u["role"] != "admin" and existing["user_id"] != u["id"]: raise HTTPException(403)
+ use_mtls_bool = use_mtls.lower() in ("true", "1", "yes")
+ sets = "config_name=?,dsn=?,username=?,table_name=?,use_mtls=?,genai_config_id=?,embedding_model_id=?"
+ vals = [config_name, dsn, username, table_name, int(use_mtls_bool), genai_config_id or None, embedding_model_id]
+ if password:
+ sets += ",password_enc=?"
+ vals.append(_enc(password))
+ if wallet_password:
+ sets += ",wallet_password_enc=?"
+ vals.append(_enc(wallet_password))
+ vals.append(vid)
+ with db() as c:
+ c.execute(f"UPDATE adb_vector_configs SET {sets} WHERE id=?", vals)
+ if wallet and wallet.filename:
+ import zipfile
+ wdir = WALLET_DIR / vid; wdir.mkdir(parents=True, exist_ok=True)
+ zp = wdir / "wallet.zip"
+ zp.write_bytes(await wallet.read())
+ with zipfile.ZipFile(str(zp), 'r') as z: z.extractall(str(wdir))
+ with db() as c: c.execute("UPDATE adb_vector_configs SET wallet_dir=? WHERE id=?", (str(wdir), vid))
+ _config_log("adb", vid, config_name, "success", "upload", "Wallet atualizado", u["id"], u["username"])
+ _audit(u["id"], u["username"], "update_adb_config", vid, config_name)
+ _config_log("adb", vid, config_name, "success", "save", f"Conexão atualizada: {config_name} ({dsn})", u["id"], u["username"])
+ return {"id": vid, "config_name": config_name}
+
@app.post("/api/adb/{vid}/ensure-table")
async def ensure_table(vid: str, u=Depends(require("admin","user"))):
with db() as c:
diff --git a/frontend/index.html b/frontend/index.html
index 68e08f8..3f3d338 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -194,7 +194,7 @@ const V='1.1';
const LOGO_W=``;
const LOGO_R=``;
-const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],genaiCfg:[],adbCfg:[],mcpSvr:[],users:[],auditLogs:[],models:{},regions:[],embModels:{},selGenai:'',expData:null};
+const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],genaiCfg:[],adbCfg:[],mcpSvr:[],users:[],auditLogs:[],models:{},regions:[],embModels:{},selGenai:'',expData:null,editing:null};
const API='/api';
async function $api(p,o={}){const h={...(o.headers||{})};if(S.token)h['Authorization']='Bearer '+S.token;
@@ -212,7 +212,7 @@ async function loadData(){try{
try{S.adbCfg=await $api('/adb/configs')}catch(e){S.adbCfg=[]}
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 R(){document.getElementById('app').innerHTML=S.user?rApp():rLogin();bind();if(S.editing?.type==='mcp')setTimeout(mcpTypeFields,0)}
function switchTab(t){S.tab=t;S.expData=null;R();if(t==='audit'&&S.user.role==='admin')loadAudit();if(t==='downloads')refreshDl();
const tm={'oci-config':'oci','genai':'genai','adb':'adb','mcp':'mcp'};if(tm[t])setTimeout(()=>refreshCLogs(tm[t]),100)}
@@ -330,69 +330,81 @@ ${!S.reports.length?'
Nenhum relat function dlRpt(id,f){window.open(API+'/reports/'+id+'/download?fmt='+f,'_blank')} async function refreshDl(){S.reports=await $api('/reports');R()} +/* ── Edit helpers ── */ +function editCfg(type,id){S.editing={type,id};R(); + const form=document.querySelector('.cd:has(#obtn), .cd:has(#gbtn), .cd:has(#abtn), .cd:has(#mcbtn)');if(form)form.scrollIntoView({behavior:'smooth',block:'start'})} +function cancelEdit(){S.editing=null;R()} + /* ── OCI Config ── */ -function rOci(){return`
| Tenancy | Region | Compartment | Criado | Ações |
|---|---|---|---|---|
| ${c.tenancy_name} | ${c.region} | +${S.ociCfg.map(c=>`|||
| ${c.tenancy_name} | ${c.region} | ${c.compartment_id?(c.compartment_id.slice(0,22)+'…'):'—'} | ${c.created_at} | -
Nenhuma credencial registrada.
| Config | Modelo | Região | Temp | Tokens | Ações |
|---|---|---|---|---|---|
| ${g.name||'—'}${g.is_default?' ⭐':''} | +${S.genaiCfg.map(g=>{const mi=S.models[g.model_id]||{};return`|||||
| ${g.name||'—'}${g.is_default?' ⭐':''} | ${mi.name||g.model_id} ${mi.provider||'?'} |
${g.genai_region} | ${g.temperature} | ${g.max_tokens} | -
Nenhum modelo configurado.
GenerativeAiInferenceClient). O endpoint é gerado automaticamente pela região selecionada.GenerativeAiInferenceClient). O endpoint é gerado automaticamente pela região selecionada.'}Nenhum MCP server registrado.
| Nome | DSN | User | Tabela | mTLS | Wallet | Embed | Ações |
|---|---|---|---|---|---|---|---|
| ${c.config_name} | ${c.dsn} | +${S.adbCfg.map(c=>{const em=S.embModels[c.embedding_model_id];return`||||||
| ${c.config_name} | ${c.dsn} | ${c.username} | ${c.table_name} | ${c.use_mtls?'✅':'—'} | ${c.wallet_dir?'✅':'❌'} | ${c.genai_config_id?''+(em?em.name:c.embedding_model_id)+'':'—'} | -
Nenhuma conexão ADB registrada.
python-oracledb Thin mode com Wallet (mTLS) para Oracle Autonomous Database.python-oracledb Thin mode com Wallet (mTLS) para Oracle Autonomous Database. Envie o Wallet ZIP para preencher o DSN automaticamente.'}