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.
- ai_parrot_semantic-1.3.2/PKG-INFO +40 -0
- ai_parrot_semantic-1.3.2/README.md +24 -0
- ai_parrot_semantic-1.3.2/pyproject.toml +30 -0
- ai_parrot_semantic-1.3.2/setup.cfg +4 -0
- ai_parrot_semantic-1.3.2/src/ai_parrot_semantic.egg-info/PKG-INFO +40 -0
- ai_parrot_semantic-1.3.2/src/ai_parrot_semantic.egg-info/SOURCES.txt +63 -0
- ai_parrot_semantic-1.3.2/src/ai_parrot_semantic.egg-info/dependency_links.txt +1 -0
- ai_parrot_semantic-1.3.2/src/ai_parrot_semantic.egg-info/requires.txt +10 -0
- ai_parrot_semantic-1.3.2/src/ai_parrot_semantic.egg-info/top_level.txt +1 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/__init__.py +7 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/audit.py +206 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/bootstrap.py +191 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/cache.py +83 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/catalog.py +196 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/context.py +118 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/engines/__init__.py +46 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/engines/native.py +541 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/errors.py +40 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/execution.py +95 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/governance/__init__.py +1 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/governance/guard.py +190 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/governance/lineage.py +192 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/governance/masking.py +151 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/governance/pbac.py +120 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/governance/pipeline.py +338 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/governance/rls.py +233 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/handoff.py +247 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/loader.py +314 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/model.py +396 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/models.py +251 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/ossie.py +55 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/policy.py +277 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/results.py +180 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/schemas/ossie-0.1.1.schema.json +345 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/sql/semantic_audit_events.sql +16 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/toolkit.py +817 -0
- ai_parrot_semantic-1.3.2/src/parrot/semantic/version.py +3 -0
- ai_parrot_semantic-1.3.2/tests/test_audit.py +177 -0
- ai_parrot_semantic-1.3.2/tests/test_bootstrap.py +132 -0
- ai_parrot_semantic-1.3.2/tests/test_cache.py +39 -0
- ai_parrot_semantic-1.3.2/tests/test_catalog.py +121 -0
- ai_parrot_semantic-1.3.2/tests/test_context.py +127 -0
- ai_parrot_semantic-1.3.2/tests/test_e2e_governed.py +249 -0
- ai_parrot_semantic-1.3.2/tests/test_e2e_handoff.py +484 -0
- ai_parrot_semantic-1.3.2/tests/test_example_agent.py +215 -0
- ai_parrot_semantic-1.3.2/tests/test_execution.py +93 -0
- ai_parrot_semantic-1.3.2/tests/test_handoff_contract.py +225 -0
- ai_parrot_semantic-1.3.2/tests/test_lineage.py +94 -0
- ai_parrot_semantic-1.3.2/tests/test_loader.py +94 -0
- ai_parrot_semantic-1.3.2/tests/test_masking.py +121 -0
- ai_parrot_semantic-1.3.2/tests/test_mcp_local.py +143 -0
- ai_parrot_semantic-1.3.2/tests/test_model_graph.py +171 -0
- ai_parrot_semantic-1.3.2/tests/test_models.py +95 -0
- ai_parrot_semantic-1.3.2/tests/test_native_compiler.py +150 -0
- ai_parrot_semantic-1.3.2/tests/test_native_extensions.py +218 -0
- ai_parrot_semantic-1.3.2/tests/test_ossie.py +51 -0
- ai_parrot_semantic-1.3.2/tests/test_packaging.py +73 -0
- ai_parrot_semantic-1.3.2/tests/test_pbac.py +126 -0
- ai_parrot_semantic-1.3.2/tests/test_pipeline.py +146 -0
- ai_parrot_semantic-1.3.2/tests/test_policy.py +136 -0
- ai_parrot_semantic-1.3.2/tests/test_positive_guard.py +179 -0
- ai_parrot_semantic-1.3.2/tests/test_results.py +115 -0
- ai_parrot_semantic-1.3.2/tests/test_rls_ownership.py +94 -0
- ai_parrot_semantic-1.3.2/tests/test_rls_pushdown.py +185 -0
- 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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
parrot
|
|
@@ -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
|