Files

1 line
15 KiB
JSON

{"repo": "cheater121/rbacx", "files": {"examples/ai_demo/demo.py": {"f3aaf70a2682636a0668872ab6854fe9a86336b5": {"t": 1785674544.6141346, "n": 2202, "h": "83117a98fe3db806", "c": "# pip install rbacx[ai]\nimport asyncio\nimport json\n\nfrom rbacx.ai import AIPolicy\n\nAPI_KEY = \"sk-...\" # ← insert your API key here\nSCHEMA = \"openapi.json\" # ← path to your schema (JSON or YAML)\nMODEL = \"gpt-5.4-mini\" # gpt-5 / claude-3-5-sonnet / llama3 / etc.\nBASE_URL = None # None = OpenAI; for other providers use:\n# \"https://openrouter.ai/api/v1\", etc.\n\n\nasync def main():\n ai = AIPolicy(api_key=API_KEY, model=MODEL, base_url=BASE_URL)\n\n # ── 1. Generate policy from schema ───────────────────────────────\n print(\"Generating policy...\")\n result = await ai.from_schema(\n SCHEMA,\n context=\"describe your app here, e.g. 'SaaS B2B, roles: admin/user'\",\n explain=True, # human-readable explanations of rules\n raw=True, # raw LLM response for debugging\n )\n\n print(\"\\n── Generated policy ──\")\n print(json.dumps(result.dsl, indent=2))\n\n if result.warnings:\n print(\"\\n── Lint warnings ──\")\n for w in result.warnings:\n print(\" •\", w)\n\n if result.explanation:\n print(\"\\n── Rule explanations ──\")\n for rule_id, text in result.explanation.items():\n print(f\" {rule_id}: {text}\")\n\n # ── 2. Refinement ────────────────────────────────────────────────\n print(\"\\nRefining...\")\n result = await ai.refine_policy(\"viewers should only be able to read, never write or delete\")\n print(\"Refined — rules:\", [r[\"id\"] for r in result.dsl.get(\"rules\", [])])\n\n # ── 3. Explain decision ──────────────────────────────────────────\n print(\"\\nExplaining a decision...\")\n expl = await ai.explain_decision(\n policy=result.dsl,\n input={\n \"subject\": {\"id\": \"user:1\", \"roles\": [\"viewer\"]},\n \"action\": \"delete\",\n \"resource\": {\"type\": \"doc\", \"id\": \"doc:42\"},\n },\n )\n verdict = \"ALLOWED\" if expl.decision.allowed else \"DENIED\"\n print(f\"Decision : {verdict} (rule: {expl.decision.rule_id})\")\n print(f\"Why : {expl.human}\")\n\n # ── 4. Save the policy ───────────────────────────────────────────\n with open(\"policy.json\", \"w\") as f:\n json.dump(result.dsl, f, indent=2)\n print(\"\\nSaved → policy.json\")\n\n\nasyncio.run(main())\n"}}, "src/rbacx/ai/__init__.py": {"f3aaf70a2682636a0668872ab6854fe9a86336b5": {"t": 1785674544.7645342, "n": 1408, "h": "9809e20eac4648b5", "c": "\"\"\"rbacx AI Policy Authoring System.\n\nOptional module — install with::\n\n pip install rbacx[ai]\n\nProvides AI-assisted policy generation, iterative refinement, and\nhuman-readable decision explanations using any OpenAI-compatible LLM.\n\nQuick start::\n\n from rbacx.ai import AIPolicy\n\n ai = AIPolicy(api_key=\"sk-...\", model=\"gpt-4o\")\n\n # Generate a policy from an OpenAPI schema\n result = await ai.from_schema(\"openapi.json\", context=\"SaaS B2B\")\n\n # Use the policy with Guard\n from rbacx.core.engine import Guard\n guard = Guard(result.dsl)\n\n # Refine iteratively\n result2 = await ai.refine_policy(\"deny delete for viewer role\")\n\n # Explain a specific decision\n expl = await ai.explain_decision(\n policy=result.dsl,\n input={\n \"subject\": {\"id\": \"u1\", \"roles\": [\"viewer\"]},\n \"action\": \"delete\",\n \"resource\": {\"type\": \"doc\", \"id\": \"d1\"},\n },\n )\n print(expl.decision.allowed) # False\n print(expl.human) # plain-English explanation\n\"\"\"\n\nfrom rbacx.ai._result import DecisionExplanation, PolicyResult\nfrom rbacx.ai.exceptions import (\n PolicyGenerationError,\n SchemaParseError,\n ValidationRetryError,\n)\nfrom rbacx.ai.policy import AIPolicy\n\n__all__ = [\n \"AIPolicy\",\n \"PolicyResult\",\n \"DecisionExplanation\",\n \"SchemaParseError\",\n \"ValidationRetryError\",\n \"PolicyGenerationError\",\n]\n"}}, "src/rbacx/ai/policy.py": {"f3aaf70a2682636a0668872ab6854fe9a86336b5": {"t": 1785674547.9872158, "n": 10336, "h": "48d51b94f371ed92", "c": "\"\"\"AIPolicy: main entry point for the AI Policy Authoring System.\"\"\"\n\nfrom pathlib import Path\nfrom typing import Any\n\nfrom rbacx.ai._client import LLMClient\nfrom rbacx.ai._explainer import ExplainGenerator, PolicyExplainer\nfrom rbacx.ai._generator import PolicyGenerator\nfrom rbacx.ai._prompt import PromptBuilder\nfrom rbacx.ai._refinement import RefinementSession\nfrom rbacx.ai._result import DecisionExplanation, PolicyResult\nfrom rbacx.ai._schema_parser import SchemaParser\nfrom rbacx.ai._validator import PolicyValidator\nfrom rbacx.ai.exceptions import PolicyGenerationError\n\n\nclass AIPolicy:\n \"\"\"AI Policy Authoring System — main entry point.\n\n Creates one :class:`~rbacx.ai._client.LLMClient` instance that is\n reused across all operations. After :meth:`from_schema` is called a\n :class:`~rbacx.ai._refinement.RefinementSession` is created internally\n and can be driven forward via :meth:`refine_policy`.\n\n Supports any OpenAI-compatible provider through the ``base_url``\n parameter — standard OpenAI, OpenRouter, Ollama, Azure OpenAI, etc.\n\n Args:\n api_key: API key for the LLM provider.\n model: model identifier, e.g. ``\"gpt-4o\"``,\n ``\"anthropic/claude-3-5-sonnet\"`` (OpenRouter),\n ``\"llama3\"`` (Ollama).\n base_url: optional base URL override. ``None`` uses the standard\n OpenAI endpoint. Examples:\n ``\"https://openrouter.ai/api/v1\"``,\n ``\"http://localhost:11434/v1\"`` (Ollama).\n timeout: HTTP request timeout in seconds passed to the SDK.\n\n Example::\n\n ai = AIPolicy(api_key=\"sk-...\", model=\"gpt-4o\")\n result = await ai.from_schema(\"openapi.json\", context=\"SaaS B2B\")\n result2 = await ai.refine_policy(feedback=\"deny delete for viewers\")\n expl = await ai.explain_decision(\n policy=result.dsl,\n input={\n \"subject\": {\"id\": \"u1\", \"roles\": [\"viewer\"]},\n \"action\": \"delete\",\n \"resource\": {\"type\": \"doc\", \"id\": \"d1\"},\n },\n )\n \"\"\"\n\n def __init__(\n self,\n api_key: str,\n model: str,\n *,\n base_url: str | None = None,\n timeout: float = 60.0,\n ) -> None:\n self._client = LLMClient(\n api_key=api_key,\n model=model,\n base_url=base_url,\n timeout=timeout,\n )\n self._generator = PolicyGenerator(self._client)\n self._validator = PolicyValidator()\n self._explainer = PolicyExplainer(self._client)\n self._explain_gen = ExplainGenerator(self._client)\n self._session: RefinementSession | None = None\n\n # ------------------------------------------------------------------\n # Public API\n # ------------------------------------------------------------------\n\n async def from_schema(\n self,\n schema: Path | str | dict[str, Any],\n *,\n context: str = \"\",\n safe_mode: bool = True,\n compile: bool = False,\n explain: bool = False,\n raw: bool = False,\n ) -> PolicyResult:\n \"\"\"Generate an rbacx policy from an API schema.\n\n Resets any existing :class:`~rbacx.ai._refinement.RefinementSession`\n and creates a new one seeded with the generated policy and the\n conversation messages used during generation.\n\n Args:\n schema: OpenAPI 3.x or 2.0 schema supplied as:\n\n * a :class:`pathlib.Path` to a ``.json`` or ``.yaml`` file,\n * a file-path string,\n * a raw JSON string, or\n * a pre-loaded ``dict``.\n\n context: free-form domain description to guide generation,\n e.g. ``\"SaaS B2B, tenant isolation, admins per-org\"``.\n safe_mode: run the validate → retry on failure → lint pipeline.\n compile: compile the policy via the rbacx compiler and include\n the result in :attr:`PolicyResult.compiled`. Raises\n :exc:`PolicyGenerationError` if the compiler is\n unavailable.\n explain: request per-rule human explanations (one extra LLM\n call). Result is in :attr:`PolicyResult.explanation`.\n raw: include the raw LLM output string in\n :attr:`PolicyResult.raw` for debugging.\n\n Returns:\n :class:`~rbacx.ai._result.PolicyResult`.\n\n Raises:\n SchemaParseError: unrecognised schema format or unreadable file.\n PolicyGenerationError: JSON parse failure or ``compile=True``\n with unavailable compiler.\n ValidationRetryError: ``safe_mode=True`` and both validation\n attempts fail.\n \"\"\"\n normalized = SchemaParser.parse(schema)\n messages = PromptBuilder.build_generation(normalized, context)\n\n policy_dict, raw_output = await self._generator.generate(messages, safe_mode=safe_mode)\n warnings = self._validator.lint(policy_dict)\n compiled = self._compile(policy_dict) if compile else None\n explanation = await self._explain_gen.explain_rules(policy_dict) if explain else None\n\n # Seed refinement session with generation messages as conversation base\n self._session = RefinementSession(\n client=self._client,\n generator=self._generator,\n validator=self._validator,\n initial_policy=policy_dict,\n initial_messages=messages,\n )\n\n return PolicyResult(\n dsl=policy_dict,\n warnings=warnings,\n compiled=compiled,\n explanation=explanation,\n raw=raw_output if raw else None,\n )\n\n async def refine_policy(\n self,\n feedback: str,\n *,\n policy: dict[str, Any] | None = None,\n compile: bool = False,\n ) -> PolicyResult:\n \"\"\"Refine a policy with natural-language feedback.\n\n If *policy* is provided the current session is reset to that policy\n as the new starting point before applying *feedback*. If *policy*\n is ``None`` the existing session is continued.\n\n Always runs the ``safe_mode`` validation pipeline internally.\n\n Args:\n feedback: natural-language refinement instruction, e.g.\n ``\"deny delete for viewer role\"``.\n policy: optional policy dict to reset the session to before\n refining. If ``None`` and no session exists,\n :exc:`RuntimeError` is raised.\n compile: compile the refined policy. Raises\n :exc:`PolicyGenerationError` if compiler unavailable.\n\n Returns:\n :class:`~rbacx.ai._result.PolicyResult` with the refined policy.\n\n Raises:\n RuntimeError: called before :meth:`from_schema` with no\n *policy* argument.\n ValidationRetryError: validation fails after retry.\n PolicyGenerationError: JSON parse error or compiler unavailable.\n \"\"\"\n if policy is not None:\n self._session = self._make_session_from_policy(policy)\n\n if self._session is None:\n raise RuntimeError(\n \"No active policy session. \"\n \"Call from_schema() first, or pass policy= to refine_policy().\"\n )\n\n return await self._session.refine(feedback, compile=compile)\n\n async def explain_decision(\n self,\n policy: dict[str, Any],\n input: dict[str, Any],\n ) -> DecisionExplanation:\n \"\"\"Explain a specific access decision using Guard + LLM.\n\n The decision is evaluated **deterministically** by a minimal\n :class:`~rbacx.core.engine.Guard` instance — the LLM is never asked\n to decide allow/deny. The LLM only produces the human-readable\n explanation of *why*.\n\n Args:\n policy: valid rbacx policy dict.\n input: access-request dict::\n\n {\n \"subject\": {\n \"id\": str,\n \"roles\": list[str], # optional\n \"attrs\": dict, # optional\n },\n \"action\": str,\n \"resource\": {\n \"type\": str,\n \"id\": str | None, # optional\n \"attrs\": dict, # optional\n },\n }\n\n Returns:\n :class:`~rbacx.ai._result.DecisionExplanation` with the\n authoritative :class:`~rbacx.core.decision.Decision` and a\n plain-English explanation.\n\n Raises:\n PolicyGenerationError: *input* is malformed or missing required\n fields.\n \"\"\"\n return await self._explainer.explain_decision(policy, input)\n\n # ------------------------------------------------------------------\n # Internal helpers\n # ------------------------------------------------------------------\n\n def _make_session_from_policy(self, policy: dict[str, Any]) -> RefinementSession:\n \"\"\"Create a new :class:`RefinementSession` from an explicit policy.\n\n Uses :meth:`PromptBuilder.build_refine` with empty history to\n produce minimal seed messages so the LLM has context about the\n starting policy.\n \"\"\"\n messages = PromptBuilder.build_refine([], policy, \"\")\n return RefinementSession(\n client=self._client,\n generator=self._generator,\n validator=self._validator,\n initial_policy=policy,\n initial_messages=messages,\n )\n\n @staticmethod\n def _compile(policy: dict[str, Any]) -> Any:\n \"\"\"Compile *policy*. Raises :exc:`PolicyGenerationError` if unavailable.\"\"\"\n try:\n from rbacx.core.compiler import compile as compile_policy # noqa: PLC0415\n except Exception as exc:\n raise PolicyGenerationError(\n \"compile=True requested but the rbacx compiler is unavailable. \"\n \"This is unexpected — ensure rbacx is installed correctly.\",\n cause=exc,\n ) from exc\n return compile_policy(policy)\n"}}}}