diff --git a/backend/app.py b/backend/app.py index ef7b62d..514d848 100644 --- a/backend/app.py +++ b/backend/app.py @@ -335,6 +335,13 @@ def init_db(): created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) ); + CREATE TABLE IF NOT EXISTS tf_valid_types ( + name TEXT PRIMARY KEY, + kind TEXT NOT NULL DEFAULT 'resource', + category TEXT DEFAULT '', + required_args TEXT DEFAULT '', + blocks TEXT DEFAULT '' + ); """) c.execute("DELETE FROM config_logs WHERE created_at < datetime('now', '-30 days')") c.execute("DELETE FROM chat_logs WHERE created_at < datetime('now', '-30 days')") @@ -2036,6 +2043,37 @@ Abaixo estão os nomes corretos de resource types do provider oracle/oci. O mode } ``` - **Remote Peering Connection**: `oci_core_remote_peering_connection` + - NÃO EXISTE `oci_core_remote_peering_connection_peer`. Para conectar dois RPCs, use `peer_id` e `peer_region_name` DENTRO do próprio `oci_core_remote_peering_connection`: + ```hcl + resource "oci_core_remote_peering_connection" "rpc_initiator" { + compartment_id = var.compartment_id + drg_id = oci_core_drg.drg_a.id + peer_id = oci_core_remote_peering_connection.rpc_accepter.id + peer_region_name = var.region_b + } + ``` +- **Route Table em Subnet**: NÃO EXISTE `oci_core_subnet_route_table_association`. Para associar route table a subnet, use: + - Opção 1: `route_table_id` DENTRO do `oci_core_subnet` + - Opção 2: `oci_core_route_table_attachment` (requer `route_table_id` e `subnet_id` OBRIGATÓRIOS): + ```hcl + resource "oci_core_route_table_attachment" "example" { + route_table_id = oci_core_route_table.example_rt.id + subnet_id = oci_core_subnet.example.id + } + ``` +- **Network Firewall** (`oci_network_firewall_network_firewall`): + - O argumento `network_firewall_policy_id` é OBRIGATÓRIO. Crie primeiro a policy: + ```hcl + resource "oci_network_firewall_network_firewall_policy" "policy" { + compartment_id = var.compartment_id + display_name = "fw-policy" + } + resource "oci_network_firewall_network_firewall" "fw" { + compartment_id = var.compartment_id + network_firewall_policy_id = oci_network_firewall_network_firewall_policy.policy.id + subnet_id = oci_core_subnet.fw_subnet.id + } + ``` - **DRG Route Table**: `oci_core_drg_route_table` - **DRG Route Distribution**: `oci_core_drg_route_distribution` - **DRG Route Table Route Rule**: `oci_core_drg_route_table_route_rule` @@ -2103,6 +2141,93 @@ Antes de entregar qualquer código (novo ou corrigido), valide TODOS os itens ab _TF_RESOURCE_REF_PATH = "/data/oci_tf_resource_reference.txt" _tf_resource_ref_cache: dict = {"content": None, "mtime": 0} + +def _populate_tf_valid_types(): + """Parse the OCI TF resource reference file and populate tf_valid_types table in SQLite.""" + p = Path(_TF_RESOURCE_REF_PATH) + if not p.exists(): + return 0 + content = p.read_text() + import re as _re + entries = [] + current_category = "" + for line in content.split('\n'): + if line.startswith('## '): + current_category = line[3:].strip() + continue + # Resource lines: - **oci_xxx** required: ... blocks: ... + m = _re.match(r'^- \*\*(\w+)\*\*\s*(.*)', line) + if m: + name = m.group(1) + rest = m.group(2).strip() + required = "" + blocks = "" + # Split by "blocks:" to separate required from blocks + blk_parts = rest.split('blocks:') + if blk_parts[0].strip().startswith('required:'): + required = blk_parts[0].replace('required:', '').strip() + if len(blk_parts) > 1: + blocks = blk_parts[1].strip() + entries.append((name, 'resource', current_category, required, blocks)) + continue + # Data source lines (in "All Resource Types" or "All Data Sources" section): - oci_xxx + m2 = _re.match(r'^- (\w+)\s*$', line) + if m2: + name = m2.group(1) + if current_category.startswith('All Data'): + entries.append((name, 'data', '', '', '')) + elif current_category.startswith('All Resource'): + entries.append((name, 'resource', '', '', '')) + if not entries: + return 0 + with db() as c: + c.execute("DELETE FROM tf_valid_types") + # Insert detailed entries first (with required_args), then bulk lists with IGNORE to not overwrite + detailed = [e for e in entries if e[3] or e[4]] # has required_args or blocks + bulk = [e for e in entries if not e[3] and not e[4]] + if detailed: + c.executemany( + "INSERT OR REPLACE INTO tf_valid_types (name, kind, category, required_args, blocks) VALUES (?,?,?,?,?)", + detailed) + if bulk: + c.executemany( + "INSERT OR IGNORE INTO tf_valid_types (name, kind, category, required_args, blocks) VALUES (?,?,?,?,?)", + bulk) + log.info(f"Populated tf_valid_types: {len(entries)} entries") + return len(entries) + + +def _validate_tf_resource_types(tf_code: str) -> list: + """Validate resource/data types in generated TF code against the tf_valid_types table. + Returns list of dicts with invalid types and suggestions.""" + import re as _re + from difflib import get_close_matches + # Extract all resource and data type declarations + used_types = set() + for m in _re.finditer(r'(?:resource|data)\s+"(\w+)"', tf_code): + used_types.add(m.group(1)) + if not used_types: + return [] + # Load valid types from DB + with db() as c: + rows = c.execute("SELECT name, kind, required_args FROM tf_valid_types").fetchall() + valid_names = {r["name"] for r in rows} + valid_required = {r["name"]: r["required_args"] for r in rows} + if not valid_names: + return [] # No reference loaded + # Check each used type + errors = [] + for t in sorted(used_types): + if t not in valid_names: + # Find closest matches + suggestions = get_close_matches(t, valid_names, n=3, cutoff=0.6) + errors.append({ + "type": t, + "suggestions": suggestions, + "message": f"Resource type '{t}' NÃO EXISTE no provider oracle/oci." + }) + return errors + def _load_tf_resource_reference() -> str: """Load the OCI Terraform resource reference file, cached by mtime.""" try: @@ -3227,7 +3352,7 @@ async def _chat_background(mid: str, sid: str, msg: ChatMsg, user: dict, genai_c # ── Terraform agent: boost max_tokens for complex infra code ── if agent_type == "terraform": - cfg_dict["max_tokens"] = max(int(cfg_dict.get("max_tokens", 6000)), 32000) + cfg_dict["max_tokens"] = max(int(cfg_dict.get("max_tokens", 6000)), 100000) # ── Inject existing OCI resources for terraform agent ── if agent_type == "terraform" and active_oci_id: @@ -3363,6 +3488,23 @@ async def _chat_background(mid: str, sid: str, msg: ChatMsg, user: dict, genai_c resp = f"⚠️ O modelo **{model_id}** retornou uma resposta vazia. Isso pode ocorrer com alguns modelos. Tente novamente ou selecione outro modelo (ex: Gemini, Llama)." log.warning(f"Chat {mid}: empty response from model {model_id}, returning fallback message") + # Validate terraform resource types against DB + if agent_type == "terraform" and resp and '```' in resp: + try: + tf_errors = _validate_tf_resource_types(resp) + if tf_errors: + warning_lines = ["\n\n⚠️ **Validação automática — Resource types inválidos detectados:**"] + for err in tf_errors: + line = f"- `{err['type']}` — NÃO EXISTE no provider oracle/oci." + if err['suggestions']: + line += f" Sugestões: {', '.join('`' + s + '`' for s in err['suggestions'])}" + warning_lines.append(line) + warning_lines.append("\n**Clique em 'Re-plan' após o modelo corrigir, ou peça correção no chat.**") + resp += '\n'.join(warning_lines) + log.warning(f"Chat {mid}: {len(tf_errors)} invalid TF resource types detected: {[e['type'] for e in tf_errors]}") + except Exception as e: + log.warning(f"TF resource type validation failed: {e}") + with db() as c: c.execute("UPDATE chat_messages SET content=?, status='done' WHERE id=?", (resp, mid)) log.info(f"Chat {mid} completed successfully") @@ -4426,6 +4568,26 @@ async def startup(): except Exception as e: log.warning(f"Could not read CIS engine version: {e}") + # Generate TF resource reference if not present (volume mount may hide build-time file) + ref_path = Path(_TF_RESOURCE_REF_PATH) + if not ref_path.exists(): + try: + result = subprocess.run(["python", "/app/gen_tf_reference.py"], capture_output=True, text=True, timeout=300) + if result.returncode == 0 and ref_path.exists(): + log.info(f"Generated TF resource reference at startup: {ref_path.stat().st_size} bytes") + else: + log.warning(f"TF reference generation failed at startup: {result.stderr[:200] if result.stderr else 'unknown'}") + except Exception as e: + log.warning(f"Could not generate TF reference at startup: {e}") + # Populate tf_valid_types table from reference file + if ref_path.exists(): + try: + count = _populate_tf_valid_types() + if count: + log.info(f"TF valid types DB populated: {count} entries") + except Exception as e: + log.warning(f"Could not populate tf_valid_types: {e}") + log.info(f"OCI CIS AI Agent v{VERSION} API started") if __name__ == "__main__": diff --git a/backend/mcp_cis_server.py b/backend/mcp_cis_server.py index 9327c93..38e2486 100644 --- a/backend/mcp_cis_server.py +++ b/backend/mcp_cis_server.py @@ -327,7 +327,7 @@ def _finding_in_compartment(finding, compartment_id): if fcomp == compartment_id: return True if fcomp is None: - return True # recurso sem compartment (IAM tenancy-level) → incluir + return True # resource without compartment (IAM tenancy-level) → include return False