ai-parrot-semantic 1.3.2__tar.gz

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.
Files changed (65) hide show
  1. ai_parrot_semantic-1.3.2/PKG-INFO +40 -0
  2. ai_parrot_semantic-1.3.2/README.md +24 -0
  3. ai_parrot_semantic-1.3.2/pyproject.toml +30 -0
  4. ai_parrot_semantic-1.3.2/setup.cfg +4 -0
  5. ai_parrot_semantic-1.3.2/src/ai_parrot_semantic.egg-info/PKG-INFO +40 -0
  6. ai_parrot_semantic-1.3.2/src/ai_parrot_semantic.egg-info/SOURCES.txt +63 -0
  7. ai_parrot_semantic-1.3.2/src/ai_parrot_semantic.egg-info/dependency_links.txt +1 -0
  8. ai_parrot_semantic-1.3.2/src/ai_parrot_semantic.egg-info/requires.txt +10 -0
  9. ai_parrot_semantic-1.3.2/src/ai_parrot_semantic.egg-info/top_level.txt +1 -0
  10. ai_parrot_semantic-1.3.2/src/parrot/semantic/__init__.py +7 -0
  11. ai_parrot_semantic-1.3.2/src/parrot/semantic/audit.py +206 -0
  12. ai_parrot_semantic-1.3.2/src/parrot/semantic/bootstrap.py +191 -0
  13. ai_parrot_semantic-1.3.2/src/parrot/semantic/cache.py +83 -0
  14. ai_parrot_semantic-1.3.2/src/parrot/semantic/catalog.py +196 -0
  15. ai_parrot_semantic-1.3.2/src/parrot/semantic/context.py +118 -0
  16. ai_parrot_semantic-1.3.2/src/parrot/semantic/engines/__init__.py +46 -0
  17. ai_parrot_semantic-1.3.2/src/parrot/semantic/engines/native.py +541 -0
  18. ai_parrot_semantic-1.3.2/src/parrot/semantic/errors.py +40 -0
  19. ai_parrot_semantic-1.3.2/src/parrot/semantic/execution.py +95 -0
  20. ai_parrot_semantic-1.3.2/src/parrot/semantic/governance/__init__.py +1 -0
  21. ai_parrot_semantic-1.3.2/src/parrot/semantic/governance/guard.py +190 -0
  22. ai_parrot_semantic-1.3.2/src/parrot/semantic/governance/lineage.py +192 -0
  23. ai_parrot_semantic-1.3.2/src/parrot/semantic/governance/masking.py +151 -0
  24. ai_parrot_semantic-1.3.2/src/parrot/semantic/governance/pbac.py +120 -0
  25. ai_parrot_semantic-1.3.2/src/parrot/semantic/governance/pipeline.py +338 -0
  26. ai_parrot_semantic-1.3.2/src/parrot/semantic/governance/rls.py +233 -0
  27. ai_parrot_semantic-1.3.2/src/parrot/semantic/handoff.py +247 -0
  28. ai_parrot_semantic-1.3.2/src/parrot/semantic/loader.py +314 -0
  29. ai_parrot_semantic-1.3.2/src/parrot/semantic/model.py +396 -0
  30. ai_parrot_semantic-1.3.2/src/parrot/semantic/models.py +251 -0
  31. ai_parrot_semantic-1.3.2/src/parrot/semantic/ossie.py +55 -0
  32. ai_parrot_semantic-1.3.2/src/parrot/semantic/policy.py +277 -0
  33. ai_parrot_semantic-1.3.2/src/parrot/semantic/results.py +180 -0
  34. ai_parrot_semantic-1.3.2/src/parrot/semantic/schemas/ossie-0.1.1.schema.json +345 -0
  35. ai_parrot_semantic-1.3.2/src/parrot/semantic/sql/semantic_audit_events.sql +16 -0
  36. ai_parrot_semantic-1.3.2/src/parrot/semantic/toolkit.py +817 -0
  37. ai_parrot_semantic-1.3.2/src/parrot/semantic/version.py +3 -0
  38. ai_parrot_semantic-1.3.2/tests/test_audit.py +177 -0
  39. ai_parrot_semantic-1.3.2/tests/test_bootstrap.py +132 -0
  40. ai_parrot_semantic-1.3.2/tests/test_cache.py +39 -0
  41. ai_parrot_semantic-1.3.2/tests/test_catalog.py +121 -0
  42. ai_parrot_semantic-1.3.2/tests/test_context.py +127 -0
  43. ai_parrot_semantic-1.3.2/tests/test_e2e_governed.py +249 -0
  44. ai_parrot_semantic-1.3.2/tests/test_e2e_handoff.py +484 -0
  45. ai_parrot_semantic-1.3.2/tests/test_example_agent.py +215 -0
  46. ai_parrot_semantic-1.3.2/tests/test_execution.py +93 -0
  47. ai_parrot_semantic-1.3.2/tests/test_handoff_contract.py +225 -0
  48. ai_parrot_semantic-1.3.2/tests/test_lineage.py +94 -0
  49. ai_parrot_semantic-1.3.2/tests/test_loader.py +94 -0
  50. ai_parrot_semantic-1.3.2/tests/test_masking.py +121 -0
  51. ai_parrot_semantic-1.3.2/tests/test_mcp_local.py +143 -0
  52. ai_parrot_semantic-1.3.2/tests/test_model_graph.py +171 -0
  53. ai_parrot_semantic-1.3.2/tests/test_models.py +95 -0
  54. ai_parrot_semantic-1.3.2/tests/test_native_compiler.py +150 -0
  55. ai_parrot_semantic-1.3.2/tests/test_native_extensions.py +218 -0
  56. ai_parrot_semantic-1.3.2/tests/test_ossie.py +51 -0
  57. ai_parrot_semantic-1.3.2/tests/test_packaging.py +73 -0
  58. ai_parrot_semantic-1.3.2/tests/test_pbac.py +126 -0
  59. ai_parrot_semantic-1.3.2/tests/test_pipeline.py +146 -0
  60. ai_parrot_semantic-1.3.2/tests/test_policy.py +136 -0
  61. ai_parrot_semantic-1.3.2/tests/test_positive_guard.py +179 -0
  62. ai_parrot_semantic-1.3.2/tests/test_results.py +115 -0
  63. ai_parrot_semantic-1.3.2/tests/test_rls_ownership.py +94 -0
  64. ai_parrot_semantic-1.3.2/tests/test_rls_pushdown.py +185 -0
  65. ai_parrot_semantic-1.3.2/tests/test_toolkit.py +74 -0
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.4
2
+ Name: ai-parrot-semantic
3
+ Version: 1.3.2
4
+ Summary: Governed semantic metric queries for AI-Parrot
5
+ License-Expression: MIT
6
+ Requires-Python: <3.14,>=3.11
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: ai-parrot
9
+ Requires-Dist: sqlglot<31,>=30.1
10
+ Requires-Dist: jsonschema>=4.20
11
+ Requires-Dist: pyyaml>=6
12
+ Provides-Extra: duckdb
13
+ Requires-Dist: duckdb>=1.0; extra == "duckdb"
14
+ Provides-Extra: bigquery
15
+ Requires-Dist: google-cloud-bigquery>=3.30; extra == "bigquery"
16
+
17
+ # ai-parrot-semantic
18
+
19
+ MIT satellite: native sqlglot compilation and ai-parrot governance.
20
+ Install: `uv pip install 'ai-parrot-semantic[duckdb]'`.
21
+
22
+ ## Layering
23
+
24
+ Generic semantic queries and the future accounting extension share this namespace.
25
+ The accounting extension must remain independently importable.
26
+
27
+ ## Governance
28
+
29
+ Models are governed by default and require a trusted `PermissionContext` plus an
30
+ available PBAC evaluator; a policy may explicitly set `governed: false` for an
31
+ ungoverned model. Hosts provide the PBAC factory and may opt in to a guarded,
32
+ subject/session-scoped dataset sink. A plain dataset manager is not a valid
33
+ sink.
34
+
35
+ The current native compiler supports declared metrics, dimensions, time grains,
36
+ declared one-to-many joins with fan-out rejection, filtered measures, and
37
+ derived metrics. The `duckdb` extra provides the local execution backend; the
38
+ `bigquery` extra provides BigQuery integration. See
39
+ [`docs/tools/semantic-layer.md`](../../docs/tools/semantic-layer.md) for the
40
+ user-facing semantic-layer documentation.
@@ -0,0 +1,24 @@
1
+ # ai-parrot-semantic
2
+
3
+ MIT satellite: native sqlglot compilation and ai-parrot governance.
4
+ Install: `uv pip install 'ai-parrot-semantic[duckdb]'`.
5
+
6
+ ## Layering
7
+
8
+ Generic semantic queries and the future accounting extension share this namespace.
9
+ The accounting extension must remain independently importable.
10
+
11
+ ## Governance
12
+
13
+ Models are governed by default and require a trusted `PermissionContext` plus an
14
+ available PBAC evaluator; a policy may explicitly set `governed: false` for an
15
+ ungoverned model. Hosts provide the PBAC factory and may opt in to a guarded,
16
+ subject/session-scoped dataset sink. A plain dataset manager is not a valid
17
+ sink.
18
+
19
+ The current native compiler supports declared metrics, dimensions, time grains,
20
+ declared one-to-many joins with fan-out rejection, filtered measures, and
21
+ derived metrics. The `duckdb` extra provides the local execution backend; the
22
+ `bigquery` extra provides BigQuery integration. See
23
+ [`docs/tools/semantic-layer.md`](../../docs/tools/semantic-layer.md) for the
24
+ user-facing semantic-layer documentation.
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0.0", "wheel>=0.44.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ai-parrot-semantic"
7
+ dynamic = ["version"]
8
+ description = "Governed semantic metric queries for AI-Parrot"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11,<3.14"
11
+ license = "MIT"
12
+ dependencies = ["ai-parrot", "sqlglot>=30.1,<31", "jsonschema>=4.20", "pyyaml>=6"]
13
+
14
+ [project.optional-dependencies]
15
+ duckdb = ["duckdb>=1.0"]
16
+ bigquery = ["google-cloud-bigquery>=3.30"]
17
+
18
+ [tool.setuptools.dynamic]
19
+ version = {attr = "parrot.semantic.version.__version__"}
20
+
21
+ [tool.setuptools.packages.find]
22
+ where = ["src"]
23
+ include = ["parrot*"]
24
+ namespaces = true
25
+
26
+ [tool.setuptools.package-data]
27
+ "parrot.semantic" = ["schemas/*.json", "sql/*.sql"]
28
+
29
+ [tool.uv.sources]
30
+ ai-parrot = {workspace = true}
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.4
2
+ Name: ai-parrot-semantic
3
+ Version: 1.3.2
4
+ Summary: Governed semantic metric queries for AI-Parrot
5
+ License-Expression: MIT
6
+ Requires-Python: <3.14,>=3.11
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: ai-parrot
9
+ Requires-Dist: sqlglot<31,>=30.1
10
+ Requires-Dist: jsonschema>=4.20
11
+ Requires-Dist: pyyaml>=6
12
+ Provides-Extra: duckdb
13
+ Requires-Dist: duckdb>=1.0; extra == "duckdb"
14
+ Provides-Extra: bigquery
15
+ Requires-Dist: google-cloud-bigquery>=3.30; extra == "bigquery"
16
+
17
+ # ai-parrot-semantic
18
+
19
+ MIT satellite: native sqlglot compilation and ai-parrot governance.
20
+ Install: `uv pip install 'ai-parrot-semantic[duckdb]'`.
21
+
22
+ ## Layering
23
+
24
+ Generic semantic queries and the future accounting extension share this namespace.
25
+ The accounting extension must remain independently importable.
26
+
27
+ ## Governance
28
+
29
+ Models are governed by default and require a trusted `PermissionContext` plus an
30
+ available PBAC evaluator; a policy may explicitly set `governed: false` for an
31
+ ungoverned model. Hosts provide the PBAC factory and may opt in to a guarded,
32
+ subject/session-scoped dataset sink. A plain dataset manager is not a valid
33
+ sink.
34
+
35
+ The current native compiler supports declared metrics, dimensions, time grains,
36
+ declared one-to-many joins with fan-out rejection, filtered measures, and
37
+ derived metrics. The `duckdb` extra provides the local execution backend; the
38
+ `bigquery` extra provides BigQuery integration. See
39
+ [`docs/tools/semantic-layer.md`](../../docs/tools/semantic-layer.md) for the
40
+ user-facing semantic-layer documentation.
@@ -0,0 +1,63 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/ai_parrot_semantic.egg-info/PKG-INFO
4
+ src/ai_parrot_semantic.egg-info/SOURCES.txt
5
+ src/ai_parrot_semantic.egg-info/dependency_links.txt
6
+ src/ai_parrot_semantic.egg-info/requires.txt
7
+ src/ai_parrot_semantic.egg-info/top_level.txt
8
+ src/parrot/semantic/__init__.py
9
+ src/parrot/semantic/audit.py
10
+ src/parrot/semantic/bootstrap.py
11
+ src/parrot/semantic/cache.py
12
+ src/parrot/semantic/catalog.py
13
+ src/parrot/semantic/context.py
14
+ src/parrot/semantic/errors.py
15
+ src/parrot/semantic/execution.py
16
+ src/parrot/semantic/handoff.py
17
+ src/parrot/semantic/loader.py
18
+ src/parrot/semantic/model.py
19
+ src/parrot/semantic/models.py
20
+ src/parrot/semantic/ossie.py
21
+ src/parrot/semantic/policy.py
22
+ src/parrot/semantic/results.py
23
+ src/parrot/semantic/toolkit.py
24
+ src/parrot/semantic/version.py
25
+ src/parrot/semantic/engines/__init__.py
26
+ src/parrot/semantic/engines/native.py
27
+ src/parrot/semantic/governance/__init__.py
28
+ src/parrot/semantic/governance/guard.py
29
+ src/parrot/semantic/governance/lineage.py
30
+ src/parrot/semantic/governance/masking.py
31
+ src/parrot/semantic/governance/pbac.py
32
+ src/parrot/semantic/governance/pipeline.py
33
+ src/parrot/semantic/governance/rls.py
34
+ src/parrot/semantic/schemas/ossie-0.1.1.schema.json
35
+ src/parrot/semantic/sql/semantic_audit_events.sql
36
+ tests/test_audit.py
37
+ tests/test_bootstrap.py
38
+ tests/test_cache.py
39
+ tests/test_catalog.py
40
+ tests/test_context.py
41
+ tests/test_e2e_governed.py
42
+ tests/test_e2e_handoff.py
43
+ tests/test_example_agent.py
44
+ tests/test_execution.py
45
+ tests/test_handoff_contract.py
46
+ tests/test_lineage.py
47
+ tests/test_loader.py
48
+ tests/test_masking.py
49
+ tests/test_mcp_local.py
50
+ tests/test_model_graph.py
51
+ tests/test_models.py
52
+ tests/test_native_compiler.py
53
+ tests/test_native_extensions.py
54
+ tests/test_ossie.py
55
+ tests/test_packaging.py
56
+ tests/test_pbac.py
57
+ tests/test_pipeline.py
58
+ tests/test_policy.py
59
+ tests/test_positive_guard.py
60
+ tests/test_results.py
61
+ tests/test_rls_ownership.py
62
+ tests/test_rls_pushdown.py
63
+ tests/test_toolkit.py
@@ -0,0 +1,10 @@
1
+ ai-parrot
2
+ sqlglot<31,>=30.1
3
+ jsonschema>=4.20
4
+ pyyaml>=6
5
+
6
+ [bigquery]
7
+ google-cloud-bigquery>=3.30
8
+
9
+ [duckdb]
10
+ duckdb>=1.0
@@ -0,0 +1,7 @@
1
+ """Governed semantic queries using the native sqlglot compiler."""
2
+
3
+ from .version import __version__
4
+ from .models import SemanticQuery, SemanticQueryResult
5
+ from .toolkit import SemanticLayerToolkit
6
+
7
+ __all__ = ["__version__", "SemanticQuery", "SemanticQueryResult", "SemanticLayerToolkit"]
@@ -0,0 +1,206 @@
1
+ """Signed semantic audit ledger (spec §3 Module 8, §8 Q3/Q5)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import re
8
+ from collections import deque
9
+ from datetime import datetime
10
+ from typing import Any, Awaitable, Callable, Literal
11
+
12
+ from pydantic import BaseModel
13
+
14
+ from parrot.security import AbstractKMSSigner, LocalHMACSigner
15
+
16
+ from .errors import SemanticExecutionError
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ _IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]{0,62}$")
21
+
22
+
23
+ class SemanticAuditEntry(BaseModel):
24
+ """One signed audit record for a semantic tool call.
25
+
26
+ ``engine`` is provenance only and is always ``"native"``. SQL text lives here,
27
+ never in exception messages.
28
+ """
29
+
30
+ schema_version: int = 1
31
+ entry_id: str
32
+ created_at: datetime
33
+ user_id: str
34
+ tenant_id: str
35
+ channel: str
36
+ agent_id: str | None
37
+ subject_digest: str
38
+ roles: list[str]
39
+ static_subject: bool
40
+ model: str
41
+ model_digest: str
42
+ policy_digest: str
43
+ engine: Literal["native"] = "native"
44
+ tool: str
45
+ structured_query: dict | None
46
+ raw_sql: bool
47
+ compiled_sql: str | None
48
+ governed_sql: str | None
49
+ dialect: str
50
+ driver: str
51
+ rls_applied: list[str]
52
+ masked_columns: list[str]
53
+ pbac: Literal["allow", "deny", "unavailable", "not_governed"]
54
+ outcome: Literal["ok", "denied", "validation_error", "execution_error"]
55
+ error_code: str | None
56
+ row_count: int | None
57
+ duration_ms: int | None
58
+ signature: str = ""
59
+
60
+ def canonical_bytes(self) -> bytes:
61
+ """Return sorted-key compact JSON of the entry, excluding ``signature``.
62
+
63
+ Returns:
64
+ Canonical UTF-8 bytes used for signing and verification.
65
+ """
66
+ data = self.model_dump(mode="json", exclude={"signature"})
67
+ return json.dumps(data, sort_keys=True, separators=(",", ":")).encode()
68
+
69
+
70
+ class SemanticAuditLedger:
71
+ """Bounded in-memory signed ledger with an optional async durable sink.
72
+
73
+ Args:
74
+ signer: Async KMS signer; defaults to a process-local HMAC signer.
75
+ storage: Async callable receiving the entry JSON; makes the ledger durable.
76
+ max_entries: Maximum number of entries kept in memory.
77
+ """
78
+
79
+ def __init__(
80
+ self,
81
+ signer: AbstractKMSSigner | None = None,
82
+ storage: Callable[[str], Awaitable[None]] | None = None,
83
+ max_entries: int = 10_000,
84
+ ) -> None:
85
+ self.signer: AbstractKMSSigner = signer or LocalHMACSigner()
86
+ self.storage = storage
87
+ self.entries: deque[SemanticAuditEntry] = deque(maxlen=max_entries)
88
+ self.logger = logger
89
+
90
+ @property
91
+ def durable(self) -> bool:
92
+ """True when a storage callable is configured (drives audit_required=None, §8 Q5)."""
93
+ return self.storage is not None
94
+
95
+ async def append(self, entry: SemanticAuditEntry, *, required: bool = False) -> SemanticAuditEntry:
96
+ """Sign the entry, keep it in memory and forward it to storage.
97
+
98
+ Args:
99
+ entry: The audit entry to record.
100
+ required: When True, a missing or failing sink raises instead of logging.
101
+
102
+ Returns:
103
+ The signed entry.
104
+
105
+ Raises:
106
+ SemanticExecutionError: ``audit_unavailable`` when required and the sink fails.
107
+ """
108
+ entry.signature = await self.signer.sign(entry.canonical_bytes())
109
+ self.entries.append(entry)
110
+ if self.storage is None:
111
+ if required:
112
+ raise SemanticExecutionError(
113
+ "audit_unavailable", "Audit storage is not configured", details={"audit_id": entry.entry_id}
114
+ )
115
+ return entry
116
+ try:
117
+ await self.storage(entry.model_dump_json())
118
+ except Exception as exc: # noqa: BLE001 - sink failures are policy-handled
119
+ self.logger.error("Semantic audit sink failed for %s: %s", entry.entry_id, type(exc).__name__)
120
+ if required:
121
+ raise SemanticExecutionError(
122
+ "audit_unavailable", "Audit storage failed", details={"audit_id": entry.entry_id}
123
+ ) from exc
124
+ return entry
125
+
126
+ async def verify(self, entry: SemanticAuditEntry) -> bool:
127
+ """Verify the entry's signature with the configured async signer.
128
+
129
+ Args:
130
+ entry: Entry to verify.
131
+
132
+ Returns:
133
+ True when the signature matches the canonical content.
134
+ """
135
+ if not entry.signature:
136
+ return False
137
+ return await self.signer.verify(entry.canonical_bytes(), entry.signature)
138
+
139
+
140
+ def _quote_table(table: str) -> str:
141
+ """Validate a bare or schema-qualified identifier and return it double-quoted.
142
+
143
+ Args:
144
+ table: ``name`` or ``schema.name``.
145
+
146
+ Returns:
147
+ The quoted identifier.
148
+
149
+ Raises:
150
+ ValueError: If any part is not a plain identifier.
151
+ """
152
+ parts = table.split(".")
153
+ if not 1 <= len(parts) <= 2 or not all(_IDENTIFIER.match(p) for p in parts):
154
+ raise ValueError("Invalid audit table identifier")
155
+ return ".".join(f'"{p}"' for p in parts)
156
+
157
+
158
+ def postgres_audit_storage(
159
+ dsn_or_pool: str | Any, *, table: str = "semantic_audit_events"
160
+ ) -> Callable[[str], Awaitable[None]]:
161
+ """Build an async storage callable that INSERTs entries into Postgres.
162
+
163
+ An injected pool (anything with ``acquire()``) is never closed. A DSN string
164
+ opens an owned asyncpg connection per write, always closed afterwards.
165
+
166
+ Args:
167
+ dsn_or_pool: asyncpg DSN or an existing pool.
168
+ table: Target table (DDL: ``parrot/semantic/sql/semantic_audit_events.sql``).
169
+
170
+ Returns:
171
+ ``async storage(json_str)``.
172
+ """
173
+ quoted = _quote_table(table)
174
+ sql = (
175
+ f"INSERT INTO {quoted} (entry_id, created_at, user_id, tenant_id, model, outcome, payload, signature) "
176
+ "VALUES ($1::uuid, $2, $3, $4, $5, $6, $7::jsonb, $8)"
177
+ )
178
+
179
+ def _params(payload: str) -> tuple[Any, ...]:
180
+ data = json.loads(payload)
181
+ return (
182
+ data["entry_id"],
183
+ datetime.fromisoformat(data["created_at"]),
184
+ data["user_id"],
185
+ data["tenant_id"],
186
+ data["model"],
187
+ data["outcome"],
188
+ payload,
189
+ data.get("signature", ""),
190
+ )
191
+
192
+ async def storage(payload: str) -> None:
193
+ params = _params(payload)
194
+ if isinstance(dsn_or_pool, str):
195
+ import asyncpg # lazy: optional dependency
196
+
197
+ conn = await asyncpg.connect(dsn_or_pool)
198
+ try:
199
+ await conn.execute(sql, *params)
200
+ finally:
201
+ await conn.close()
202
+ return
203
+ async with dsn_or_pool.acquire() as conn:
204
+ await conn.execute(sql, *params)
205
+
206
+ return storage
@@ -0,0 +1,191 @@
1
+ """Trusted stdio-only semantic toolkit bootstrap; never an HTTP identity fallback."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ from typing import Any
7
+ from uuid import uuid4
8
+
9
+ from parrot.auth import DataPlanePolicyGuard, PermissionContext, RlsRegistry, UserSession
10
+ from parrot.tools.toolkit import ToolkitTool
11
+ from pydantic import BaseModel, Field
12
+
13
+ from .context import SemanticRequestIdentity, bind_semantic_context, current_identity, current_pctx
14
+ from .errors import SemanticAccessError, SemanticPolicyError
15
+ from .toolkit import SemanticLayerToolkit
16
+
17
+ _EVALUATOR_MODULES = (
18
+ "navigator_auth.abac.policies.environment",
19
+ "navigator_auth.abac.policies.evaluator",
20
+ )
21
+
22
+
23
+ class StaticSubject(BaseModel):
24
+ """Trusted local operator identity, never accepted as a tool argument."""
25
+
26
+ user_id: str = Field(min_length=1)
27
+ tenant_id: str = Field(min_length=1)
28
+ roles: list[str]
29
+ metadata: dict[str, Any] = Field(default_factory=dict)
30
+
31
+
32
+ class LocalSemanticToolkitTool(ToolkitTool):
33
+ """Bind the immutable local operator identity around one tool call."""
34
+
35
+ def __init__(self, *args: Any, local_toolkit: "LocalSemanticLayerToolkit", **kwargs: Any) -> None:
36
+ """Initialize the generated tool with its local-only toolkit owner."""
37
+ self._local_toolkit = local_toolkit
38
+ super().__init__(*args, **kwargs)
39
+
40
+ async def _execute(self, **kwargs: Any) -> Any:
41
+ """Execute with the configured identity regardless of caller arguments."""
42
+ previous_context = getattr(self, "_current_pctx", None)
43
+ self._current_pctx = self._local_toolkit._static_permission_context
44
+ self._local_toolkit.logger.info(
45
+ "semantic static subject call tool=%s user=%s agent=%s",
46
+ self.name,
47
+ self._local_toolkit._static_permission_context.user_id,
48
+ self._local_toolkit._static_identity.agent_id,
49
+ )
50
+ try:
51
+ async with bind_semantic_context(
52
+ self._local_toolkit._static_permission_context,
53
+ identity=self._local_toolkit._static_identity,
54
+ ):
55
+ return await super()._execute(**kwargs)
56
+ finally:
57
+ self._current_pctx = previous_context
58
+
59
+
60
+ class LocalSemanticLayerToolkit(SemanticLayerToolkit):
61
+ """Stdio-local subclass that builds its configured PBAC guard at startup."""
62
+
63
+ local_only = True
64
+
65
+ def __init__(
66
+ self,
67
+ *,
68
+ static_subject: StaticSubject | dict[str, Any],
69
+ agent_id: str,
70
+ guard_factory: str,
71
+ guard_factory_kwargs: dict[str, Any] | None = None,
72
+ **kwargs: Any,
73
+ ) -> None:
74
+ """Resolve trusted factory, validate evaluator, create local process session.
75
+
76
+ Args:
77
+ static_subject: Trusted host configuration for the sole local operator.
78
+ agent_id: Host-configured semantic agent identifier.
79
+ guard_factory: Dotted host factory that creates a PBAC guard.
80
+ guard_factory_kwargs: Non-secret host configuration for the factory.
81
+ **kwargs: Ordinary :class:`SemanticLayerToolkit` constructor settings.
82
+
83
+ Raises:
84
+ SemanticPolicyError: If local identity or PBAC setup is invalid.
85
+ """
86
+ try:
87
+ subject = StaticSubject.model_validate(static_subject)
88
+ except Exception as exc: # noqa: BLE001 - Pydantic reports malformed host configuration
89
+ raise SemanticPolicyError("invalid_static_subject", "Invalid local static subject") from exc
90
+ if not agent_id.strip():
91
+ raise SemanticPolicyError("invalid_agent_id", "A non-empty local agent_id is required")
92
+ if not isinstance(guard_factory, str) or not guard_factory.strip():
93
+ raise SemanticPolicyError("guard_factory_required", "A configured guard_factory is required")
94
+ if guard_factory_kwargs is not None and not isinstance(guard_factory_kwargs, dict):
95
+ raise SemanticPolicyError("invalid_guard_factory_kwargs", "guard_factory_kwargs must be a mapping")
96
+
97
+ self._require_evaluator_modules()
98
+ rls_registry = RlsRegistry()
99
+ guard = self._build_guard(guard_factory, rls_registry, guard_factory_kwargs or {})
100
+ self._static_permission_context = PermissionContext(
101
+ session=UserSession(
102
+ user_id=subject.user_id,
103
+ tenant_id=subject.tenant_id,
104
+ roles=frozenset(subject.roles),
105
+ metadata=dict(subject.metadata),
106
+ ),
107
+ channel="mcp-local",
108
+ )
109
+ self._static_identity = SemanticRequestIdentity(
110
+ ctx=self._static_permission_context,
111
+ channel="mcp-local",
112
+ agent_id=agent_id,
113
+ session_id=str(uuid4()),
114
+ )
115
+ super().__init__(dataplane_guard=guard, rls_registry=rls_registry, **kwargs)
116
+
117
+ @staticmethod
118
+ def _require_evaluator_modules() -> None:
119
+ """Require the exact navigator-auth modules used by the PBAC gate."""
120
+ try:
121
+ evaluator_module, environment_module = (
122
+ importlib.import_module(module) for module in reversed(_EVALUATOR_MODULES)
123
+ )
124
+ if not (hasattr(evaluator_module, "PolicyEvaluator") and hasattr(environment_module, "Environment")):
125
+ raise AttributeError("PolicyEvaluator or Environment is missing")
126
+ except (AttributeError, ImportError, ModuleNotFoundError) as exc:
127
+ raise SemanticPolicyError("pbac_unavailable", "Required PBAC evaluator modules are unavailable") from exc
128
+
129
+ @staticmethod
130
+ def _build_guard(
131
+ factory_path: str, rls_registry: RlsRegistry, factory_kwargs: dict[str, Any]
132
+ ) -> DataPlanePolicyGuard:
133
+ """Load and invoke a trusted host PBAC guard factory.
134
+
135
+ Args:
136
+ factory_path: Dotted callable path from host configuration.
137
+ rls_registry: Registry shared with the semantic toolkit.
138
+ factory_kwargs: Non-secret factory configuration.
139
+
140
+ Returns:
141
+ Configured data-plane guard.
142
+
143
+ Raises:
144
+ SemanticPolicyError: If the configured factory cannot provide a valid guard.
145
+ """
146
+ try:
147
+ module_path, attribute = factory_path.rsplit(".", 1)
148
+ factory = getattr(importlib.import_module(module_path), attribute)
149
+ except (AttributeError, ImportError, ModuleNotFoundError, ValueError) as exc:
150
+ raise SemanticPolicyError("invalid_guard_factory", "Configured guard_factory cannot be loaded") from exc
151
+ if not callable(factory):
152
+ raise SemanticPolicyError("invalid_guard_factory", "Configured guard_factory must be callable")
153
+ try:
154
+ guard = factory(rls_registry=rls_registry, **factory_kwargs)
155
+ except Exception as exc: # noqa: BLE001 - policy construction must fail startup
156
+ raise SemanticPolicyError("guard_factory_failed", "Configured PBAC policy could not be loaded") from exc
157
+ if not isinstance(guard, DataPlanePolicyGuard) or getattr(guard, "_evaluator", None) is None:
158
+ raise SemanticPolicyError(
159
+ "invalid_policy_guard", "guard_factory must return a configured DataPlanePolicyGuard"
160
+ )
161
+ return guard
162
+
163
+ def _create_tool_from_method(self, name: str, bound_method: callable) -> ToolkitTool:
164
+ """Create tools that always bind this toolkit's configured local identity."""
165
+ base_tool = super()._create_tool_from_method(name, bound_method)
166
+ local_tool = LocalSemanticToolkitTool(
167
+ name=base_tool.name,
168
+ bound_method=bound_method,
169
+ description=base_tool.description,
170
+ args_schema=base_tool.args_schema,
171
+ return_direct=base_tool.return_direct,
172
+ executor=base_tool.executor,
173
+ webhook_callback_url=base_tool.webhook_callback_url,
174
+ remote_timeout_seconds=base_tool.remote_timeout_seconds,
175
+ local_toolkit=self,
176
+ )
177
+ for attribute in ("_required_permissions", "credential_provider", "routing_meta"):
178
+ if hasattr(base_tool, attribute):
179
+ setattr(local_tool, attribute, getattr(base_tool, attribute))
180
+ return local_tool
181
+
182
+ def _audit_identity(self) -> tuple[Any, str, str | None, bool]:
183
+ """Return the immutable local identity for audit entries; refuse any other binding.
184
+
185
+ Raises:
186
+ SemanticAccessError: When the bound identity is not this toolkit's static identity.
187
+ """
188
+ context = current_pctx()
189
+ if context is None or current_identity() != self._static_identity:
190
+ raise SemanticAccessError("context_required", "A matching local semantic context is required")
191
+ return context, "mcp-local", self._static_identity.agent_id, True