"""Rule-based admission gates for evidence-backed skill evolution.""" from __future__ import annotations import hashlib import json import re from dataclasses import asdict, dataclass, field from datetime import datetime, timezone from typing import Any, Mapping from openspace.skill_engine.capture_contract import ( capture_contract_failures, capture_contract_ref_ids, ) from openspace.skill_engine.evidence import EvidencePacket, ResourceRef from openspace.skill_engine.signals.types import ( STATUS_ACTIONABLE_PARTIAL, STATUS_AGGREGATE_ONLY, TRIGGERABLE_EVIDENCE_STATUSES, ) from openspace.utils.logging import Logger logger = Logger.get_logger(__name__) _OUTCOMES = {"direct", "candidate", "reject", "noop", "needs_human_review"} _ENVIRONMENT_FAILURE_TERMS = ( "api key", "apikey", "openai_api_key", "anthropic_api_key", "missing key", "unauthorized", "authentication", "auth failed", "401", "403", "network outage", "network error", "dns", "connection refused", "connection reset", "service unavailable", "external service", "sandbox", "permission mode", ) _EPHEMERAL_CAPTURE_TERMS = ( "api key", "apikey", "_api_key", "secret", "token", "password", "credential", "one-time", "one time", "single-use", "temporary url", "signed url", "presigned", "session-specific", "temporary environment", "/tmp/", "/private/tmp", ) _SCRATCHPAD_EPHEMERAL_CAPTURE_TERMS = ( "/tmp/", "/private/tmp", ) _CONTEXT_EPHEMERAL_CAPTURE_TERMS = tuple( term for term in _EPHEMERAL_CAPTURE_TERMS if term not in _SCRATCHPAD_EPHEMERAL_CAPTURE_TERMS ) _DERIVED_DIVERGENCE_TERMS = ( "divergence", "diverge", "subscenario", "sub-scenario", "specialize", "specialized", "stable sub", "scope too broad", "too broad", "too wide", "different tool", "different tools", "tool combination", "cannot stay clear", "separate workflow", ) _BUGFIX_TERMS = ("bug", "fix", "broken", "failure", "failed", "repair") _PERMISSION_BYPASS_TERMS = ("bypass", "circumvent", "evade") _PERMISSION_CONTROL_TERMS = ( "permission", "approval", "authorization", "access control", "security control", ) _QUALITY_SIGNAL_EXTERNAL_ATTRIBUTIONS = { "attribution:permission", "attribution:environment", "attribution:tool_external", } @dataclass(frozen=True, slots=True) class AdmissionResult: admission_id: str decision_id: str packet_id: str outcome: str hard_failures: list[str] = field(default_factory=list) warnings: list[str] = field(default_factory=list) required_refs_checked: list[str] = field(default_factory=list) source_validation_passed: bool = False reviewed_by: str = "rule" created_at: str = "" def to_dict(self) -> dict[str, Any]: return asdict(self) @classmethod def from_mapping(cls, data: Mapping[str, Any]) -> "AdmissionResult": outcome = str(data.get("outcome") or "reject").strip().lower() if outcome not in _OUTCOMES: outcome = "reject" return cls( admission_id=str(data.get("admission_id") or ""), decision_id=str(data.get("decision_id") or ""), packet_id=str(data.get("packet_id") or ""), outcome=outcome, hard_failures=_str_list(data.get("hard_failures")), warnings=_str_list(data.get("warnings")), required_refs_checked=_str_list(data.get("required_refs_checked")), source_validation_passed=bool( data.get("source_validation_passed", False) ), reviewed_by=str(data.get("reviewed_by") or "rule"), created_at=str(data.get("created_at") or ""), ) class EvolutionAdmission: """Deterministic hard gate between decision proposals and authoring.""" def __init__( self, *, evidence_store: Any | None = None, skill_store: Any | None = None, registry: Any | None = None, allow_single_observation_capture: bool = True, ) -> None: self.evidence_store = evidence_store self.skill_store = skill_store self.registry = registry self.allow_single_observation_capture = bool( allow_single_observation_capture ) def admit( self, decision: Any, packet: EvidencePacket, job: Any | None = None, ) -> AdmissionResult: try: result = self._admit(decision, packet, job) except Exception as exc: logger.debug("Evolution admission failed", exc_info=True) result = self._result( decision, packet, outcome="reject", hard_failures=["admission_error"], warnings=[str(exc)[:500]], required_refs_checked=_claim_ref_ids(decision), ) self._persist(result) return result def _admit( self, decision: Any, packet: EvidencePacket, job: Any | None = None, ) -> AdmissionResult: action = _action(decision) checked = _claim_ref_ids(decision) hard_failures = self._base_hard_failures(decision, packet, job) warnings: list[str] = [] if action == "NOOP": warnings.extend(_str_list(_attr(decision, "reason_tags"))) noop_reason = _none_or_str(_attr(decision, "noop_reason")) if noop_reason: warnings.append(noop_reason) return self._result( decision, packet, outcome="noop", hard_failures=hard_failures, warnings=warnings, required_refs_checked=checked, ) if _candidate_policy(decision) == "reject": hard_failures.append("decision_policy_reject") hard_failures.extend(_str_list(_attr(decision, "risks"))) if hard_failures: return self._result( decision, packet, outcome="reject", hard_failures=hard_failures, warnings=warnings, required_refs_checked=checked, ) if action == "FIX": return self._admit_fix(decision, packet, checked, job=job) if action == "DERIVED": return self._admit_derived(decision, packet, checked) if action == "CAPTURED": return self._admit_captured(decision, packet, checked) return self._result( decision, packet, outcome="reject", hard_failures=["unsupported_action"], required_refs_checked=checked, ) def _base_hard_failures( self, decision: Any, packet: EvidencePacket, job: Any | None, ) -> list[str]: failures: list[str] = [] claims = list(_attr(decision, "evidence_claims") or []) if not claims: failures.append("no_evidence_claims") valid_refs = _packet_ref_ids(packet) missing_refs: list[str] = [] for index, claim in enumerate(claims): refs = _str_list(_attr(claim, "refs")) if not refs: failures.append(f"claim_{index}_missing_refs") continue missing_refs.extend(ref_id for ref_id in refs if ref_id not in valid_refs) if missing_refs: failures.append("missing_refs") failures.extend(f"missing_ref:{ref_id}" for ref_id in missing_refs) failures.extend(_quality_signal_packet_failures(packet, job)) return list(dict.fromkeys(failures)) def _admit_fix( self, decision: Any, packet: EvidencePacket, checked: list[str], *, job: Any | None = None, ) -> AdmissionResult: target_skill_ids = _target_skill_ids(decision) hard_failures: list[str] = [] warnings: list[str] = [] if not target_skill_ids: hard_failures.append("missing_target_skill") for skill_id in target_skill_ids: if not self._skill_exists(skill_id, packet): hard_failures.append("unknown_target_skill") hard_failures.append(f"unknown_target_skill:{skill_id}") skill_file_refs = _skill_file_refs(packet, target_skill_ids) lifecycle_refs = _skill_lifecycle_refs(packet, target_skill_ids) manual_fix_refs = _manual_fix_request_refs(packet) failure_refs = _failure_or_friction_refs(packet) target_state_refs = _target_skill_state_refs(packet, target_skill_ids) tool_quality_refs = list(_refs(packet, "tool_quality_record")) tool_incident_refs = list(_refs(packet, "tool_incident")) quality_signal_refs = _quality_signal_refs(packet) tool_dependency_warning = _tool_quality_dependency_warning( packet, target_skill_ids, [*tool_quality_refs, *tool_incident_refs], ) tool_dependency_confirmed = ( bool(tool_quality_refs or tool_incident_refs) and tool_dependency_warning is None ) checked.extend(ref.ref_id for ref in skill_file_refs) checked.extend(ref.ref_id for ref in lifecycle_refs) checked.extend(ref.ref_id for ref in manual_fix_refs) checked.extend(ref.ref_id for ref in failure_refs) checked.extend(ref.ref_id for ref in target_state_refs) checked.extend(ref.ref_id for ref in tool_quality_refs) checked.extend(ref.ref_id for ref in tool_incident_refs) checked.extend(ref.ref_id for ref in quality_signal_refs) if not skill_file_refs: hard_failures.append("missing_skill_file_ref") if not lifecycle_refs and not manual_fix_refs and not tool_dependency_confirmed: hard_failures.append("missing_skill_lifecycle_ref") if not failure_refs and not manual_fix_refs: hard_failures.append("missing_failure_evidence") if ( tool_quality_refs and not tool_incident_refs and not _is_quality_signal_context(packet, None) ): hard_failures.append("tool_quality_aggregate_without_incident") hard_failures.extend( _quality_signal_fix_failures( decision, packet, target_skill_ids=target_skill_ids, ) ) if hard_failures: return self._result( decision, packet, outcome="reject", hard_failures=hard_failures, warnings=warnings, required_refs_checked=checked, ) environment_refs = [ref for ref in failure_refs if _is_environment_failure(ref)] non_environment_refs = [ref for ref in failure_refs if ref not in environment_refs] if environment_refs and not non_environment_refs: warnings.append("environment_failure") warnings.extend(_environment_reason_tags(environment_refs)) return self._result( decision, packet, outcome="noop", warnings=warnings, required_refs_checked=checked, ) if tool_dependency_warning: warnings.append(tool_dependency_warning) return self._result( decision, packet, outcome="candidate", warnings=warnings, required_refs_checked=checked, ) if not manual_fix_refs and not _fix_causality_plausible(decision, packet, failure_refs): warnings.append("causality_uncertain") return self._result( decision, packet, outcome="candidate", warnings=warnings, required_refs_checked=checked, ) return self._result( decision, packet, outcome="direct", warnings=warnings, required_refs_checked=checked, ) def _admit_derived( self, decision: Any, packet: EvidencePacket, checked: list[str], ) -> AdmissionResult: target_skill_ids = _target_skill_ids(decision) hard_failures: list[str] = [] warnings: list[str] = [] if not target_skill_ids: hard_failures.append("missing_parent_skill") for skill_id in target_skill_ids: if not self._skill_exists(skill_id, packet): hard_failures.append("unknown_parent_skill") hard_failures.append(f"unknown_parent_skill:{skill_id}") skill_file_refs = _skill_file_refs(packet, target_skill_ids) checked.extend(ref.ref_id for ref in skill_file_refs) if not skill_file_refs: hard_failures.append("missing_parent_skill_file_ref") if hard_failures: return self._result( decision, packet, outcome="reject", hard_failures=hard_failures, required_refs_checked=checked, ) text = _decision_text(decision) has_divergence = _contains_any(text, _DERIVED_DIVERGENCE_TERMS) user_explicit = _is_user_explicit(decision, packet) provisional_allowed = self.allow_single_observation_capture or user_explicit if not has_divergence: warnings.append("no_derived_divergence") if _contains_any(text, _BUGFIX_TERMS) and not has_divergence: warnings.append("bugfix_should_be_fix") if not provisional_allowed: warnings.append("provisional_evolution_disabled") elif not user_explicit: warnings.append("single_observation_allowed") outcome = "candidate" if has_divergence and provisional_allowed: outcome = "direct" return self._result( decision, packet, outcome=outcome, warnings=warnings, required_refs_checked=checked, ) def _admit_captured( self, decision: Any, packet: EvidencePacket, checked: list[str], ) -> AdmissionResult: warnings: list[str] = [] contract_failures = capture_contract_failures( _attr(decision, "proposal_contract"), packet, claimed_refs=_claim_ref_ids(decision), ) if contract_failures: return self._result( decision, packet, outcome="noop", warnings=contract_failures, required_refs_checked=checked, ) if _captures_permission_bypass(decision): return self._result( decision, packet, outcome="noop", warnings=["permission_bypass_capture"], required_refs_checked=checked, ) if _existing_skill_covers(decision): return self._result( decision, packet, outcome="noop", warnings=["existing_skill_covers_workflow"], required_refs_checked=checked, ) if _capture_depends_on_ephemeral_context(packet, decision): return self._result( decision, packet, outcome="noop", warnings=["ephemeral_or_secret_dependent_capture"], required_refs_checked=checked, ) user_explicit = _is_user_explicit(decision, packet) provisional_allowed = self.allow_single_observation_capture or user_explicit if not provisional_allowed: warnings.append("provisional_evolution_disabled") elif not user_explicit: warnings.append("single_observation_allowed") outcome = "candidate" if provisional_allowed: outcome = "direct" return self._result( decision, packet, outcome=outcome, warnings=warnings, required_refs_checked=checked, source_validation_passed=True, ) def _skill_exists(self, skill_id: str, packet: EvidencePacket) -> bool: if not skill_id: return False for ref_type in ("skill_file", "skill_record", "skill_event"): for ref in _refs(packet, ref_type): values = _metadata_values(ref.metadata, "skill_id", "skill_ids") if skill_id in values or skill_id in ref.ref_id: return True for source in (self.skill_store, self.registry): if source is None: continue for method_name in ("load_record", "get_skill", "get", "load"): method = getattr(source, method_name, None) if not callable(method): continue try: if method(skill_id) is not None: return True except Exception: logger.debug("Skill lookup failed for %s", skill_id, exc_info=True) break return False def _result( self, decision: Any, packet: EvidencePacket, *, outcome: str, hard_failures: list[str] | None = None, warnings: list[str] | None = None, required_refs_checked: list[str] | None = None, source_validation_passed: bool = False, ) -> AdmissionResult: normalized_outcome = outcome if outcome in _OUTCOMES else "reject" failure_tags = list(dict.fromkeys(_str_list(hard_failures))) warning_tags = list(dict.fromkeys(_str_list(warnings))) refs_checked = list(dict.fromkeys(_str_list(required_refs_checked))) decision_id = str(_attr(decision, "decision_id") or "") packet_id = str(getattr(packet, "packet_id", "") or "") created_at = _utc_now() admission_id = "adm_" + _digest( { "decision_id": decision_id, "packet_id": packet_id, "outcome": normalized_outcome, "hard_failures": failure_tags, "warnings": warning_tags, "refs": refs_checked, "source_validation_passed": bool(source_validation_passed), } )[:16] return AdmissionResult( admission_id=admission_id, decision_id=decision_id, packet_id=packet_id, outcome=normalized_outcome, hard_failures=failure_tags, warnings=warning_tags, required_refs_checked=refs_checked, source_validation_passed=bool(source_validation_passed), reviewed_by="rule", created_at=created_at, ) def _persist(self, result: AdmissionResult) -> None: persist = getattr(self.evidence_store, "persist_admission", None) if not callable(persist): return try: persist(result) except Exception: logger.debug( "Failed to persist admission result %s", result.admission_id, exc_info=True, ) def _action(decision: Any) -> str: raw = ( _attr(decision, "proposed_action") or _attr(decision, "action_type") or _attr(decision, "evolution_type") or "" ) return str(getattr(raw, "value", raw) or "").strip().upper() def _candidate_policy(decision: Any) -> str: return str(_attr(decision, "candidate_policy") or "").strip().lower() def _target_skill_ids(decision: Any) -> list[str]: return _str_list(_attr(decision, "target_skill_ids") or _attr(decision, "target_skills")) def _claim_ref_ids(decision: Any) -> list[str]: refs: list[str] = [] for claim in list(_attr(decision, "evidence_claims") or []): refs.extend(_str_list(_attr(claim, "refs"))) return list(dict.fromkeys(refs)) def _packet_ref_ids(packet: EvidencePacket) -> set[str]: refs = { ref.ref_id for group in packet.selected_refs.values() for ref in group if ref.ref_id } refs.update(path.ref_id for path in packet.readable_paths if path.ref_id) return refs def _refs(packet: EvidencePacket, ref_type: str) -> list[ResourceRef]: return list(packet.selected_refs.get(ref_type) or []) def _quality_signal_refs(packet: EvidencePacket) -> list[ResourceRef]: return _refs(packet, "quality_signal_ref") def _is_quality_signal_context( packet: EvidencePacket, job: Any | None, ) -> bool: trigger_type = str(_attr(job, "trigger_type") or "").strip().upper() if trigger_type == "QUALITY_SIGNAL": return True if str(getattr(packet, "profile_name", "") or "") == "quality_signal": return True return False def _quality_signal_packet_failures( packet: EvidencePacket, job: Any | None, ) -> list[str]: if not _is_quality_signal_context(packet, job): return [] refs = _quality_signal_refs(packet) if not refs: return ["missing_quality_signal_ref"] failures: list[str] = [] for ref in refs: metadata = ref.metadata actionability = str(metadata.get("actionability") or "").strip() evidence_status = str(metadata.get("evidence_status") or "").strip() signal_type = str(metadata.get("signal_type") or "").strip() if actionability != "trigger_review": failures.append("quality_signal_not_trigger_review") if evidence_status == STATUS_AGGREGATE_ONLY: failures.append("quality_signal_aggregate_only") failures.append("aggregate_only_quality_source") elif evidence_status not in TRIGGERABLE_EVIDENCE_STATUSES: failures.append("quality_signal_incomplete") if signal_type == "aggregate_without_incident": failures.append("quality_signal_aggregate_only") failures.append("aggregate_only_quality_source") return list(dict.fromkeys(failures)) def _quality_signal_fix_failures( decision: Any, packet: EvidencePacket, *, target_skill_ids: list[str], ) -> list[str]: if not _is_quality_signal_context(packet, None): return [] failures: list[str] = [] reason_tags = {tag.lower() for tag in _str_list(_attr(decision, "reason_tags"))} if reason_tags.intersection(_QUALITY_SIGNAL_EXTERNAL_ATTRIBUTIONS): failures.append("attribution_external_only") has_tool_event = bool(_refs(packet, "tool_event")) has_exact_tool_evidence = bool( _refs(packet, "tool_result") or _refs(packet, "tool_incident") ) if not has_tool_event or ( not has_exact_tool_evidence and not _has_actionable_partial_quality_signal( packet, target_skill_ids=target_skill_ids, ) ): failures.append("missing_representative_tool_evidence") target_set = {item for item in target_skill_ids if item} signal_skill_ids = _quality_signal_skill_ids(packet) if target_set and signal_skill_ids and not target_set.intersection(signal_skill_ids): failures.append("quality_signal_target_skill_mismatch") return list(dict.fromkeys(failures)) def _has_actionable_partial_quality_signal( packet: EvidencePacket, *, target_skill_ids: list[str], ) -> bool: failed_tool_event_ids = { ref.ref_id for ref in _refs(packet, "tool_event") if _is_failed_tool_event_ref(ref) } skill_event_ids = {ref.ref_id for ref in _refs(packet, "skill_event")} skill_file_ids = {ref.ref_id for ref in _refs(packet, "skill_file")} target_set = {item for item in target_skill_ids if item} for ref in _quality_signal_refs(packet): actionability = str(ref.metadata.get("actionability") or "").strip() evidence_status = str(ref.metadata.get("evidence_status") or "").strip() if actionability != "trigger_review" or evidence_status != STATUS_ACTIONABLE_PARTIAL: continue backrefs = {str(item) for item in (ref.raw_backrefs or []) if item} if not failed_tool_event_ids.intersection(backrefs): continue if not skill_event_ids.intersection(backrefs): continue if not skill_file_ids.intersection(backrefs): continue if target_set and not _quality_signal_ref_skill_ids(ref).intersection(target_set): continue return True return False def _is_failed_tool_event_ref(ref: ResourceRef) -> bool: if ref.ref_type != "tool_event": return False metadata = ref.metadata success = metadata.get("success") if success is False: return True if isinstance(success, str) and success.strip().lower() in {"0", "false", "no"}: return True status = str(metadata.get("status") or "").strip().lower() if status and status not in {"ok", "passed", "success", "succeeded"}: return True permission_status = str(metadata.get("permission_status") or "").strip().lower() return permission_status in {"blocked", "denied", "permission_denied", "rejected"} def _quality_signal_skill_ids(packet: EvidencePacket) -> set[str]: skill_ids: set[str] = set() for ref in _quality_signal_refs(packet): skill_ids.update(_quality_signal_ref_skill_ids(ref)) return {item for item in skill_ids if item} def _quality_signal_ref_skill_ids(ref: ResourceRef) -> set[str]: metadata = ref.metadata skill_ids = set(_metadata_values(metadata, "skill_id", "skill_ids")) subject_type = str(metadata.get("subject_type") or "") subject_id = str(metadata.get("subject_id") or "") if subject_type == "tool_skill_relation" and subject_id: skill_ids.add(subject_id.split(":", 1)[0]) return {item for item in skill_ids if item} def _skill_file_refs(packet: EvidencePacket, skill_ids: list[str]) -> list[ResourceRef]: target_set = set(skill_ids) return [ ref for ref in _refs(packet, "skill_file") if not target_set or _metadata_values(ref.metadata, "skill_id", "skill_ids").intersection(target_set) or any(skill_id in ref.ref_id for skill_id in target_set) ] def _skill_lifecycle_refs(packet: EvidencePacket, skill_ids: list[str]) -> list[ResourceRef]: target_set = set(skill_ids) allowed = {"selected", "invoked", "applied"} refs: list[ResourceRef] = [] for ref_type in ("skill_event", "skill_record"): for ref in _refs(packet, ref_type): values = _metadata_values(ref.metadata, "skill_id", "skill_ids") if target_set and not values.intersection(target_set) and not any( skill_id in ref.ref_id for skill_id in target_set ): continue lifecycle = str( ref.metadata.get("event_type") or ref.metadata.get("lifecycle_event") or ref.metadata.get("status") or "" ).strip().lower() nested = ref.metadata.get("metadata") if not lifecycle and isinstance(nested, Mapping): lifecycle = str( nested.get("event_type") or nested.get("lifecycle_event") or "" ).strip().lower() if lifecycle in allowed: refs.append(ref) return refs def _manual_fix_request_refs(packet: EvidencePacket) -> list[ResourceRef]: refs = list(_refs(packet, "manual_request_ref")) if not refs: return [] if ( str(getattr(packet, "profile_name", "") or "") == "manual_fix_or_derive" and str(getattr(packet, "subprofile", "") or "") == "fix" ): return refs selected: list[ResourceRef] = [] for ref in refs: action = str(ref.metadata.get("action") or "").strip().lower() if action in {"fix", "repair"}: selected.append(ref) return selected def _target_skill_state_refs( packet: EvidencePacket, skill_ids: list[str], ) -> list[ResourceRef]: target_set = set(skill_ids) refs: list[ResourceRef] = [] for ref in _refs(packet, "skill_record"): values = _metadata_values(ref.metadata, "skill_id", "skill_ids") if ( not target_set or values.intersection(target_set) or any(skill_id in ref.ref_id for skill_id in target_set) ): refs.append(ref) return refs def _failure_or_friction_refs(packet: EvidencePacket) -> list[ResourceRef]: refs: list[ResourceRef] = [] for ref in _refs(packet, "runtime_snapshot"): metadata = ref.metadata status = str(metadata.get("status") or "").strip().lower() stop_reason = str(metadata.get("stop_reason") or "").strip().lower() if status and status not in {"success", "completed", "ok", "passed"}: refs.append(ref) elif stop_reason in {"max_iterations", "error", "cancelled", "incomplete"}: refs.append(ref) refs.extend(_tool_failure_refs(packet)) for ref in _refs(packet, "skill_event"): lifecycle = str(ref.metadata.get("event_type") or ref.metadata.get("status") or "").lower() if lifecycle in {"fallback", "failed", "error", "permission_denied"}: refs.append(ref) for ref in _refs(packet, "transcript_message"): text = _ref_text(ref).lower() if any(term in text for term in ("correction", "not what i asked", "wrong", "failed")): refs.append(ref) return _dedupe_refs(refs) def _tool_failure_refs(packet: EvidencePacket) -> list[ResourceRef]: refs: list[ResourceRef] = [] for ref_type in ("tool_event", "tool_result", "tool_incident"): for ref in _refs(packet, ref_type): metadata = ref.metadata status = str( metadata.get("status") or metadata.get("outcome") or metadata.get("result") or "" ).strip().lower() success = metadata.get("success") if ref_type == "tool_incident": refs.append(ref) elif status and status not in {"success", "ok", "completed", "passed"}: refs.append(ref) elif success is False: refs.append(ref) elif ( metadata.get("error_type") or metadata.get("error_message") or metadata.get("error_bucket") or metadata.get("permission_status") in {"denied", "rejected"} ): refs.append(ref) return _dedupe_refs(refs) def _is_environment_failure(ref: ResourceRef) -> bool: return _contains_any(_ref_text(ref), _ENVIRONMENT_FAILURE_TERMS) def _environment_reason_tags(refs: list[ResourceRef]) -> list[str]: tags: list[str] = [] text = "\n".join(_ref_text(ref).lower() for ref in refs) if "api key" in text or "apikey" in text or "_api_key" in text: tags.append("api_key") if "network" in text or "dns" in text or "connection" in text: tags.append("network") if "sandbox" in text or "permission mode" in text: tags.append("sandbox_or_permission_mode") if "external service" in text or "service unavailable" in text: tags.append("external_service") return tags or ["environment"] def _fix_causality_plausible( decision: Any, packet: EvidencePacket, failure_refs: list[ResourceRef], ) -> bool: if not failure_refs: return False text = _decision_text(decision).lower() if any(term in text for term in ("no_causality", "unrelated_to_skill", "external_only")): return False if any(term in text for term in ("skill", "instruction", "step", "workflow", "fix", "repair")): return True return bool(_skill_lifecycle_refs(packet, _target_skill_ids(decision))) def _tool_quality_dependency_warning( packet: EvidencePacket, target_skill_ids: list[str], tool_refs: list[ResourceRef], ) -> str | None: if not tool_refs: return None tool_variants = _tool_identity_variants_from_refs(tool_refs) if not tool_variants: return "tool_dependency_uncertain" dependency_variants = _target_skill_dependency_variants(packet, target_skill_ids) if dependency_variants and dependency_variants.intersection(tool_variants): return None if dependency_variants: return "tool_dependency_mismatch" if _target_skill_text_mentions_tool(packet, target_skill_ids, tool_variants): return None return "tool_dependency_uncertain" def _target_skill_dependency_variants( packet: EvidencePacket, target_skill_ids: list[str], ) -> set[str]: variants: set[str] = set() for ref in _target_skill_state_refs(packet, target_skill_ids): for value in _metadata_tool_values(ref.metadata): variants.update(_tool_key_variants(value)) return variants def _metadata_tool_values(metadata: Mapping[str, Any]) -> set[str]: keys = { "tool_dependency", "tool_dependencies", "tool_key", "tool_keys", "critical_tool", "critical_tools", "allowed-tools", "allowed_tools", } values: set[str] = set() for key, value in metadata.items(): if key in keys: values.update(_split_tool_values(value)) if isinstance(value, Mapping): values.update(_metadata_tool_values(value)) return values def _split_tool_values(value: Any) -> set[str]: if value is None: return set() if isinstance(value, str): raw = value.strip() if not raw: return set() parts = { item.strip() for item in _split_tool_value_tokens(raw) if item.strip() } parts.add(raw) return parts if isinstance(value, Mapping): return _metadata_tool_values(value) if isinstance(value, (list, tuple, set)): values: set[str] = set() for item in value: values.update(_split_tool_values(item)) return values text = str(value).strip() return {text} if text else set() def _split_tool_value_tokens(value: str) -> list[str]: return re.split(r"[\s,;\n]+", value) def _tool_identity_variants_from_refs(refs: list[ResourceRef]) -> set[str]: variants: set[str] = set() for ref in refs: metadata = ref.metadata raw_key = metadata.get("tool_key") if raw_key: variants.update(_tool_key_variants(raw_key)) tool_name = metadata.get("tool_name") backend = metadata.get("backend") server = metadata.get("server") or metadata.get("server_name") if tool_name: variants.update(_tool_key_variants(tool_name)) if backend: variants.update( _tool_key_variants( f"{backend}:{server or 'default'}:{tool_name}" ) ) variants.update(_tool_key_variants(_tool_key_from_ref_id(ref.ref_id))) return {item for item in variants if item} def _tool_key_from_ref_id(ref_id: str) -> str: parts = str(ref_id or "").split(":") if len(parts) >= 4 and parts[0] in {"tool_quality_record", "tool_incident"}: return ":".join(parts[1:4]) return "" def _tool_key_variants(value: Any) -> set[str]: text = str(value or "").strip().lower() if not text: return set() variants = {text} parts = [part for part in text.split(":") if part] if len(parts) >= 3: backend, server, tool_name = parts[0], parts[1], parts[2] variants.add(f"{backend}:{server}:{tool_name}") variants.add(f"{backend}:{tool_name}") variants.add(tool_name) elif len(parts) == 2: backend, tool_name = parts variants.add(f"{backend}:default:{tool_name}") variants.add(f"{backend}:{tool_name}") variants.add(tool_name) elif len(parts) == 1: variants.add(parts[0]) return variants def _target_skill_text_mentions_tool( packet: EvidencePacket, target_skill_ids: list[str], tool_variants: set[str], ) -> bool: names = {variant.split(":")[-1] for variant in tool_variants if variant} names.update(tool_variants) names = {name for name in names if len(name) >= 3} if not names: return False text_parts: list[str] = [] for ref in _skill_file_refs(packet, target_skill_ids): text_parts.append(_ref_text(ref).lower()) for ref in _target_skill_state_refs(packet, target_skill_ids): text_parts.append(_ref_text(ref).lower()) text = "\n".join(text_parts) return any(name in text for name in names) def _capture_depends_on_ephemeral_context( packet: EvidencePacket, decision: Any, ) -> bool: cited_ref_ids = set( capture_contract_ref_ids(_attr(decision, "proposal_contract")) ) if cited_ref_ids: cited_refs = [ ref for refs in packet.selected_refs.values() for ref in refs if ref.ref_id in cited_ref_ids ] return any( bool(getattr(ref, "contains_secret", False)) or _contains_ephemeral_term( _ref_text(ref), _CONTEXT_EPHEMERAL_CAPTURE_TERMS, ) for ref in cited_refs ) # Legacy decisions without a source-validation contract retain the # conservative packet-wide fallback. if _contains_ephemeral_term( _decision_text(decision), _EPHEMERAL_CAPTURE_TERMS, ): return True manual_text = "\n".join(_ref_text(ref) for ref in _refs(packet, "manual_request_ref")) if _contains_ephemeral_term(manual_text, _EPHEMERAL_CAPTURE_TERMS): return True parts: list[str] = [] for ref_type in ( "runtime_snapshot", "transcript_message", "tool_event", "tool_result", "file_history", ): for ref in _refs(packet, ref_type): if getattr(ref, "contains_secret", False): return True parts.append(_ref_text(ref)) return _contains_ephemeral_term( "\n".join(parts), _CONTEXT_EPHEMERAL_CAPTURE_TERMS, ) def _captures_permission_bypass(decision: Any) -> bool: contract = _attr(decision, "proposal_contract") contract_parts: list[str] = [] if isinstance(contract, Mapping): contract_parts.append(str(contract.get("capability") or "")) contract_parts.extend(_str_list(contract.get("preconditions"))) contract_parts.extend(_str_list(contract.get("limitations"))) text = "\n".join([_decision_text(decision), *contract_parts]).lower() return bool( any(term in text for term in _PERMISSION_BYPASS_TERMS) and any(term in text for term in _PERMISSION_CONTROL_TERMS) ) def _is_user_explicit(decision: Any, packet: EvidencePacket) -> bool: if _refs(packet, "manual_request_ref"): return True recurrence = str(_attr(decision, "recurrence") or "").lower() if recurrence == "user_explicit": return True tags = { str(tag).strip().lower() for tag in _str_list(_attr(decision, "reason_tags")) } return bool(tags & {"user_explicit", "user_requested", "manual"}) def _existing_skill_covers(decision: Any) -> bool: text = _decision_text(decision).lower() return "existing_skill_covers" in text or "covered_by_existing_skill" in text def _dedupe_refs(refs: list[ResourceRef]) -> list[ResourceRef]: seen: set[str] = set() result: list[ResourceRef] = [] for ref in refs: key = ref.ref_id if key in seen: continue seen.add(key) result.append(ref) return result def _decision_text(decision: Any) -> str: parts: list[str] = [] for name in ("reason_summary", "noop_reason"): value = _attr(decision, name) if value: parts.append(str(value)) parts.extend(_str_list(_attr(decision, "reason_tags"))) parts.extend(_str_list(_attr(decision, "risks"))) for claim in list(_attr(decision, "evidence_claims") or []): value = _attr(claim, "claim") if value: parts.append(str(value)) return "\n".join(parts) def _contains_any(text: str, terms: tuple[str, ...]) -> bool: normalized = str(text or "").lower() return any(term in normalized for term in terms) def _contains_ephemeral_term(text: str, terms: tuple[str, ...]) -> bool: normalized = str(text or "").lower() pluralizable = { "api key", "apikey", "_api_key", "secret", "token", "password", "credential", } for term in terms: if term.startswith("/"): if term in normalized: return True continue suffix = "s?" if term in pluralizable else "" pattern = rf"(? str: return "\n".join( [ str(ref.preview or ""), json.dumps(ref.metadata, sort_keys=True, default=str), ] ) def _metadata_values(metadata: Mapping[str, Any], *keys: str) -> set[str]: values: set[str] = set() for key in keys: value = metadata.get(key) if isinstance(value, str) and value: values.add(value) elif isinstance(value, (list, tuple, set)): values.update(str(item) for item in value if str(item)) nested = metadata.get("metadata") if isinstance(nested, Mapping): values.update(_metadata_values(nested, *keys)) return values def _attr(value: Any, name: str) -> Any: if isinstance(value, Mapping): return value.get(name) return getattr(value, name, None) def _none_or_str(value: Any) -> str | None: if value is None: return None text = str(value) return text if text else None def _str_list(value: Any) -> list[str]: if value is None: return [] if isinstance(value, str): return [value] if value else [] if isinstance(value, (list, tuple, set)): return [str(item) for item in value if str(item)] return [] def _digest(payload: Any) -> str: return hashlib.sha256( json.dumps(payload, sort_keys=True, default=str).encode("utf-8") ).hexdigest() def _utc_now() -> str: return datetime.now(timezone.utc).isoformat()