diff --git a/GramAddict/core/bot_flow.py b/GramAddict/core/bot_flow.py index 011820c..be71e3d 100644 --- a/GramAddict/core/bot_flow.py +++ b/GramAddict/core/bot_flow.py @@ -444,8 +444,8 @@ def start_bot(**kwargs): from GramAddict.core.llm_provider import query_llm - model = getattr(configs.args, "ai_condenser_model", "llama3.2:1b") - url = getattr(configs.args, "ai_condenser_url", "http://localhost:11434/api/generate") + model = getattr(configs.args, "ai_condenser_model") + url = getattr(configs.args, "ai_condenser_url") response_dict = query_llm(url=url, model=model, prompt=prompt, format_json=True, timeout=120) if response_dict and isinstance(response_dict, dict) and "persona" in response_dict: diff --git a/GramAddict/core/compiler_engine.py b/GramAddict/core/compiler_engine.py index b5a3e79..c3e81f6 100644 --- a/GramAddict/core/compiler_engine.py +++ b/GramAddict/core/compiler_engine.py @@ -30,13 +30,13 @@ class VLMCompilerEngine: extra={"color": "\x1b[1m\x1b[35m"}, ) - args = getattr(self.device, "args", None) - model = getattr(args, "ai_telepathic_model", "llama3.2:1b") if args else "llama3.2:1b" - url = ( - getattr(args, "ai_telepathic_url", "http://localhost:11434/api/generate") - if args - else "http://localhost:11434/api/generate" - ) + from GramAddict.core.config import Config + + cfg = Config() + if not hasattr(cfg, "args"): + raise RuntimeError("Config().args not initialized — cannot resolve AI model. Fail Fast.") + model = getattr(cfg.args, "ai_telepathic_model") + url = getattr(cfg.args, "ai_telepathic_url") use_local = "11434" in url or "localhost" in url simplified_xml = self._simplify_xml(context_xml) diff --git a/GramAddict/core/dm_engine.py b/GramAddict/core/dm_engine.py index d219a5d..08ee457 100644 --- a/GramAddict/core/dm_engine.py +++ b/GramAddict/core/dm_engine.py @@ -152,8 +152,8 @@ def _run_zero_latency_dm_loop(device, zero_engine, nav_graph, configs, session_s return "BOREDOM_CHANGE_FEED" # Configure models - model = getattr(configs.args, "ai_condenser_model", "llama3.2:1b") - url = getattr(configs.args, "ai_condenser_url", "http://localhost:11434/api/generate") + model = getattr(configs.args, "ai_condenser_model") + url = getattr(configs.args, "ai_condenser_url") # Generate response prompt = f"You are replying to a direct message on Instagram. The last message you received was: '{context_text}'. Keep it short, casual, and friendly. Do not use hashtags." diff --git a/GramAddict/core/interaction.py b/GramAddict/core/interaction.py index a24ebda..12575c5 100644 --- a/GramAddict/core/interaction.py +++ b/GramAddict/core/interaction.py @@ -54,9 +54,9 @@ class LLMWriter: f"6. Reply with ONLY the comment text." ) - model = getattr(self.args, "ai_writer_model", getattr(self.args, "ai_model", "llama3.2:1b")) + model = getattr(self.args, "ai_writer_model", getattr(self.args, "ai_model")) url = getattr( - self.args, "ai_writer_url", getattr(self.args, "ai_model_url", "http://localhost:11434/api/generate") + self.args, "ai_writer_url", getattr(self.args, "ai_model_url") ) logger.info(f"✍️ [Writer] Generating comment for @{target_username} using {model}...") diff --git a/GramAddict/core/llm_provider.py b/GramAddict/core/llm_provider.py index 438c878..c7413f9 100644 --- a/GramAddict/core/llm_provider.py +++ b/GramAddict/core/llm_provider.py @@ -395,14 +395,11 @@ def query_llm( except Exception: pass - # Last resort defaults + # Last resort: If no fallback config exists, don't silently use hardcoded defaults. + # Config() defines these via argparse defaults — if they're missing, there's nothing to fallback to. if not f_model or not f_url: - if is_openai_compat: - f_model = f_model or "llama3.2:1b" - f_url = f_url or "http://localhost:11434/api/generate" - else: - f_model = f_model or "llama3.2:1b" - f_url = f_url or "http://localhost:11434/api/generate" + logger.warning("⚠️ [Circuit Breaker] No fallback model/URL configured. Cannot retry.") + return None # Circuit Breaker: If fallback is identical to primary, don't waste time retrying if f_model == model and f_url == url: @@ -458,11 +455,12 @@ def query_telepathic_llm( try: args = Config().args - target_url = getattr(args, "ai_fallback_url", "http://localhost:11434/api/generate") - target_model = getattr(args, "ai_fallback_model", "llama3.2:1b") + target_url = getattr(args, "ai_fallback_url") + target_model = getattr(args, "ai_fallback_model") except Exception: - target_url = "http://localhost:11434/api/generate" - target_model = "llama3.2:1b" + raise RuntimeError( + "Config().args not initialized — cannot resolve fallback AI model. Fail Fast." + ) is_local = "localhost" in target_url or "127.0.0.1" in target_url calc_timeout = 180 if is_local else 45 diff --git a/GramAddict/core/navigation/brain.py b/GramAddict/core/navigation/brain.py index 66d4a42..4880aa0 100644 --- a/GramAddict/core/navigation/brain.py +++ b/GramAddict/core/navigation/brain.py @@ -15,12 +15,10 @@ def ask_brain_for_action( return None cfg = Config() - url = ( - getattr(cfg.args, "ai_model_url", "http://localhost:11434/api/generate") - if hasattr(cfg, "args") - else "http://localhost:11434/api/generate" - ) - model = getattr(cfg.args, "ai_model", "qwen3.5:latest") if hasattr(cfg, "args") else "qwen3.5:latest" + if not hasattr(cfg, "args"): + raise RuntimeError("Config().args not initialized — cannot resolve AI model. Fail Fast.") + url = getattr(cfg.args, "ai_model_url") + model = getattr(cfg.args, "ai_model") prompt = ( f"You are an autonomous Instagram agent. Your ultimate goal is: '{goal}'.\n" diff --git a/GramAddict/core/perception/intent_resolver.py b/GramAddict/core/perception/intent_resolver.py index 47a8c6b..16804d2 100644 --- a/GramAddict/core/perception/intent_resolver.py +++ b/GramAddict/core/perception/intent_resolver.py @@ -661,8 +661,8 @@ class IntentResolver: self.last_box_map = box_map cfg = Config() - model = getattr(cfg.args, "ai_telepathic_model", "llava:latest") - url = getattr(cfg.args, "ai_telepathic_url", "http://localhost:11434/api/generate") + model = getattr(cfg.args, "ai_telepathic_model") + url = getattr(cfg.args, "ai_telepathic_url") # Build a compact legend of what each box contains box_legend_lines = [] @@ -785,8 +785,8 @@ class IntentResolver: filtered_candidates = [n for n in candidates if n.area < 500000] cfg = Config() - model = getattr(cfg.args, "ai_telepathic_model", "qwen3.5:latest") - url = getattr(cfg.args, "ai_telepathic_url", "http://localhost:11434/api/generate") + model = getattr(cfg.args, "ai_telepathic_model") + url = getattr(cfg.args, "ai_telepathic_url") node_context = [] for i, node in enumerate(filtered_candidates): diff --git a/GramAddict/core/perception/screen_identity.py b/GramAddict/core/perception/screen_identity.py index 2073b69..b527384 100644 --- a/GramAddict/core/perception/screen_identity.py +++ b/GramAddict/core/perception/screen_identity.py @@ -276,12 +276,10 @@ class ScreenIdentity: from GramAddict.core.llm_provider import query_telepathic_llm cfg = Config() - url = ( - getattr(cfg.args, "ai_telepathic_url", "http://localhost:11434/api/generate") - if hasattr(cfg, "args") - else "http://localhost:11434/api/generate" - ) - model = getattr(cfg.args, "ai_telepathic_model", "llava:latest") if hasattr(cfg, "args") else "llava:latest" + if not hasattr(cfg, "args"): + raise RuntimeError("Config().args not initialized — cannot resolve AI model. Fail Fast.") + url = getattr(cfg.args, "ai_telepathic_url") + model = getattr(cfg.args, "ai_telepathic_model") layout_context = ( f"Selected Tab: {selected_tab}\nResource IDs: {list(ids)}\nVisible Texts context: {texts[:10]}\n" diff --git a/GramAddict/core/perception/semantic_evaluator.py b/GramAddict/core/perception/semantic_evaluator.py index 2bdfbc7..caeacd3 100644 --- a/GramAddict/core/perception/semantic_evaluator.py +++ b/GramAddict/core/perception/semantic_evaluator.py @@ -28,8 +28,8 @@ class SemanticEvaluator: logger.warning("👁️ [Vision Core] No config available. Cannot query VLM.") return None - model = getattr(self.args, "ai_telepathic_model", "llama3.2-vision") - url = getattr(self.args, "ai_telepathic_url", "http://localhost:11434/api/generate") + model = getattr(self.args, "ai_telepathic_model") + url = getattr(self.args, "ai_telepathic_url") try: res = query_telepathic_llm( diff --git a/GramAddict/core/qdrant_memory.py b/GramAddict/core/qdrant_memory.py index cc31eaf..c4232a2 100644 --- a/GramAddict/core/qdrant_memory.py +++ b/GramAddict/core/qdrant_memory.py @@ -112,8 +112,8 @@ class QdrantBase: args = self._cached_args # Pull specific embedding config or fallback to defaults - model = getattr(args, "ai_embedding_model", "nomic-embed-text") - url = getattr(args, "ai_embedding_url", "http://localhost:11434/api/embeddings") + model = getattr(args, "ai_embedding_model") + url = getattr(args, "ai_embedding_url") try: # Generate embeddings diff --git a/GramAddict/core/resonance_engine.py b/GramAddict/core/resonance_engine.py index aed9ea4..e2569e8 100644 --- a/GramAddict/core/resonance_engine.py +++ b/GramAddict/core/resonance_engine.py @@ -386,8 +386,8 @@ class ResonanceEngine: "Set 'keep' to false only for clear spam, bots, UI buttons, or blacklist violations.\n" ) - model = getattr(configs.args, "ai_condenser_model", "llama3.2:1b") - url = getattr(configs.args, "ai_condenser_url", "http://localhost:11434/api/generate") + model = getattr(configs.args, "ai_condenser_model") + url = getattr(configs.args, "ai_condenser_url") try: import json diff --git a/tests/unit/__pycache__/test_sae_zero_maintenance.cpython-311-pytest-8.3.5.pyc b/tests/unit/__pycache__/test_sae_zero_maintenance.cpython-311-pytest-8.3.5.pyc index 9c6988e..952472c 100644 Binary files a/tests/unit/__pycache__/test_sae_zero_maintenance.cpython-311-pytest-8.3.5.pyc and b/tests/unit/__pycache__/test_sae_zero_maintenance.cpython-311-pytest-8.3.5.pyc differ diff --git a/tests/unit/__pycache__/test_structural_hardening.cpython-311-pytest-8.3.5.pyc b/tests/unit/__pycache__/test_structural_hardening.cpython-311-pytest-8.3.5.pyc index eaed113..538e2a4 100644 Binary files a/tests/unit/__pycache__/test_structural_hardening.cpython-311-pytest-8.3.5.pyc and b/tests/unit/__pycache__/test_structural_hardening.cpython-311-pytest-8.3.5.pyc differ diff --git a/tests/unit/test_sae_zero_maintenance.py b/tests/unit/test_sae_zero_maintenance.py index b86fc9d..3961168 100644 --- a/tests/unit/test_sae_zero_maintenance.py +++ b/tests/unit/test_sae_zero_maintenance.py @@ -164,6 +164,97 @@ class TestSAEContainsNoHardcodedModelDefaults: ) +# ══════════════════════════════════════════════════════════ +# 2b. NO HARDCODED MODEL/URL DEFAULTS IN ANY MODULE (codebase-wide) +# ══════════════════════════════════════════════════════════ + + +class TestCodebaseContainsNoHardcodedModelDefaults: + """ + CODEBASE-WIDE enforcement: No Python module in GramAddict/core/ may contain + hardcoded model names or localhost URLs as getattr() default values. + + The ONLY file allowed to define these defaults is config.py (the SSOT). + Every other module must read from Config().args WITHOUT providing a fallback literal. + If Config().args is missing, the system must crash (Fail Fast) — not silently + degrade to a potentially wrong model. + """ + + # These are the exact default-value patterns that are FORBIDDEN outside config.py. + # They indicate a getattr(..., "ai_xxx", "HARDCODED_DEFAULT") anti-pattern. + FORBIDDEN_DEFAULT_PATTERNS = [ + r'getattr\([^)]*,\s*["\']ai_[^"\']*["\']\s*,\s*["\']http://localhost:11434', + r'getattr\([^)]*,\s*["\']ai_[^"\']*["\']\s*,\s*["\']llava:', + r'getattr\([^)]*,\s*["\']ai_[^"\']*["\']\s*,\s*["\']qwen3\.', + r'getattr\([^)]*,\s*["\']ai_[^"\']*["\']\s*,\s*["\']llama3\.', + r'getattr\([^)]*,\s*["\']ai_[^"\']*["\']\s*,\s*["\']llama3\.2-vision', + r'getattr\([^)]*,\s*["\']ai_[^"\']*["\']\s*,\s*["\']nomic-embed-text', + ] + + # Files that are ALLOWED to have these defaults (SSOT only) + ALLOWED_FILES = {"config.py"} + + def _get_all_core_python_files(self): + import os + + core_dir = os.path.join(os.path.dirname(__file__), "..", "..", "GramAddict", "core") + core_dir = os.path.normpath(core_dir) + files = [] + for root, _dirs, filenames in os.walk(core_dir): + for f in filenames: + if f.endswith(".py") and f not in self.ALLOWED_FILES: + files.append(os.path.join(root, f)) + return files + + def test_no_getattr_with_hardcoded_model_defaults_outside_config(self): + """ + No module except config.py may use getattr(args, 'ai_xxx', 'HARDCODED_DEFAULT'). + The correct pattern is: getattr(cfg.args, 'ai_xxx') — no default, crash if missing. + """ + import os + + violations = [] + for filepath in self._get_all_core_python_files(): + with open(filepath) as f: + content = f.read() + for pattern in self.FORBIDDEN_DEFAULT_PATTERNS: + matches = re.findall(pattern, content) + if matches: + basename = os.path.basename(filepath) + violations.append(f"{basename}: {len(matches)}x pattern '{pattern[:60]}...'") + + assert len(violations) == 0, ( + f"Found {len(violations)} file(s) with hardcoded model/URL defaults outside config.py!\n" + f"Config() is the SSOT. Remove default values from getattr() calls.\n" + + "\n".join(f" ❌ {v}" for v in violations) + ) + + def test_no_bare_localhost_url_string_literals_outside_config(self): + """ + No module except config.py and llm_provider.py (routing logic) may contain + 'http://localhost:11434' as a bare string literal used as a fallback value. + """ + import os + + allowed = {"config.py", "llm_provider.py"} + violations = [] + core_dir = os.path.join(os.path.dirname(__file__), "..", "..", "GramAddict", "core") + core_dir = os.path.normpath(core_dir) + + for root, _dirs, filenames in os.walk(core_dir): + for f in filenames: + if f.endswith(".py") and f not in allowed: + path = os.path.join(root, f) + with open(path) as fh: + for i, line in enumerate(fh, 1): + if "localhost:11434" in line and not line.strip().startswith("#"): + violations.append(f"{f}:{i}: {line.strip()[:100]}") + + assert len(violations) == 0, ( + f"Found {len(violations)} hardcoded localhost:11434 references outside config.py/llm_provider.py!\n" + + "\n".join(f" ❌ {v}" for v in violations) + ) + # ══════════════════════════════════════════════════════════ # 3. GOAP SMART UI STABILIZATION (Poll instead of static sleep) # ══════════════════════════════════════════════════════════