from __future__ import annotations from .shared import * class PKGGuardrailsMixin: @staticmethod def _routing_rule_candidate_paths() -> list[Path]: candidates: list[Path] = [] configured = str(os.getenv("OPENCLAW_ROUTING_RULES_PATH", "") or "").strip() if configured: candidates.append(Path(configured).expanduser()) backend_root = Path(__file__).resolve().parents[2] project_root = Path(__file__).resolve().parents[3] candidates.extend( [ project_root / ".openclaw" / "pmflow-routing-rules.json", backend_root / ".openclaw" / "pmflow-routing-rules.json", ] ) deduped: list[Path] = [] seen: set[str] = set() for path in candidates: key = str(path) if key in seen: continue seen.add(key) deduped.append(path) return deduped @classmethod def _resolve_guardrail_target_models(cls) -> dict[str, dict[str, str]]: bundle = load_model_config() provider_defaults = bundle.get("provider_defaults") if isinstance(bundle.get("provider_defaults"), dict) else {} tier_defaults = bundle.get("tier_defaults") if isinstance(bundle.get("tier_defaults"), dict) else {} config_path = str(bundle.get("config_path") or "") specs: dict[str, dict[str, str]] = {} for tier in ("low", "high"): tier_spec = tier_defaults.get(tier) if isinstance(tier_defaults.get(tier), dict) else {} provider = str(tier_spec.get("provider") or "glm").strip() provider_spec = provider_defaults.get(provider) if isinstance(provider_defaults.get(provider), dict) else {} specs[tier] = { "tier": tier, "provider": provider, "model": str(tier_spec.get("model") or provider_spec.get("model") or "").strip(), "api_env": str(tier_spec.get("api_env") or provider_spec.get("api_env") or "").strip(), "source": "routing_file", "source_path": config_path, "rule_name": "", } return specs async def _sync_model_guardrail_entities(self) -> dict[str, Any]: counters = {"models": 0, "policies": 0, "interfaces": 0, "relations": 0, "evidences": 0} model_specs = self._resolve_guardrail_target_models() policy_entity = await self.upsert_entity( entity_type="governance_policy", entity_key=DDL_GUARDRAIL_POLICY_KEY, display_name=DDL_GUARDRAIL_POLICY_NAME, payload={ "policy_code": "ddl_via_interface_only", "enforcement_level": "hard_block", "blocked_sql_prefixes": DDL_BLOCKED_PREFIXES, "required_execution_path": "api_or_migration_interface", "enforced_by": [ "backend/app/database.py", "backend/app/services/llm.py", "backend/app/config/ai_prompts.py", ], }, source_type="governance", source_ref="backend/app/database.py", ) counters["policies"] += 1 await self.add_evidence( evidence_type="policy_guardrail", content_excerpt="Runtime DDL is blocked; model planning must route DDL via interface/migration only.", entity_id=int(policy_entity.id), source_type="governance", source_ref="backend/app/database.py", metadata={"blocked_sql_prefixes": DDL_BLOCKED_PREFIXES}, ) counters["evidences"] += 1 interface_specs = [ { "entity_key": "interface:api:/ai-steward/command/dispatch", "display_name": "POST /ai-steward/command/dispatch", "payload": {"path": "/ai-steward/command/dispatch", "kind": "api"}, "source_ref": "backend/app/api/ai_steward.py", }, { "entity_key": "interface:migration:alembic", "display_name": "Alembic Migration Interface", "payload": {"path": "alembic", "kind": "migration"}, "source_ref": "backend/alembic/versions", }, ] interface_ids: list[int] = [] for item in interface_specs: interface_entity = await self.upsert_entity( entity_type="interface", entity_key=str(item["entity_key"]), display_name=str(item["display_name"]), payload=dict(item["payload"]), source_type="governance", source_ref=str(item["source_ref"]), ) counters["interfaces"] += 1 interface_ids.append(int(interface_entity.id)) rel = await self.upsert_relation( relation_type="policy_enforced_by_interface", subject_entity_id=int(policy_entity.id), object_entity_id=int(interface_entity.id), source_type="governance", source_ref=str(item["source_ref"]), ) counters["relations"] += 1 await self.add_evidence( evidence_type="policy_guardrail", content_excerpt=f"{DDL_GUARDRAIL_POLICY_NAME} -> {item['display_name']}", relation_id=int(rel.id), source_type="governance", source_ref=str(item["source_ref"]), metadata=dict(item["payload"]), ) counters["evidences"] += 1 for tier in ("low", "high"): spec = model_specs.get(tier) if isinstance(model_specs.get(tier), dict) else {} model_name = str(spec.get("model") or "").strip() or ("qwen-plus" if tier == "low" else "MiniMax-M2.5") provider = str(spec.get("provider") or "").strip() api_env = str(spec.get("api_env") or "").strip() source = str(spec.get("source") or "env_default").strip() source_ref = str(spec.get("source_path") or spec.get("rule_name") or "llm_routing_env").strip()[:255] entity = await self.upsert_entity( entity_type="ai_model", entity_key=f"ai_model:{tier}:{model_name.lower()[:160]}", display_name=model_name[:255], payload={ "tier": tier, "provider": provider, "model": model_name, "api_env": api_env, "source": source, "routing_rule": str(spec.get("rule_name") or "").strip(), }, source_type="model_routing", source_ref=source_ref or None, ) counters["models"] += 1 rel = await self.upsert_relation( relation_type="model_must_follow_policy", subject_entity_id=int(entity.id), object_entity_id=int(policy_entity.id), source_type="governance", source_ref=source_ref or None, payload={"tier": tier, "policy_code": "ddl_via_interface_only"}, ) counters["relations"] += 1 await self.add_evidence( evidence_type="policy_guardrail", content_excerpt=f"{tier} model {model_name} must follow {DDL_GUARDRAIL_POLICY_KEY}", relation_id=int(rel.id), source_type="governance", source_ref=source_ref or None, metadata={"tier": tier, "provider": provider, "model": model_name}, ) counters["evidences"] += 1 for interface_id in interface_ids: rel = await self.upsert_relation( relation_type="model_executes_via_interface", subject_entity_id=int(entity.id), object_entity_id=int(interface_id), source_type="governance", source_ref=source_ref or None, payload={"tier": tier}, ) counters["relations"] += 1 await self.add_evidence( evidence_type="policy_guardrail", content_excerpt=f"{tier} model {model_name} executes via controlled interface", relation_id=int(rel.id), source_type="governance", source_ref=source_ref or None, metadata={"tier": tier}, ) counters["evidences"] += 1 return counters async def sync_model_guardrails_if_due( self, *, interval_hours: int = 6, ) -> dict[str, Any]: hours = max(1, int(interval_hours)) offset_raw = await self.get_ingest_offset(channel="model_guardrail", offset_key="last_model_guardrail_sync_at") last_sync_at = self._parse_iso_dt(offset_raw) now = _utc_now_naive() if last_sync_at is not None and now - last_sync_at < timedelta(hours=hours): return { "executed": False, "reason": "not_due", "last_sync_at": last_sync_at.isoformat(), "next_due_at": (last_sync_at + timedelta(hours=hours)).isoformat(), } payload = await self._sync_model_guardrail_entities() await self.enqueue_ingest_event( channel="model_guardrail", event_key=f"model_guardrail:{_utc_now_naive().strftime('%Y%m%d%H%M%S')}", payload=payload, status="done", ) await self.set_ingest_offset( channel="model_guardrail", offset_key="last_model_guardrail_sync_at", offset_value=_utc_now_naive().isoformat(), payload=payload, ) await self.db.commit() return { "executed": True, "reason": "due", "payload": payload, } async def sync_model_guardrails_now(self) -> dict[str, Any]: payload = await self._sync_model_guardrail_entities() await self.enqueue_ingest_event( channel="model_guardrail", event_key=f"model_guardrail:{_utc_now_naive().strftime('%Y%m%d%H%M%S')}", payload=payload, status="done", ) await self.set_ingest_offset( channel="model_guardrail", offset_key="last_model_guardrail_sync_at", offset_value=_utc_now_naive().isoformat(), payload=payload, ) await self.db.commit() return payload