parapet 0.2.0__py3-none-win_amd64.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.
- parapet/__init__.py +156 -0
- parapet/header.py +97 -0
- parapet/sidecar.py +521 -0
- parapet/transport.py +290 -0
- parapet/trust.py +177 -0
- parapet-0.2.0.data/scripts/parapet-engine.exe +0 -0
- parapet-0.2.0.data/scripts/parapet.exe +0 -0
- parapet-0.2.0.dist-info/METADATA +12 -0
- parapet-0.2.0.dist-info/RECORD +11 -0
- parapet-0.2.0.dist-info/WHEEL +4 -0
- parapet-0.2.0.dist-info/sboms/parapet.cyclonedx.json +8404 -0
parapet/__init__.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Copyright 2026 The Parapet Project
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
# Parapet Python SDK — defined in M1.11
|
|
5
|
+
#
|
|
6
|
+
# Public API:
|
|
7
|
+
# parapet.init(config_path) — validate config, start engine sidecar, patch httpx
|
|
8
|
+
# parapet.session(...) — context manager that sets W3C Baggage for the scope
|
|
9
|
+
# parapet.shutdown() — stop owned engine if permitted by file-cleanup gate
|
|
10
|
+
# parapet.untrusted(content) — mark a string as untrusted content
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from contextlib import contextmanager
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Generator
|
|
17
|
+
|
|
18
|
+
from parapet.header import _active_baggage, build_baggage_header
|
|
19
|
+
from parapet.sidecar import EngineState, start_engine, stop_engine
|
|
20
|
+
from parapet.transport import patch_httpx
|
|
21
|
+
from parapet.trust import _ensure_registry, _restore_registry, _swap_registry
|
|
22
|
+
from typing import Iterable
|
|
23
|
+
|
|
24
|
+
__all__ = ["init", "session", "shutdown", "untrusted"]
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger("parapet")
|
|
27
|
+
|
|
28
|
+
_DEFAULT_PORT = 9800
|
|
29
|
+
|
|
30
|
+
# Module-level engine state — single instance shared across init/session.
|
|
31
|
+
_engine_state = EngineState()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SessionContext:
|
|
35
|
+
"""Holds the baggage string for the duration of a session scope."""
|
|
36
|
+
|
|
37
|
+
__slots__ = ("baggage",)
|
|
38
|
+
|
|
39
|
+
def __init__(self, baggage: str) -> None:
|
|
40
|
+
self.baggage = baggage
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def init(
|
|
44
|
+
config_path: str,
|
|
45
|
+
*,
|
|
46
|
+
port: int = _DEFAULT_PORT,
|
|
47
|
+
engine_bin: str | None = None,
|
|
48
|
+
extra_hosts: Iterable[str] | None = None,
|
|
49
|
+
state_dir: str | Path | None = None,
|
|
50
|
+
engine_args: Iterable[str] | None = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Initialize the parapet SDK.
|
|
53
|
+
|
|
54
|
+
Validates that *config_path* exists, starts the engine sidecar (if not
|
|
55
|
+
already running), and patches httpx to route LLM requests through the
|
|
56
|
+
engine.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
config_path: Path to the parapet.yaml configuration file.
|
|
60
|
+
port: Port for the engine to listen on (default 9800).
|
|
61
|
+
engine_bin: Explicit path to the ``parapet-engine`` binary.
|
|
62
|
+
Falls back to ``PARAPET_ENGINE_PATH`` env var, then ``PATH``.
|
|
63
|
+
extra_hosts: Additional LLM API hosts to intercept beyond the
|
|
64
|
+
built-in defaults (e.g., ``["api.together.xyz"]``).
|
|
65
|
+
state_dir: Optional private state directory for receipt evidence
|
|
66
|
+
and the ``parapet-control/v0`` channel.
|
|
67
|
+
engine_args: Optional additional CLI flags passed to ``parapet-engine``.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
FileNotFoundError: If *config_path* does not exist or the engine
|
|
71
|
+
binary cannot be found.
|
|
72
|
+
"""
|
|
73
|
+
path = Path(config_path)
|
|
74
|
+
if not path.exists():
|
|
75
|
+
raise FileNotFoundError(
|
|
76
|
+
f"Config file not found: {config_path}"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
hosts = frozenset(extra_hosts) if extra_hosts else None
|
|
80
|
+
resolved_state_dir = Path(state_dir) if state_dir is not None else None
|
|
81
|
+
extra_engine_args = list(engine_args) if engine_args is not None else None
|
|
82
|
+
start_engine(
|
|
83
|
+
config_path=config_path,
|
|
84
|
+
port=port,
|
|
85
|
+
engine_bin=engine_bin,
|
|
86
|
+
state=_engine_state,
|
|
87
|
+
state_dir=resolved_state_dir,
|
|
88
|
+
engine_args=extra_engine_args,
|
|
89
|
+
)
|
|
90
|
+
patch_httpx(port=port, extra_hosts=hosts)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def shutdown() -> bool:
|
|
94
|
+
"""Stop the owned engine sidecar if permitted by the file-cleanup gate.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
``True`` if an owned engine was stopped, ``False`` if the engine is
|
|
98
|
+
not owned or shutdown was refused by the cleanup gate.
|
|
99
|
+
"""
|
|
100
|
+
return stop_engine(state=_engine_state)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@contextmanager
|
|
104
|
+
def session(
|
|
105
|
+
*,
|
|
106
|
+
user_id: str | None = None,
|
|
107
|
+
role: str | None = None,
|
|
108
|
+
) -> Generator[SessionContext, None, None]:
|
|
109
|
+
"""Set W3C Baggage headers for the duration of this scope.
|
|
110
|
+
|
|
111
|
+
Must be called after ``init()``.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
user_id: Optional user identifier to include in baggage.
|
|
115
|
+
role: Optional role string to include in baggage.
|
|
116
|
+
|
|
117
|
+
Yields:
|
|
118
|
+
A ``SessionContext`` with the computed baggage string.
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
RuntimeError: If ``init()`` has not been called.
|
|
122
|
+
"""
|
|
123
|
+
if not _engine_state.initialized:
|
|
124
|
+
raise RuntimeError(
|
|
125
|
+
"parapet.session() called before init(). Call parapet.init() first."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
baggage = build_baggage_header(user_id=user_id, role=role)
|
|
129
|
+
baggage_token = _active_baggage.set(baggage)
|
|
130
|
+
trust_token = _swap_registry()
|
|
131
|
+
try:
|
|
132
|
+
yield SessionContext(baggage=baggage)
|
|
133
|
+
finally:
|
|
134
|
+
_restore_registry(trust_token)
|
|
135
|
+
_active_baggage.reset(baggage_token)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def untrusted(content: str, source: str = "unknown") -> str:
|
|
139
|
+
"""Mark a string as untrusted content.
|
|
140
|
+
|
|
141
|
+
Registers the string in the per-context trust registry so the SDK
|
|
142
|
+
can locate it in serialized requests and emit byte-range trust spans.
|
|
143
|
+
|
|
144
|
+
The string is returned unchanged for inline use::
|
|
145
|
+
|
|
146
|
+
prompt = f"Context: {parapet.untrusted(rag_snippet, source='rag')}"
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
content: The untrusted content string.
|
|
150
|
+
source: Provenance label (e.g., "rag", "user_input", "web_search").
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
The content string, unchanged.
|
|
154
|
+
"""
|
|
155
|
+
registry = _ensure_registry()
|
|
156
|
+
return registry.register(content, source=source)
|
parapet/header.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Copyright 2026 The Parapet Project
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Header serialization for Parapet SDK.
|
|
5
|
+
|
|
6
|
+
Includes:
|
|
7
|
+
- W3C Baggage header (``baggage: key1=value1,key2=value2``)
|
|
8
|
+
- X-Guard-Trust header (``inline:<base64-encoded JSON>``)
|
|
9
|
+
- Active baggage ContextVar for session-scoped propagation
|
|
10
|
+
|
|
11
|
+
Values are percent-encoded to handle special characters safely.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import base64
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
from contextvars import ContextVar
|
|
19
|
+
from urllib.parse import quote
|
|
20
|
+
|
|
21
|
+
__all__ = ["build_baggage_header", "build_trust_header", "get_active_baggage"]
|
|
22
|
+
|
|
23
|
+
# Active baggage for the current session scope (set by session(), read by transport).
|
|
24
|
+
_active_baggage: ContextVar[str | None] = ContextVar(
|
|
25
|
+
"parapet_active_baggage", default=None
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_active_baggage() -> str | None:
|
|
30
|
+
"""Return the active baggage string for the current context, or None."""
|
|
31
|
+
return _active_baggage.get()
|
|
32
|
+
|
|
33
|
+
MAX_TRUST_HEADER_BYTES = 4096 # 4KB inline limit
|
|
34
|
+
|
|
35
|
+
# Characters that must be percent-encoded in baggage values.
|
|
36
|
+
# W3C Baggage uses a subset of RFC 3986 — commas, semicolons, equals,
|
|
37
|
+
# and spaces are delimiters and must not appear unescaped in values.
|
|
38
|
+
_SAFE_CHARS = "" # encode everything except unreserved chars
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _encode_value(value: str) -> str:
|
|
42
|
+
"""Percent-encode a baggage value per W3C Baggage spec."""
|
|
43
|
+
return quote(value, safe=_SAFE_CHARS)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def build_baggage_header(
|
|
47
|
+
*,
|
|
48
|
+
user_id: str | None = None,
|
|
49
|
+
role: str | None = None,
|
|
50
|
+
) -> str:
|
|
51
|
+
"""Build a W3C Baggage header value from optional fields.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
user_id: Optional user identifier.
|
|
55
|
+
role: Optional role string.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Baggage header value string (e.g. ``user_id=u_1,role=admin``).
|
|
59
|
+
Returns empty string when no fields are provided.
|
|
60
|
+
"""
|
|
61
|
+
parts: list[str] = []
|
|
62
|
+
|
|
63
|
+
if user_id is not None:
|
|
64
|
+
parts.append(f"user_id={_encode_value(user_id)}")
|
|
65
|
+
|
|
66
|
+
if role is not None:
|
|
67
|
+
parts.append(f"role={_encode_value(role)}")
|
|
68
|
+
|
|
69
|
+
return ",".join(parts)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def build_trust_header(spans: list) -> str | None:
|
|
73
|
+
"""Serialize trust spans as X-Guard-Trust header value.
|
|
74
|
+
|
|
75
|
+
Format: ``inline:<base64-encoded JSON>``
|
|
76
|
+
JSON schema: ``[{"s": start, "e": end, "src": source}, ...]``
|
|
77
|
+
|
|
78
|
+
Returns None if no spans or if encoded value exceeds 4KB limit.
|
|
79
|
+
Compact keys (s/e/src) to minimize header size.
|
|
80
|
+
"""
|
|
81
|
+
if not spans:
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
compact = [{"s": s.start, "e": s.end, "src": s.source} for s in spans]
|
|
85
|
+
payload = json.dumps(compact, separators=(",", ":")).encode("utf-8")
|
|
86
|
+
|
|
87
|
+
if len(payload) > MAX_TRUST_HEADER_BYTES:
|
|
88
|
+
logging.getLogger("parapet.header").warning(
|
|
89
|
+
"X-Guard-Trust header exceeds %d byte limit (%d spans, %d bytes); dropping",
|
|
90
|
+
MAX_TRUST_HEADER_BYTES,
|
|
91
|
+
len(spans),
|
|
92
|
+
len(payload),
|
|
93
|
+
)
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
encoded = base64.b64encode(payload).decode("ascii")
|
|
97
|
+
return f"inline:{encoded}"
|