246 lines
10 KiB
Python
246 lines
10 KiB
Python
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]
|
|
candidates.extend(
|
|
[
|
|
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 "GLM-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
|