guardian-runtime 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
guardian/__init__.py ADDED
@@ -0,0 +1,51 @@
1
+ """
2
+ Guardian Runtime — Local-first AI governance layer.
3
+
4
+ Usage:
5
+ from guardian import Guardian
6
+
7
+ guardian = Guardian.from_policy("policy.yaml")
8
+ response = guardian.complete(model="gpt-4", messages=[...])
9
+ """
10
+ from __future__ import annotations # Python 3.9 compatibility
11
+
12
+ from guardian.core.engine import GuardianEngine
13
+ from guardian.core.policy import load_policy
14
+
15
+ __version__ = "0.1.0"
16
+ __all__ = ["Guardian"]
17
+
18
+
19
+ class Guardian:
20
+ """Main entry point for Guardian Runtime."""
21
+
22
+ def __init__(self, engine: GuardianEngine):
23
+ self._engine = engine
24
+
25
+ @classmethod
26
+ def from_policy(cls, path: str) -> "Guardian":
27
+ """Load a Guardian instance from a YAML policy file."""
28
+ policy = load_policy(path)
29
+ engine = GuardianEngine(policy)
30
+ return cls(engine)
31
+
32
+ def complete(
33
+ self,
34
+ model: str,
35
+ messages: list,
36
+ agent_id: str = "default",
37
+ session_id: str | None = None,
38
+ **kwargs,
39
+ ):
40
+ """Wrap an LLM call with full Guardian governance."""
41
+ return self._engine.complete(
42
+ model=model,
43
+ messages=messages,
44
+ agent_id=agent_id,
45
+ session_id=session_id,
46
+ **kwargs,
47
+ )
48
+
49
+ def get_cost_report(self, agent_id: str = "default") -> dict:
50
+ """Return cost report for the given agent."""
51
+ return self._engine.get_cost_report(agent_id)
File without changes
guardian/cli/main.py ADDED
@@ -0,0 +1,17 @@
1
+ """Guardian CLI — entry point for all `guardian` commands."""
2
+
3
+ import click
4
+
5
+
6
+ @click.group()
7
+ @click.version_option(package_name="guardian-runtime")
8
+ def cli():
9
+ """⛨ Guardian Runtime — Local-first AI governance."""
10
+ pass
11
+
12
+
13
+ # Commands will be registered here as they are built
14
+ # from guardian.cli.init import init_command
15
+ # from guardian.cli.validate import validate_command
16
+ # from guardian.cli.status import status_command
17
+ # from guardian.cli.logs import logs_command
File without changes
@@ -0,0 +1,19 @@
1
+ """Guardian core engine — orchestrates the full request/response pipeline."""
2
+ from __future__ import annotations # Python 3.9 compatibility
3
+ # TODO (Week 6): Implement GuardianEngine.complete() pipeline
4
+
5
+
6
+ class GuardianEngine:
7
+ """Orchestrates: license check → input guard → LLM → output guard → log."""
8
+
9
+ def __init__(self, policy):
10
+ self.policy = policy
11
+ # TODO: initialize sub-components from policy
12
+
13
+ def complete(self, model, messages, agent_id="default", session_id=None, **kwargs):
14
+ """Full governed LLM call. Returns GuardianResponse."""
15
+ raise NotImplementedError("Engine not yet implemented — see ARCHITECTURE.md §4.2")
16
+
17
+ def get_cost_report(self, agent_id: str) -> dict:
18
+ """Return local cost report for the given agent."""
19
+ raise NotImplementedError
@@ -0,0 +1,21 @@
1
+ """License key validation and once-daily server sync."""
2
+ from __future__ import annotations # Python 3.9 compatibility
3
+ # TODO (Week 6): Implement LicenseManager
4
+ # See ARCHITECTURE.md §4.9 for full spec — especially fail-open and grace period logic
5
+
6
+
7
+ class LicenseManager:
8
+ """Validates license locally and syncs with guardian-ai.dev once per day."""
9
+
10
+ LICENSE_SERVER_URL = "https://guardian-ai.dev/api/validate"
11
+
12
+ def check_or_sync(self):
13
+ """Called before every guardian.complete(). Syncs if >24h since last sync."""
14
+ raise NotImplementedError
15
+
16
+ def sync_with_server(self):
17
+ """POST { license_key, checks_used } → receive { valid, plan, limit, expiry }."""
18
+ raise NotImplementedError
19
+
20
+ def is_initialized(self) -> bool:
21
+ raise NotImplementedError
@@ -0,0 +1,290 @@
1
+ """YAML policy loader and Pydantic schema validation.
2
+
3
+ Reads a guardian_policy.yaml file, validates it against a strict Pydantic V2
4
+ schema, and returns a Policy object that every Guardian component uses.
5
+
6
+ If the YAML has typos, missing fields, or wrong types, Pydantic will raise a
7
+ clear PolicyValidationError with details.
8
+
9
+ See ARCHITECTURE.md §4.7 for full specification.
10
+ """
11
+ from __future__ import annotations # Python 3.9 compatibility
12
+
13
+ from enum import Enum
14
+ from pathlib import Path
15
+ from typing import Any, Dict, List, Optional
16
+
17
+ import yaml
18
+ from pydantic import BaseModel, Field, field_validator, model_validator
19
+
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Enums for strict validation
23
+ # ---------------------------------------------------------------------------
24
+
25
+ class PIIAction(str, Enum):
26
+ """Action to take when PII is detected."""
27
+ BLOCK = "block"
28
+ REDACT = "redact"
29
+ FLAG = "flag"
30
+
31
+
32
+ class LogSink(str, Enum):
33
+ """Where to write Guardian logs."""
34
+ JSONL = "jsonl"
35
+ CONSOLE = "console"
36
+ BOTH = "both"
37
+
38
+
39
+ class LogLevel(str, Enum):
40
+ """What severity of events to log."""
41
+ ALL = "ALL"
42
+ VIOLATIONS_ONLY = "VIOLATIONS_ONLY"
43
+ HIGH_SEVERITY = "HIGH_SEVERITY"
44
+
45
+
46
+ class HallucinationProvider(str, Enum):
47
+ """Supported LLM providers for hallucination detection (BYOM)."""
48
+ OPENAI = "openai"
49
+ ANTHROPIC = "anthropic"
50
+ OLLAMA = "ollama"
51
+ GEMINI = "gemini"
52
+
53
+
54
+ class LoopAction(str, Enum):
55
+ """Action to take when a prompt loop is detected."""
56
+ BLOCK = "block"
57
+ BLOCK_AND_ALERT = "block_and_alert"
58
+
59
+
60
+ # Valid PII entity names — must match PIIType enum values in pii.py
61
+ VALID_PII_ENTITIES = frozenset({
62
+ "ssn", "credit_card", "email", "phone",
63
+ "aadhaar", "pan", "upi_id", "passport", "secret",
64
+ })
65
+
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # Exceptions
69
+ # ---------------------------------------------------------------------------
70
+
71
+ class PolicyValidationError(Exception):
72
+ """Raised when a YAML policy file fails schema validation.
73
+
74
+ Attributes:
75
+ errors: List of Pydantic validation error dicts.
76
+ """
77
+
78
+ def __init__(self, message: str, errors: Optional[List[Dict[str, Any]]] = None):
79
+ super().__init__(message)
80
+ self.errors = errors or []
81
+
82
+
83
+ # ---------------------------------------------------------------------------
84
+ # Config sub-models
85
+ # ---------------------------------------------------------------------------
86
+
87
+ class ScopeConfig(BaseModel):
88
+ """Topic-scoping configuration for the Input Guard."""
89
+ allowed_topics: List[str] = Field(default_factory=list)
90
+ block_message: str = "This topic is outside my scope."
91
+
92
+
93
+ class InputGuardConfig(BaseModel):
94
+ """Configuration for the Input Guard pipeline."""
95
+ pii_detection: bool = True
96
+ pii_entities: List[str] = Field(
97
+ default_factory=lambda: list(VALID_PII_ENTITIES),
98
+ description="List of PII entity types to detect.",
99
+ )
100
+ pii_action: PIIAction = PIIAction.BLOCK
101
+ jailbreak_detection: bool = True
102
+ scope: Optional[ScopeConfig] = None
103
+
104
+ @field_validator("pii_entities")
105
+ @classmethod
106
+ def validate_pii_entities(cls, v: List[str]) -> List[str]:
107
+ """Ensure every entity name matches a known PIIType value."""
108
+ invalid = [e for e in v if e not in VALID_PII_ENTITIES]
109
+ if invalid:
110
+ raise ValueError(
111
+ f"Unknown PII entities: {invalid}. "
112
+ f"Valid values: {sorted(VALID_PII_ENTITIES)}"
113
+ )
114
+ return v
115
+
116
+
117
+ class OutputGuardConfig(BaseModel):
118
+ """Configuration for the Output Guard pipeline."""
119
+ hallucination_check: bool = False
120
+ hallucination_provider: HallucinationProvider = HallucinationProvider.OPENAI
121
+ hallucination_model: str = "gpt-4o-mini"
122
+ pii_detection: bool = True
123
+ profanity_filter: bool = False
124
+ competitor_block: List[str] = Field(default_factory=list)
125
+
126
+
127
+ class AutoDowngradeConfig(BaseModel):
128
+ """Auto-downgrade model when budget threshold is reached."""
129
+ enabled: bool = False
130
+ threshold: float = Field(default=0.80, ge=0.0, le=1.0)
131
+ target_model: str = "gpt-3.5-turbo"
132
+
133
+
134
+ class LoopConfig(BaseModel):
135
+ """Semantic loop detection settings."""
136
+ max_retries: int = Field(default=3, ge=1)
137
+ similarity_threshold: float = Field(default=0.90, ge=0.0, le=1.0)
138
+ action: LoopAction = LoopAction.BLOCK_AND_ALERT
139
+
140
+
141
+ class CostConfig(BaseModel):
142
+ """FinOps budget and cost control settings."""
143
+ daily_budget: float = Field(default=10.00, ge=0.0)
144
+ monthly_budget: Optional[float] = Field(default=None, ge=0.0)
145
+ per_session_limit: float = Field(default=0.50, ge=0.0)
146
+ currency: str = "USD"
147
+ auto_downgrade: Optional[AutoDowngradeConfig] = None
148
+ loop_detection: Optional[LoopConfig] = None
149
+
150
+
151
+ class RateLimitConfig(BaseModel):
152
+ """Rate limit for a specific tool."""
153
+ max_calls: int = Field(ge=1)
154
+ per: str = "session" # "session" | "minute" | "hour"
155
+ cooldown_seconds: int = Field(default=0, ge=0)
156
+
157
+
158
+ class ArgRuleConfig(BaseModel):
159
+ """Argument validation rule for a tool parameter."""
160
+ type: str = "string" # "string" | "int" | "enum"
161
+ pattern: Optional[str] = None
162
+ values: Optional[List[str]] = None
163
+
164
+
165
+ class ToolConfig(BaseModel):
166
+ """Tool governance configuration."""
167
+ allowed: List[str] = Field(default_factory=list)
168
+ denied: List[str] = Field(default_factory=list)
169
+ rate_limits: Dict[str, RateLimitConfig] = Field(default_factory=dict)
170
+ argument_validation: Dict[str, Dict[str, ArgRuleConfig]] = Field(
171
+ default_factory=dict
172
+ )
173
+
174
+
175
+ class LoggingConfig(BaseModel):
176
+ """Logging configuration."""
177
+ sink: LogSink = LogSink.JSONL
178
+ log_level: LogLevel = LogLevel.ALL
179
+ retention_days: int = Field(default=30, ge=1)
180
+
181
+
182
+ class AlertConfig(BaseModel):
183
+ """Alert/notification configuration (v0.2+)."""
184
+ slack_webhook: Optional[str] = None
185
+ email: Optional[str] = None
186
+
187
+
188
+ class ComplianceConfig(BaseModel):
189
+ """Compliance framework configuration."""
190
+ frameworks: List[str] = Field(default_factory=list) # e.g. ["dpdp", "gdpr"]
191
+
192
+
193
+ # ---------------------------------------------------------------------------
194
+ # Agent-level policy
195
+ # ---------------------------------------------------------------------------
196
+
197
+ class AgentPolicy(BaseModel):
198
+ """Complete policy for a single agent."""
199
+ input_guard: Optional[InputGuardConfig] = None
200
+ output_guard: Optional[OutputGuardConfig] = None
201
+ cost: Optional[CostConfig] = None
202
+ tools: Optional[ToolConfig] = None
203
+
204
+
205
+ # ---------------------------------------------------------------------------
206
+ # Root Policy model
207
+ # ---------------------------------------------------------------------------
208
+
209
+ class Policy(BaseModel):
210
+ """Root policy model — represents a complete guardian_policy.yaml file.
211
+
212
+ Usage:
213
+ policy = load_policy("guardian_policy.yaml")
214
+ agent_config = policy.get_agent("support-bot")
215
+ """
216
+ version: str = "1.0"
217
+ name: str = "default"
218
+ environment: Optional[str] = None # "dev" | "staging" | "production"
219
+ agents: Dict[str, AgentPolicy] = Field(default_factory=dict)
220
+ logging: Optional[LoggingConfig] = None
221
+ alerts: Optional[AlertConfig] = None
222
+ compliance: Optional[ComplianceConfig] = None
223
+
224
+ @model_validator(mode="after")
225
+ def ensure_default_agent(self) -> "Policy":
226
+ """Ensure there is at least a 'default' agent entry."""
227
+ if not self.agents:
228
+ self.agents = {"default": AgentPolicy()}
229
+ return self
230
+
231
+ def get_agent(self, agent_id: str) -> AgentPolicy:
232
+ """Get configuration for a specific agent, falling back to 'default'.
233
+
234
+ Args:
235
+ agent_id: The agent identifier to look up.
236
+
237
+ Returns:
238
+ AgentPolicy for the given agent, or the 'default' agent if not found.
239
+ """
240
+ if agent_id in self.agents:
241
+ return self.agents[agent_id]
242
+ return self.agents.get("default", AgentPolicy())
243
+
244
+
245
+ # ---------------------------------------------------------------------------
246
+ # Public API
247
+ # ---------------------------------------------------------------------------
248
+
249
+ def load_policy(path: str | Path) -> Policy:
250
+ """Load and validate a Guardian YAML policy file.
251
+
252
+ Args:
253
+ path: Path to the YAML policy file.
254
+
255
+ Returns:
256
+ A fully validated Policy object.
257
+
258
+ Raises:
259
+ FileNotFoundError: If the YAML file does not exist.
260
+ PolicyValidationError: If the YAML content fails schema validation.
261
+ """
262
+ path = Path(path)
263
+
264
+ if not path.exists():
265
+ raise FileNotFoundError(f"Policy file not found: {path}")
266
+
267
+ with open(path, "r") as f:
268
+ raw = yaml.safe_load(f)
269
+
270
+ if raw is None:
271
+ raise PolicyValidationError(
272
+ f"Policy file is empty: {path}"
273
+ )
274
+
275
+ if not isinstance(raw, dict):
276
+ raise PolicyValidationError(
277
+ f"Policy file must contain a YAML mapping, got {type(raw).__name__}: {path}"
278
+ )
279
+
280
+ try:
281
+ return Policy.model_validate(raw)
282
+ except Exception as e:
283
+ # Re-wrap Pydantic ValidationError into our own exception
284
+ errors = []
285
+ if hasattr(e, "errors"):
286
+ errors = e.errors() # type: ignore[union-attr]
287
+ raise PolicyValidationError(
288
+ f"Policy validation failed for {path}: {e}",
289
+ errors=errors,
290
+ ) from e
@@ -0,0 +1,27 @@
1
+ """Local ~/.guardian/ file manager — config.json and usage.json."""
2
+ from __future__ import annotations # Python 3.9 compatibility
3
+ # TODO (Week 6): Implement LocalStorage
4
+ # See ARCHITECTURE.md §4.8 for full spec
5
+
6
+
7
+ class LocalStorage:
8
+ """Manages ~/.guardian/config.json and ~/.guardian/usage.json."""
9
+
10
+ def save_license(self, key: str, plan: str, limit: int, expiry: str | None = None):
11
+ raise NotImplementedError
12
+
13
+ def load_license(self) -> dict | None:
14
+ raise NotImplementedError
15
+
16
+ def increment_usage(self) -> int:
17
+ raise NotImplementedError
18
+
19
+ def get_usage(self) -> dict:
20
+ raise NotImplementedError
21
+
22
+ def check_usage_limit(self) -> tuple[bool, int, int]:
23
+ """Returns (within_limit, current_count, plan_limit)."""
24
+ raise NotImplementedError
25
+
26
+ def mark_synced(self, timestamp: str):
27
+ raise NotImplementedError
File without changes
@@ -0,0 +1,29 @@
1
+ """Per-model cost tables and cost estimation."""
2
+ # TODO (Week 4): Implement estimate_cost()
3
+ # See ARCHITECTURE.md §4.5 — keep this table updated as providers change pricing
4
+
5
+ # Cost per 1,000 tokens in USD
6
+ MODEL_COST_PER_1K: dict[str, dict[str, float]] = {
7
+ # OpenAI
8
+ "gpt-4o": {"input": 0.005, "output": 0.015},
9
+ "gpt-4o-mini": {"input": 0.00015, "output": 0.0006},
10
+ "gpt-4-turbo": {"input": 0.01, "output": 0.03},
11
+ "gpt-4": {"input": 0.03, "output": 0.06},
12
+ "gpt-3.5-turbo": {"input": 0.0005, "output": 0.0015},
13
+ # Anthropic
14
+ "claude-3-5-sonnet": {"input": 0.003, "output": 0.015},
15
+ "claude-3-opus": {"input": 0.015, "output": 0.075},
16
+ "claude-3-haiku": {"input": 0.00025, "output": 0.00125},
17
+ # Google
18
+ "gemini-1-5-pro": {"input": 0.00125, "output": 0.005},
19
+ "gemini-1-5-flash": {"input": 0.000075,"output": 0.0003},
20
+ }
21
+
22
+
23
+ def estimate_cost(input_tokens: int, output_tokens: int = 0, model: str = "gpt-4o") -> float:
24
+ """Estimate cost in USD for given token counts and model."""
25
+ raise NotImplementedError
26
+
27
+
28
+ def get_supported_models() -> list[str]:
29
+ return list(MODEL_COST_PER_1K.keys())
@@ -0,0 +1,14 @@
1
+ """Token counting — tiktoken wrapper."""
2
+ from __future__ import annotations # Python 3.9 compatibility
3
+ # TODO (Week 4): Implement token counting functions
4
+ # See ARCHITECTURE.md §4.5
5
+
6
+
7
+ def count_tokens(text: str, model: str = "gpt-4") -> int:
8
+ """Count tokens in text for the given model using tiktoken."""
9
+ raise NotImplementedError
10
+
11
+
12
+ def count_messages_tokens(messages: list[dict], model: str = "gpt-4") -> int:
13
+ """Count total tokens for a list of chat messages including ChatML overhead."""
14
+ raise NotImplementedError
File without changes
File without changes
@@ -0,0 +1,31 @@
1
+ """Hallucination detector — LLM-as-judge pattern."""
2
+ from __future__ import annotations # Python 3.9 compatibility
3
+ # TODO (Week 5): Implement HallucinationDetector
4
+ # See ARCHITECTURE.md §4.4.1
5
+ # Note: uses developer's OpenAI API key for the judge call, NOT ours
6
+
7
+ from dataclasses import dataclass, field
8
+
9
+
10
+ @dataclass
11
+ class HallucinationResult:
12
+ verdict: str # "grounded" | "partially_grounded" | "hallucinated"
13
+ confidence: float
14
+ unsupported_claims: list[str] = field(default_factory=list)
15
+ explanation: str = ""
16
+
17
+ @property
18
+ def is_hallucination(self) -> bool:
19
+ return self.verdict == "hallucinated"
20
+
21
+
22
+ class HallucinationDetector:
23
+ """Uses a small LLM (gpt-4o-mini) to verify response grounding in context."""
24
+
25
+ def __init__(self, judge_model: str = "gpt-4o-mini", threshold: float = 0.7):
26
+ self.judge_model = judge_model
27
+ self.threshold = threshold
28
+
29
+ def check(self, question: str, response: str, context: str) -> HallucinationResult:
30
+ """Judge whether the response is grounded in the provided context."""
31
+ raise NotImplementedError
@@ -0,0 +1,33 @@
1
+ """Jailbreak and prompt injection detector — 50+ patterns, zero external deps."""
2
+ from __future__ import annotations # Python 3.9 compatibility
3
+ # TODO (Week 4): Implement JailbreakDetector
4
+ # See ARCHITECTURE.md §4.3.2
5
+
6
+ import re
7
+ from dataclasses import dataclass
8
+
9
+
10
+ @dataclass
11
+ class JailbreakResult:
12
+ is_jailbreak: bool
13
+ confidence: float
14
+ pattern_matched: str | None
15
+ category: str | None # "dan" | "instruction_override" | "role_play" | "encoding" | "extraction"
16
+
17
+
18
+ # TODO (Week 4): Fill in all 50+ patterns from ARCHITECTURE.md §4.3.2
19
+ JAILBREAK_PATTERNS: list[tuple[str, str]] = []
20
+
21
+
22
+ class JailbreakDetector:
23
+ """Detects jailbreak attempts using compiled regex patterns."""
24
+
25
+ def __init__(self):
26
+ self._compiled = [
27
+ (re.compile(pattern, re.IGNORECASE), category)
28
+ for pattern, category in JAILBREAK_PATTERNS
29
+ ]
30
+
31
+ def detect(self, text: str) -> JailbreakResult:
32
+ """Return first matching jailbreak pattern, or is_jailbreak=False."""
33
+ raise NotImplementedError