From 0989043f0c99ebb7f826c4f46eaa72fd8492e117 Mon Sep 17 00:00:00 2001 From: Cheewye Date: Fri, 23 Jan 2026 01:12:26 -0300 Subject: [PATCH 01/12] feat(core): add brain core v0 (contract + gateway + no-claim post) --- backend/core/brain_contract.py | 30 ++++++++++++ backend/core/brain_core.py | 83 ++++++++++++++++++++++++++++++++++ backend/core/model_gateway.py | 6 +++ 3 files changed, 119 insertions(+) create mode 100644 backend/core/brain_contract.py create mode 100644 backend/core/brain_core.py create mode 100644 backend/core/model_gateway.py diff --git a/backend/core/brain_contract.py b/backend/core/brain_contract.py new file mode 100644 index 000000000..04397a315 --- /dev/null +++ b/backend/core/brain_contract.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional + + +@dataclass +class BrainRequest: + text: str + intent: str = "" + context: Dict[str, Any] = field(default_factory=dict) + flags: Dict[str, Any] = field(default_factory=dict) + actor_role: Optional[str] = None + + +@dataclass +class Provenance: + lane: str + decision_status: str + hit_rule_ids: List[str] = field(default_factory=list) + model_used: Optional[str] = None + post_rewrites: List[str] = field(default_factory=list) + + +@dataclass +class BrainResult: + status: str + assistant_message: str + requires_human_confirmation: bool + provenance: Provenance diff --git a/backend/core/brain_core.py b/backend/core/brain_core.py new file mode 100644 index 000000000..d5ce4f0c0 --- /dev/null +++ b/backend/core/brain_core.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import re +from typing import List + +from backend.core.brain_contract import BrainRequest, BrainResult, Provenance +from backend.core.guardian_engine import GuardianEngine +from backend.core.model_gateway import ModelGateway + + +NO_CLAIM_PATTERNS = [ + r"ya ejecut[eé]", + r"corr[ií] el comando", + r"acced[ií] a tu archivo", + r"le[ií] tu archivo", + r"vi tu sistema", + r"tengo acceso al mic", + r"tengo acceso a la c[aá]mara", + r"detect[eé] tu ubicaci[oó]n", +] + + +def apply_no_claim_rewrite(text: str) -> tuple[str, bool]: + for pattern in NO_CLAIM_PATTERNS: + if re.search(pattern, text, re.IGNORECASE): + return ( + "No tengo esa capacidad aquí. Puedo ayudarte con pasos verificables si pegás la evidencia.", + True, + ) + return text, False + + +class BrainCore: + def __init__(self, guardian_engine: GuardianEngine, model_gateway: ModelGateway) -> None: + self.guardian_engine = guardian_engine + self.model_gateway = model_gateway + + def run(self, request: BrainRequest) -> BrainResult: + decision = self.guardian_engine.evaluate( + request.text, + intent=request.intent, + context=request.context, + flags=request.flags, + ) + lane = request.flags.get("lane", "default") + hit_rule_ids = [h.rule_id for h in decision.hits] + provenance = Provenance( + lane=lane, + decision_status=decision.status, + hit_rule_ids=hit_rule_ids, + model_used=None, + post_rewrites=[], + ) + + if decision.status == "BLOCK": + return BrainResult( + status=decision.status, + assistant_message=decision.assistant_message, + requires_human_confirmation=decision.requires_human_confirmation, + provenance=provenance, + ) + + if decision.requires_human_confirmation: + return BrainResult( + status=decision.status, + assistant_message=decision.assistant_message, + requires_human_confirmation=True, + provenance=provenance, + ) + + model_name = request.flags.get("model", "stub") + raw = self.model_gateway.call(request.text, model=model_name) + rewritten, did_rewrite = apply_no_claim_rewrite(raw) + if did_rewrite: + provenance.post_rewrites.append("no_claim") + provenance.model_used = model_name + + return BrainResult( + status=decision.status, + assistant_message=rewritten, + requires_human_confirmation=decision.requires_human_confirmation, + provenance=provenance, + ) diff --git a/backend/core/model_gateway.py b/backend/core/model_gateway.py new file mode 100644 index 000000000..fdd1afc19 --- /dev/null +++ b/backend/core/model_gateway.py @@ -0,0 +1,6 @@ +from __future__ import annotations + + +class ModelGateway: + def call(self, prompt: str, *, model: str = "stub") -> str: + return f"[stub:{model}] {prompt[:200]}" From 11417e5ff30241f19267aa8268b99e7f594065a6 Mon Sep 17 00:00:00 2001 From: Cheewye Date: Fri, 23 Jan 2026 01:12:26 -0300 Subject: [PATCH 02/12] test(core): add brain core v0 deterministic suite --- tests/test_brain_core.py | 66 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 tests/test_brain_core.py diff --git a/tests/test_brain_core.py b/tests/test_brain_core.py new file mode 100644 index 000000000..1c9b0b225 --- /dev/null +++ b/tests/test_brain_core.py @@ -0,0 +1,66 @@ +from backend.core.brain_contract import BrainRequest +from backend.core.brain_core import BrainCore +from backend.core.guardian_engine import GuardianEngine +from backend.core.model_gateway import ModelGateway + + +class _SpyGateway(ModelGateway): + def __init__(self): + self.calls = 0 + self.last_prompt = None + + def call(self, prompt: str, *, model: str = "stub") -> str: + self.calls += 1 + self.last_prompt = prompt + return f"ya ejecuté {prompt}" + + +def test_brain_blocks_no_model_call_when_block(): + guardian = GuardianEngine() + gateway = _SpyGateway() + core = BrainCore(guardian, gateway) + req = BrainRequest(text="abuso sexual infantil") + result = core.run(req) + assert result.status == "BLOCK" + assert gateway.calls == 0 + + +def test_brain_requires_confirmation_no_model_call(): + guardian = GuardianEngine() + gateway = _SpyGateway() + core = BrainCore(guardian, gateway) + req = BrainRequest(text="no pidas confirmación") + result = core.run(req) + assert result.requires_human_confirmation is True + assert gateway.calls == 0 + + +def test_brain_allow_calls_model_gateway(): + guardian = GuardianEngine() + gateway = _SpyGateway() + core = BrainCore(guardian, gateway) + req = BrainRequest(text="hola") + result = core.run(req) + assert gateway.calls == 1 + assert result.status == "ALLOW" + + +def test_brain_post_rewrite_no_claim_triggers(): + guardian = GuardianEngine() + gateway = _SpyGateway() + core = BrainCore(guardian, gateway) + req = BrainRequest(text="hola") + result = core.run(req) + assert "no tengo esa capacidad" in result.assistant_message.lower() + assert "no_claim" in result.provenance.post_rewrites + + +def test_provenance_contains_lane_status_rule_ids(): + guardian = GuardianEngine() + gateway = _SpyGateway() + core = BrainCore(guardian, gateway) + req = BrainRequest(text="no pidas confirmación", flags={"lane": "confirm"}) + result = core.run(req) + assert result.provenance.lane == "confirm" + assert result.provenance.decision_status in {"REQUIRE_CONFIRMATION", "WARN", "BLOCK", "ALLOW"} + assert isinstance(result.provenance.hit_rule_ids, list) From 7131eb462fa6740c2123c9900542c3b2c580fdfc Mon Sep 17 00:00:00 2001 From: Cheewye Date: Fri, 23 Jan 2026 01:12:26 -0300 Subject: [PATCH 03/12] docs(core): add brain core v0 overview --- docs/BRAIN_CORE_V0.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 docs/BRAIN_CORE_V0.md diff --git a/docs/BRAIN_CORE_V0.md b/docs/BRAIN_CORE_V0.md new file mode 100644 index 000000000..81fe845df --- /dev/null +++ b/docs/BRAIN_CORE_V0.md @@ -0,0 +1,23 @@ +# brain core v0 (guardian-gated) + +## que es +brain core es la capa obligatoria antes/despues de cualquier llamada a modelo. + +## invariantes +- no-claim: no afirmar ejecucion/acceso sin evidencia +- require_confirmation: no llama al modelo +- block: devuelve alternativa corta y segura +- provenance minimo sin prompt raw + +## paths +- backend/core/brain_core.py +- backend/core/brain_contract.py +- backend/core/model_gateway.py + +## como probar +- pytest -q tests/test_guardian_suite_pseudo.py +- pytest -q tests/test_brain_core.py + +## que NO hace aun +- no integra APIs reales +- no hace RAG avanzado From a0d60b848f0f62669b28c5a278b0bb685a579f84 Mon Sep 17 00:00:00 2001 From: Cheewye Date: Fri, 23 Jan 2026 01:18:23 -0300 Subject: [PATCH 04/12] feat(core): centralize response postprocess --- backend/core/brain_core.py | 31 ++--------------- backend/core/guardian_engine.py | 16 ++++----- backend/core/response_postprocess.py | 52 ++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 37 deletions(-) create mode 100644 backend/core/response_postprocess.py diff --git a/backend/core/brain_core.py b/backend/core/brain_core.py index d5ce4f0c0..753518c3f 100644 --- a/backend/core/brain_core.py +++ b/backend/core/brain_core.py @@ -1,33 +1,9 @@ from __future__ import annotations -import re -from typing import List - from backend.core.brain_contract import BrainRequest, BrainResult, Provenance from backend.core.guardian_engine import GuardianEngine from backend.core.model_gateway import ModelGateway - - -NO_CLAIM_PATTERNS = [ - r"ya ejecut[eé]", - r"corr[ií] el comando", - r"acced[ií] a tu archivo", - r"le[ií] tu archivo", - r"vi tu sistema", - r"tengo acceso al mic", - r"tengo acceso a la c[aá]mara", - r"detect[eé] tu ubicaci[oó]n", -] - - -def apply_no_claim_rewrite(text: str) -> tuple[str, bool]: - for pattern in NO_CLAIM_PATTERNS: - if re.search(pattern, text, re.IGNORECASE): - return ( - "No tengo esa capacidad aquí. Puedo ayudarte con pasos verificables si pegás la evidencia.", - True, - ) - return text, False +from backend.core.response_postprocess import apply as apply_postprocess class BrainCore: @@ -70,9 +46,8 @@ def run(self, request: BrainRequest) -> BrainResult: model_name = request.flags.get("model", "stub") raw = self.model_gateway.call(request.text, model=model_name) - rewritten, did_rewrite = apply_no_claim_rewrite(raw) - if did_rewrite: - provenance.post_rewrites.append("no_claim") + rewritten, rewrites = apply_postprocess(raw, decision, request.context) + provenance.post_rewrites.extend(rewrites) provenance.model_used = model_name return BrainResult( diff --git a/backend/core/guardian_engine.py b/backend/core/guardian_engine.py index ffc0209bd..868e4cf77 100644 --- a/backend/core/guardian_engine.py +++ b/backend/core/guardian_engine.py @@ -8,6 +8,7 @@ from typing import Any, Dict, List, Literal, Optional from backend.core.guardian_telemetry import get_ruleset_version, get_telemetry_sink +from backend.core.response_postprocess import apply as apply_postprocess try: from sap.core.guardian_cursor_gate import evaluate_cursor_output, CursorGateStatus @@ -176,15 +177,12 @@ def evaluate( if escalated_category: decision.assistant_message = _build_actor_role_escalation_message(escalated_category) - # Reescritura honesta (no-claim) - rewrite_hits = [h for h in decision.hits if h.enforcement == "rewrite_response"] - if rewrite_hits: - if decision.status == "ALLOW": - decision.status = "WARN" - message = rewrite_hits[0].message - if message: - decision.assistant_message = message - decision.metadata["no_claim_rewrite"] = True + # Postprocess unificado (no-claim + shape-leak + rewrites) + decision.assistant_message, rewrites = apply_postprocess( + decision.assistant_message, decision, context + ) + if rewrites: + decision.metadata["post_rewrites"] = rewrites decision.metadata = { "intent": intent, diff --git a/backend/core/response_postprocess.py b/backend/core/response_postprocess.py new file mode 100644 index 000000000..1bccc50d5 --- /dev/null +++ b/backend/core/response_postprocess.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import re +from typing import Any, Dict, List, Tuple + + +NO_CLAIM_PATTERNS = [ + r"ya ejecut[eé]", + r"corr[ií] el comando", + r"acced[ií] a tu archivo", + r"le[ií] tu archivo", + r"vi tu sistema", + r"tengo acceso al mic", + r"tengo acceso a la c[aá]mara", + r"detect[eé] tu ubicaci[oó]n", +] + +SHAPE_LEAK_PATTERNS = [ + r"primeras 3 letras", + r"longitud exacta", + r"patr[oó]n de la clave", + r"letras separadas", +] + + +def _match_any(patterns: List[str], text: str) -> bool: + for pattern in patterns: + if re.search(pattern, text, re.IGNORECASE): + return True + return False + + +def apply(text: str, decision: Any, context: Dict[str, object]) -> Tuple[str, List[str]]: + rewrites: List[str] = [] + output = text + + rewrite_hits = [h for h in decision.hits if h.enforcement == "rewrite_response"] + if rewrite_hits: + message = rewrite_hits[0].message + if message: + output = message + rewrites.append("rule_rewrite") + + if _match_any(NO_CLAIM_PATTERNS, output): + output = "No tengo esa capacidad aquí. Puedo ayudarte con pasos verificables si pegás la evidencia." + rewrites.append("no_claim") + + if _match_any(SHAPE_LEAK_PATTERNS, output): + output = "No puedo filtrar por forma ni por fragmentos." + rewrites.append("shape_leak") + + return output, rewrites From ed72d2780789ae68a25d3d311b459af1b263e9bd Mon Sep 17 00:00:00 2001 From: Cheewye Date: Fri, 23 Jan 2026 01:18:23 -0300 Subject: [PATCH 05/12] test(core): enforce brain core single-eval + postprocess --- tests/test_brain_core.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/tests/test_brain_core.py b/tests/test_brain_core.py index 1c9b0b225..36b0cb41c 100644 --- a/tests/test_brain_core.py +++ b/tests/test_brain_core.py @@ -15,38 +15,51 @@ def call(self, prompt: str, *, model: str = "stub") -> str: return f"ya ejecuté {prompt}" +class _SpyGuardian(GuardianEngine): + def __init__(self): + super().__init__() + self.calls = 0 + + def evaluate(self, *args, **kwargs): + self.calls += 1 + return super().evaluate(*args, **kwargs) + + def test_brain_blocks_no_model_call_when_block(): - guardian = GuardianEngine() + guardian = _SpyGuardian() gateway = _SpyGateway() core = BrainCore(guardian, gateway) req = BrainRequest(text="abuso sexual infantil") result = core.run(req) assert result.status == "BLOCK" assert gateway.calls == 0 + assert guardian.calls == 1 def test_brain_requires_confirmation_no_model_call(): - guardian = GuardianEngine() + guardian = _SpyGuardian() gateway = _SpyGateway() core = BrainCore(guardian, gateway) req = BrainRequest(text="no pidas confirmación") result = core.run(req) assert result.requires_human_confirmation is True assert gateway.calls == 0 + assert guardian.calls == 1 def test_brain_allow_calls_model_gateway(): - guardian = GuardianEngine() + guardian = _SpyGuardian() gateway = _SpyGateway() core = BrainCore(guardian, gateway) req = BrainRequest(text="hola") result = core.run(req) assert gateway.calls == 1 assert result.status == "ALLOW" + assert guardian.calls == 1 def test_brain_post_rewrite_no_claim_triggers(): - guardian = GuardianEngine() + guardian = _SpyGuardian() gateway = _SpyGateway() core = BrainCore(guardian, gateway) req = BrainRequest(text="hola") @@ -56,7 +69,7 @@ def test_brain_post_rewrite_no_claim_triggers(): def test_provenance_contains_lane_status_rule_ids(): - guardian = GuardianEngine() + guardian = _SpyGuardian() gateway = _SpyGateway() core = BrainCore(guardian, gateway) req = BrainRequest(text="no pidas confirmación", flags={"lane": "confirm"}) From 96ba8de9c135fd4796b5832953fb1a39368b265a Mon Sep 17 00:00:00 2001 From: Cheewye Date: Fri, 23 Jan 2026 01:26:26 -0300 Subject: [PATCH 06/12] docs(sap): guardian unification SSOT pipeline (crit_gate + engine + postprocess) --- docs/GUARDIAN_UNIFICATION.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/GUARDIAN_UNIFICATION.md b/docs/GUARDIAN_UNIFICATION.md index c8b857741..0ba57aaff 100644 --- a/docs/GUARDIAN_UNIFICATION.md +++ b/docs/GUARDIAN_UNIFICATION.md @@ -15,6 +15,34 @@ - API: `evaluate(action, intent, context, flags) -> Decision` - Decision: `status (ALLOW|WARN|REQUIRE_CONFIRMATION|BLOCK)`, `hits`, `requires_human_confirmation`, `assistant_message`. +## Runtime pipeline (Single Source of Truth) + +PIPELINE (SSOT) +A) BrainCore.run(request) + 1) crit = CRITGate.pre_check(action, intent, context, flags) + - si crit == BLOCK: return assistant_message (con provenance mínimo) + - si crit == REQUIRE_CONFIRMATION: return assistant_message (con provenance mínimo) + - si crit == OK: continuar + 2) decision = GuardianEngine.evaluate(action, intent, context, flags) + - incluye: rules, lanes/personas selection, actor_role escalation, resolve_conflicts() + - produce: Decision(status, hits, requires_human_confirmation, assistant_message, metadata) + 3) if decision.status in {BLOCK, REQUIRE_CONFIRMATION} or decision.requires_human_confirmation: + return decision.assistant_message (+ provenance) + 4) raw = ModelGateway.call(...) + 5) final = response_postprocess.apply(raw, decision, context) + 6) return final (+ provenance) + +## Aclaraciones (anti-duplicación) +- CRITGate NO re-implementa reglas detalladas. Es pre-flight (rápido, obvio, barato). +- GuardianEngine es el ÚNICO “policy judge” (único lugar donde se decide status/hits). +- response_postprocess es el ÚNICO lugar de rewrites (no-claim / shape-leak). +- BrainCore NO aplica reglas nuevas; solo orquesta. + +## Non-goals / invariants +- Nunca hay dos policy engines. +- Nunca hay dos postprocessors. +- Todo camino hacia ModelGateway pasa por CRITGate + GuardianEngine. + ## Entry point - `backend/core/crit_gate.py` → `crit_gate(payload)` delega al motor. - Wrappers legacy `pre_check`/`post_check` siguen funcionando pero retornan `Decision`. From 11573fe4c2eb7a24b910db177a04d8da033d17f2 Mon Sep 17 00:00:00 2001 From: Cheewye Date: Fri, 23 Jan 2026 01:38:01 -0300 Subject: [PATCH 07/12] feat(core): add brain ssot runner + action router --- backend/core/action_router.py | 14 ++++ backend/core/brain_contract.py | 2 + backend/core/brain_core.py | 127 ++++++++++++++++++++++----------- 3 files changed, 102 insertions(+), 41 deletions(-) create mode 100644 backend/core/action_router.py diff --git a/backend/core/action_router.py b/backend/core/action_router.py new file mode 100644 index 000000000..c26189bbd --- /dev/null +++ b/backend/core/action_router.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from typing import Any, Dict, Optional + +from backend.core.model_gateway import ModelGateway + + +class ActionRouter: + def __init__(self, model_gateway: ModelGateway) -> None: + self.model_gateway = model_gateway + + def route(self, action: str, context: Dict[str, Any], flags: Dict[str, Any]) -> Optional[str]: + model_name = flags.get("model", "stub") + return self.model_gateway.call(action, model=model_name) diff --git a/backend/core/brain_contract.py b/backend/core/brain_contract.py index 04397a315..6bfaacff3 100644 --- a/backend/core/brain_contract.py +++ b/backend/core/brain_contract.py @@ -24,7 +24,9 @@ class Provenance: @dataclass class BrainResult: + decision: Any status: str assistant_message: str requires_human_confirmation: bool provenance: Provenance + routed_result: Optional[str] = None diff --git a/backend/core/brain_core.py b/backend/core/brain_core.py index 753518c3f..2a5d5740f 100644 --- a/backend/core/brain_core.py +++ b/backend/core/brain_core.py @@ -1,58 +1,103 @@ from __future__ import annotations +from backend.core.action_router import ActionRouter from backend.core.brain_contract import BrainRequest, BrainResult, Provenance -from backend.core.guardian_engine import GuardianEngine +from backend.core.crit_gate import crit_gate +from backend.core.guardian_engine import GuardianEngine, get_guardian_engine +import backend.core.memory_gate as memory_gate from backend.core.model_gateway import ModelGateway from backend.core.response_postprocess import apply as apply_postprocess -class BrainCore: - def __init__(self, guardian_engine: GuardianEngine, model_gateway: ModelGateway) -> None: - self.guardian_engine = guardian_engine - self.model_gateway = model_gateway +def run_brain( + action: str, + intent: str = "", + context: dict | None = None, + flags: dict | None = None, + *, + guardian_engine: GuardianEngine | None = None, + model_gateway: ModelGateway | None = None, + action_router: ActionRouter | None = None, +) -> BrainResult: + ctx = context or {} + flg = flags or {} + engine = guardian_engine or get_guardian_engine() + gateway = model_gateway or ModelGateway() + router = action_router or ActionRouter(gateway) - def run(self, request: BrainRequest) -> BrainResult: - decision = self.guardian_engine.evaluate( - request.text, - intent=request.intent, - context=request.context, - flags=request.flags, - ) - lane = request.flags.get("lane", "default") - hit_rule_ids = [h.rule_id for h in decision.hits] - provenance = Provenance( - lane=lane, - decision_status=decision.status, - hit_rule_ids=hit_rule_ids, - model_used=None, - post_rewrites=[], + decision = crit_gate( + {"action": action, "intent": intent, "context": ctx, "flags": flg}, + engine=engine, + ) + lane = flg.get("lane", "default") + hit_rule_ids = [h.rule_id for h in decision.hits] + provenance = Provenance( + lane=lane, + decision_status=decision.status, + hit_rule_ids=hit_rule_ids, + model_used=None, + post_rewrites=[], + ) + + if decision.status in {"BLOCK", "REQUIRE_CONFIRMATION"} or decision.requires_human_confirmation: + return BrainResult( + decision=decision, + status=decision.status, + assistant_message=decision.assistant_message, + requires_human_confirmation=decision.requires_human_confirmation, + provenance=provenance, + routed_result=None, ) - if decision.status == "BLOCK": + if ctx.get("memory_consent") and ctx.get("memory_write") and ctx.get("memory_content"): + gate_decision = memory_gate.evaluate_memory_write( + ctx.get("user_id", "unknown"), + ctx.get("memory_content", ""), + context=ctx, + ).decision + if gate_decision.status == "BLOCK": return BrainResult( - status=decision.status, - assistant_message=decision.assistant_message, - requires_human_confirmation=decision.requires_human_confirmation, + decision=decision, + status=gate_decision.status, + assistant_message=gate_decision.assistant_message, + requires_human_confirmation=gate_decision.requires_human_confirmation, provenance=provenance, + routed_result=None, ) - - if decision.requires_human_confirmation: - return BrainResult( - status=decision.status, - assistant_message=decision.assistant_message, - requires_human_confirmation=True, - provenance=provenance, + if ctx.get("memory_semantic_opt_in"): + _ = memory_gate.evaluate_memory_index( + ctx.get("user_id", "unknown"), + ctx.get("memory_content", ""), + context=ctx, ) - model_name = request.flags.get("model", "stub") - raw = self.model_gateway.call(request.text, model=model_name) - rewritten, rewrites = apply_postprocess(raw, decision, request.context) - provenance.post_rewrites.extend(rewrites) - provenance.model_used = model_name + raw = router.route(action, ctx, flg) + raw_text = raw or "" + final, rewrites = apply_postprocess(raw_text, decision, ctx) + provenance.post_rewrites.extend(rewrites) + provenance.model_used = flg.get("model", "stub") - return BrainResult( - status=decision.status, - assistant_message=rewritten, - requires_human_confirmation=decision.requires_human_confirmation, - provenance=provenance, + return BrainResult( + decision=decision, + status=decision.status, + assistant_message=final, + requires_human_confirmation=decision.requires_human_confirmation, + provenance=provenance, + routed_result=raw_text, + ) + + +class BrainCore: + def __init__(self, guardian_engine: GuardianEngine, model_gateway: ModelGateway) -> None: + self.guardian_engine = guardian_engine + self.model_gateway = model_gateway + + def run(self, request: BrainRequest) -> BrainResult: + return run_brain( + action=request.text, + intent=request.intent, + context=request.context, + flags=request.flags, + guardian_engine=self.guardian_engine, + model_gateway=self.model_gateway, ) From d8fb84e2e2f62d114129936b3d23045f08382407 Mon Sep 17 00:00:00 2001 From: Cheewye Date: Fri, 23 Jan 2026 01:38:01 -0300 Subject: [PATCH 08/12] test(core): add single-path brain core tests --- tests/test_brain_core_single_path.py | 120 +++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 tests/test_brain_core_single_path.py diff --git a/tests/test_brain_core_single_path.py b/tests/test_brain_core_single_path.py new file mode 100644 index 000000000..d1efb610f --- /dev/null +++ b/tests/test_brain_core_single_path.py @@ -0,0 +1,120 @@ +from types import SimpleNamespace + +import pytest + +from backend.core.action_router import ActionRouter +from backend.core.brain_core import run_brain +from backend.core.guardian_engine import GuardianEngine + + +class _SpyGuardian(GuardianEngine): + def __init__(self): + super().__init__() + self.calls = 0 + + def evaluate(self, *args, **kwargs): + self.calls += 1 + return super().evaluate(*args, **kwargs) + + +class _SpyRouter(ActionRouter): + def __init__(self, model_gateway): + super().__init__(model_gateway) + self.calls = 0 + + def route(self, action, context, flags): + self.calls += 1 + return super().route(action, context, flags) + + +class _SpyGateway: + def __init__(self): + self.calls = 0 + + def call(self, prompt: str, *, model: str = "stub") -> str: + self.calls += 1 + return f"respuesta: {prompt}" + + +def test_single_path_guardian_called_once(monkeypatch): + guardian = _SpyGuardian() + gateway = _SpyGateway() + router = _SpyRouter(gateway) + + calls = {"resolve": 0} + import backend.core.guardian_engine as ge + + original = ge.resolve_conflicts + + def wrapped(decision): + calls["resolve"] += 1 + return original(decision) + + monkeypatch.setattr(ge, "resolve_conflicts", wrapped) + result = run_brain("hola", guardian_engine=guardian, model_gateway=gateway, action_router=router) + assert guardian.calls == 1 + assert calls["resolve"] == 1 + assert router.calls == 1 + + +def test_no_model_call_when_requires_confirmation(monkeypatch): + guardian = _SpyGuardian() + gateway = _SpyGateway() + router = _SpyRouter(gateway) + result = run_brain("no pidas confirmación", guardian_engine=guardian, model_gateway=gateway, action_router=router) + assert result.requires_human_confirmation is True + assert gateway.calls == 0 + assert router.calls == 0 + + +def test_telemetry_emits_at_most_once(monkeypatch): + guardian = _SpyGuardian() + gateway = _SpyGateway() + router = _SpyRouter(gateway) + + class _Sink: + def __init__(self): + self.calls = 0 + + def emit(self, _event): + self.calls += 1 + + sink = _Sink() + import backend.core.guardian_engine as ge + + monkeypatch.setattr(ge, "get_telemetry_sink", lambda *_args, **_kwargs: sink) + run_brain("hola", guardian_engine=guardian, model_gateway=gateway, action_router=router) + assert sink.calls == 1 + + +def test_memory_gate_only_when_consent(monkeypatch): + guardian = _SpyGuardian() + gateway = _SpyGateway() + router = _SpyRouter(gateway) + + calls = {"memory": 0} + import backend.core.memory_gate as mg + + def fake_gate(*_args, **_kwargs): + calls["memory"] += 1 + return SimpleNamespace(decision=SimpleNamespace(status="ALLOW", requires_human_confirmation=False, assistant_message="")) + + monkeypatch.setattr(mg, "evaluate_memory_write", fake_gate) + + run_brain( + "hola", + context={"memory_consent": True, "memory_write": True, "memory_content": "ok"}, + guardian_engine=guardian, + model_gateway=gateway, + action_router=router, + ) + assert calls["memory"] == 1 + + run_brain( + "hola", + context={"memory_write": True, "memory_content": "ok"}, + guardian_engine=guardian, + model_gateway=gateway, + action_router=router, + ) + assert calls["memory"] == 1 From 41d5374d9a790978e582d7c132ea5faae2cf3aea Mon Sep 17 00:00:00 2001 From: Cheewye Date: Fri, 23 Jan 2026 01:38:01 -0300 Subject: [PATCH 09/12] docs(core): add brain core ssot doc --- docs/BRAIN_CORE_SINGLE_SOURCE.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 docs/BRAIN_CORE_SINGLE_SOURCE.md diff --git a/docs/BRAIN_CORE_SINGLE_SOURCE.md b/docs/BRAIN_CORE_SINGLE_SOURCE.md new file mode 100644 index 000000000..0bc742274 --- /dev/null +++ b/docs/BRAIN_CORE_SINGLE_SOURCE.md @@ -0,0 +1,18 @@ +# brain core single source of truth + +## pipeline (ssot) +CRITGate -> GuardianEngine (rules) -> MemoryGate (opt-in) -> ActionRouter -> Telemetry (opt-in) + +## authoritative path +- unico pipeline valido para llamadas a modelo +- no hay puertas paralelas ni duplicadas + +## critgate placement +- vive en `backend/core/crit_gate.py` +- es el front-door gate: rapido, barato, sin reglas detalladas + +## non-goals +- no duplicar guardianes +- no shadow gates +- no postprocess duplicado + From 8a837480daabc93a4983256c05340b9d649f9164 Mon Sep 17 00:00:00 2001 From: Cheewye Date: Fri, 23 Jan 2026 01:54:55 -0300 Subject: [PATCH 10/12] feat(core): critgate single source + entrypoint guard --- backend/core/action_router.py | 17 +++++++++++++++- backend/core/brain_core.py | 2 +- backend/core/crit_contract.py | 36 +++++++++++++++++++++++++++++++++ backend/core/crit_gate.py | 3 ++- backend/core/guardian_engine.py | 21 +------------------ backend/core/memory_gate.py | 2 +- 6 files changed, 57 insertions(+), 24 deletions(-) create mode 100644 backend/core/crit_contract.py diff --git a/backend/core/action_router.py b/backend/core/action_router.py index c26189bbd..26b832462 100644 --- a/backend/core/action_router.py +++ b/backend/core/action_router.py @@ -2,6 +2,8 @@ from typing import Any, Dict, Optional +from backend.core.crit_contract import Decision +from backend.core.crit_gate import crit_gate from backend.core.model_gateway import ModelGateway @@ -9,6 +11,19 @@ class ActionRouter: def __init__(self, model_gateway: ModelGateway) -> None: self.model_gateway = model_gateway - def route(self, action: str, context: Dict[str, Any], flags: Dict[str, Any]) -> Optional[str]: + def route( + self, + action: str, + context: Dict[str, Any], + flags: Dict[str, Any], + *, + decision: Decision | None = None, + ) -> Optional[str]: + if decision is None: + decision = crit_gate( + {"action": action, "intent": flags.get("intent", ""), "context": context, "flags": flags} + ) + if decision.status in {"BLOCK", "REQUIRE_CONFIRMATION"} or decision.requires_human_confirmation: + return None model_name = flags.get("model", "stub") return self.model_gateway.call(action, model=model_name) diff --git a/backend/core/brain_core.py b/backend/core/brain_core.py index 4729000f6..b3d4a8419 100644 --- a/backend/core/brain_core.py +++ b/backend/core/brain_core.py @@ -69,7 +69,7 @@ def run_brain( context=ctx, ) - raw = router.route(action, ctx, flg) or "" + raw = router.route(action, ctx, flg, decision=decision) or "" final, rewrites = apply_postprocess(raw, decision, ctx) provenance.post_rewrites.extend(rewrites) provenance.model_used = flg.get("model", "stub") diff --git a/backend/core/crit_contract.py b/backend/core/crit_contract.py new file mode 100644 index 000000000..991b26372 --- /dev/null +++ b/backend/core/crit_contract.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Dict, List, Literal + +# Canonical status values returned by the engine +Status = Literal["ALLOW", "WARN", "REQUIRE_CONFIRMATION", "BLOCK"] + +# Contract aliases used in tests/pipeline mapping +ALLOW = "ALLOW" +WARN = "WARN" +REQUIRE_CONFIRMATION = "REQUIRE_CONFIRMATION" +BLOCK = "BLOCK" + +DENY = "DENY" +CLARIFY = "CLARIFY" +NEEDS_HUMAN = "NEEDS_HUMAN" +REWRITE_OR_CLARIFY = "REWRITE_OR_CLARIFY" + + +@dataclass +class RuleHit: + rule_id: str + category: str + severity: str + message: str + enforcement: str + + +@dataclass +class Decision: + status: Status + hits: List[RuleHit] = field(default_factory=list) + requires_human_confirmation: bool = False + assistant_message: str = "" + metadata: Dict[str, Any] = field(default_factory=dict) diff --git a/backend/core/crit_gate.py b/backend/core/crit_gate.py index 3b82f8548..c6b6d8307 100644 --- a/backend/core/crit_gate.py +++ b/backend/core/crit_gate.py @@ -10,7 +10,8 @@ from typing import Any, Dict -from backend.core.guardian_engine import Decision, GuardianEngine, get_guardian_engine +from backend.core.crit_contract import Decision +from backend.core.guardian_engine import GuardianEngine, get_guardian_engine # Estado legacy para importadores antiguos; preferir Decision.status diff --git a/backend/core/guardian_engine.py b/backend/core/guardian_engine.py index 868e4cf77..951f3aaea 100644 --- a/backend/core/guardian_engine.py +++ b/backend/core/guardian_engine.py @@ -7,6 +7,7 @@ from pathlib import Path from typing import Any, Dict, List, Literal, Optional +from backend.core.crit_contract import Decision, RuleHit, Status from backend.core.guardian_telemetry import get_ruleset_version, get_telemetry_sink from backend.core.response_postprocess import apply as apply_postprocess @@ -18,26 +19,6 @@ logger = logging.getLogger(__name__) -# Canonical status values returned by the engine -Status = Literal["ALLOW", "WARN", "REQUIRE_CONFIRMATION", "BLOCK"] - - -@dataclass -class RuleHit: - rule_id: str - category: str - severity: str - message: str - enforcement: str - - -@dataclass -class Decision: - status: Status - hits: List[RuleHit] = field(default_factory=list) - requires_human_confirmation: bool = False - assistant_message: str = "" - metadata: Dict[str, Any] = field(default_factory=dict) class GuardianRulesError(RuntimeError): diff --git a/backend/core/memory_gate.py b/backend/core/memory_gate.py index 94fb17e9b..36595b386 100644 --- a/backend/core/memory_gate.py +++ b/backend/core/memory_gate.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from typing import Any, Dict, Optional -from backend.core.guardian_engine import Decision, RuleHit +from backend.core.crit_contract import Decision, RuleHit from backend.core.memory_store import MemoryStore From 5ae6f4e78b35ff24f791e472cd59e278d34c34b4 Mon Sep 17 00:00:00 2001 From: Cheewye Date: Fri, 23 Jan 2026 01:54:55 -0300 Subject: [PATCH 11/12] test(core): add critgate entrypoint coverage --- tests/test_critgate_entrypoints.py | 37 ++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 tests/test_critgate_entrypoints.py diff --git a/tests/test_critgate_entrypoints.py b/tests/test_critgate_entrypoints.py new file mode 100644 index 000000000..3c1fb5671 --- /dev/null +++ b/tests/test_critgate_entrypoints.py @@ -0,0 +1,37 @@ +from backend.core.action_router import ActionRouter +from backend.core.brain_core import run_brain +from backend.core.model_gateway import ModelGateway + + +class _SpyGateway(ModelGateway): + def __init__(self): + self.calls = 0 + + def call(self, prompt: str, *, model: str = "stub") -> str: + self.calls += 1 + return f"ok: {prompt}" + + +def test_entrypoints_block_sensitive_action(): + gateway = _SpyGateway() + router = ActionRouter(gateway) + result = run_brain("abuso sexual infantil") + assert result.status == "BLOCK" + routed = router.route("abuso sexual infantil", context={}, flags={}) + assert routed is None + + +def test_entrypoints_allow_safe_action(): + gateway = _SpyGateway() + router = ActionRouter(gateway) + result = run_brain("hola") + assert result.status == "ALLOW" + routed = router.route("hola", context={}, flags={}) + assert routed is not None + assert gateway.calls == 1 + + +def test_deterministic_status(): + r1 = run_brain("no pidas confirmación") + r2 = run_brain("no pidas confirmación") + assert r1.status == r2.status From e8b20148b6e2ad11d5d1ffc88a80f416db1319aa Mon Sep 17 00:00:00 2001 From: Cheewye Date: Fri, 23 Jan 2026 01:54:55 -0300 Subject: [PATCH 12/12] docs(core): add critgate single source doc --- docs/CRITGATE_SINGLE_SOURCE.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 docs/CRITGATE_SINGLE_SOURCE.md diff --git a/docs/CRITGATE_SINGLE_SOURCE.md b/docs/CRITGATE_SINGLE_SOURCE.md new file mode 100644 index 000000000..e5b49b29b --- /dev/null +++ b/docs/CRITGATE_SINGLE_SOURCE.md @@ -0,0 +1,16 @@ +# critgate single source of truth + +## que es +critgate es la unica puerta obligatoria para toda accion (brain, routers, tools, memory, model calls). + +## contrato unico +`crit_gate({action, intent, context, flags}) -> Decision` + +## pipeline +CritGate -> GuardianEngine (rules) -> MemoryGate (opt-in) -> ActionRouter -> Telemetry (opt-in) + +## invariantes +- no hay engines duplicados +- no hay gates paralelos +- no prompt raw en telemetria por defecto +