cloakapi 0.5.1__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.
Files changed (61) hide show
  1. LICENSE +202 -0
  2. NOTICE +18 -0
  3. THIRD-PARTY-NOTICES.md +22 -0
  4. TRADEMARKS.md +19 -0
  5. cloakapi/__init__.py +271 -0
  6. cloakapi/_disclosure/clk.txt +11 -0
  7. cloakapi/_disclosure/clk_message.txt +11 -0
  8. cloakapi/_disclosure/cloak_angle.txt +16 -0
  9. cloakapi/_disclosure/cloak_angle_message.txt +16 -0
  10. cloakapi/_http.py +443 -0
  11. cloakapi/_pretokenised.py +134 -0
  12. cloakapi/_relay.py +213 -0
  13. cloakapi/_release.py +30 -0
  14. cloakapi/_request_strings.py +1267 -0
  15. cloakapi/_ruleset/patterns.json +2238 -0
  16. cloakapi/_stream_detok.py +788 -0
  17. cloakapi/_version.py +31 -0
  18. cloakapi/async_client.py +704 -0
  19. cloakapi/attachments.py +176 -0
  20. cloakapi/cli.py +245 -0
  21. cloakapi/client.py +848 -0
  22. cloakapi/config.py +491 -0
  23. cloakapi/coverage.py +21 -0
  24. cloakapi/degraded.py +196 -0
  25. cloakapi/disclosure.py +334 -0
  26. cloakapi/engine.py +922 -0
  27. cloakapi/errors.py +304 -0
  28. cloakapi/notices.py +142 -0
  29. cloakapi/pii/__init__.py +4 -0
  30. cloakapi/pii/_checksum.py +510 -0
  31. cloakapi/pii/_ruleset.py +131 -0
  32. cloakapi/pii/patterns.py +1713 -0
  33. cloakapi/pii/rulefile.py +738 -0
  34. cloakapi/pii/tokenizer.py +537 -0
  35. cloakapi/posture.py +112 -0
  36. cloakapi/py.typed +1 -0
  37. cloakapi/receipts.py +605 -0
  38. cloakapi/resources/__init__.py +32 -0
  39. cloakapi/resources/admin.py +104 -0
  40. cloakapi/resources/buffer.py +54 -0
  41. cloakapi/resources/chat.py +293 -0
  42. cloakapi/resources/detect.py +115 -0
  43. cloakapi/resources/messages.py +260 -0
  44. cloakapi/resources/models.py +58 -0
  45. cloakapi/resources/receipts.py +252 -0
  46. cloakapi/resources/reconstruct.py +173 -0
  47. cloakapi/resources/tokens.py +47 -0
  48. cloakapi/review.py +112 -0
  49. cloakapi/streaming.py +446 -0
  50. cloakapi/surrogate_mode.py +308 -0
  51. cloakapi/types.py +284 -0
  52. cloakapi-0.5.1.dist-info/METADATA +441 -0
  53. cloakapi-0.5.1.dist-info/RECORD +61 -0
  54. cloakapi-0.5.1.dist-info/WHEEL +4 -0
  55. cloakapi-0.5.1.dist-info/entry_points.txt +2 -0
  56. cloakapi-0.5.1.dist-info/licenses/LICENSE +202 -0
  57. cloakapi-0.5.1.dist-info/licenses/NOTICE +18 -0
  58. licenses/Apache-2.0.txt +202 -0
  59. licenses/CC-BY-4.0.txt +104 -0
  60. licenses/MIT-Faker.txt +21 -0
  61. licenses/MIT.txt +21 -0
cloakapi/_relay.py ADDED
@@ -0,0 +1,213 @@
1
+ """One relay request, from the tokenised body to the bytes that leave.
2
+
3
+ The sync and async clients share every step that does not touch the network:
4
+ what the request declares about itself, which notices it carries, and what the
5
+ reviewer is shown. Two copies of these rules drifted before, so they live here
6
+ once. The clients pass in their own ``engine_available`` and
7
+ ``require_pinned_engine_identity`` so each keeps a single place to substitute
8
+ them.
9
+
10
+ What a prepared request always has:
11
+
12
+ * the exact body bytes, serialised once, that the MAC is minted over and that
13
+ are then sent;
14
+ * ``X-Cloak-Coverage`` and ``X-CloakAPI-Tokenized: true;coverage=<coverage>``,
15
+ which state which local layers ran;
16
+ * engine identity headers only when a pinned engine with this SDK's rule set
17
+ ran. A request the structured rules prepared claims no engine.
18
+
19
+ What never happens here: a request is not refused for its content. Reduced
20
+ coverage, a value in a place that must stay as written (an identifier, a tool
21
+ schema literal, a number, a key inside tool arguments) and media this device
22
+ did not read become notices on the request, shown to the reviewer and returned
23
+ with the reply. A detected value used as an object key in free-form data is
24
+ replaced here, where it stands.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from collections.abc import Callable
30
+ from dataclasses import dataclass, field
31
+ from typing import Any
32
+
33
+ from ._http import absolutt_url, serialise_body
34
+ from ._request_strings import assert_no_thai_khmer, bind_tokeniser, settle_structure
35
+ from .coverage import Coverage
36
+ from .degraded import KIND_NO_NER, engine_reason, guard_reduced_coverage
37
+ from .errors import EngineIdentityError, PlaintextRelayNotSupportedError
38
+ from .notices import (
39
+ REDUCED_COVERAGE,
40
+ ReviewInfo,
41
+ SendWarning,
42
+ place_warnings,
43
+ print_unchecked_content,
44
+ reduced_coverage_warning,
45
+ unchecked_content_warning,
46
+ )
47
+ from .review import RelayReview
48
+
49
+ _NO_ENGINE_SENTENCE = "No on-device engine is installed for this platform."
50
+ _ENGINE_REFUSED_SENTENCE = "The on-device engine could not process part of this request."
51
+ _MODEL_MESSAGE = (
52
+ "The name model is not available on this device, so names are checked against "
53
+ "the built-in list only."
54
+ )
55
+
56
+
57
+ @dataclass
58
+ class BuiltRelay:
59
+ """A relay request that is ready for review. Nothing has been sent."""
60
+
61
+ method: str
62
+ path: str
63
+ url: str
64
+ content: bytes
65
+ headers: dict[str, str]
66
+ coverage: Coverage
67
+ warnings: tuple[SendWarning, ...] = ()
68
+ token_map: dict[str, str] = field(default_factory=dict)
69
+ #: The signed receipt the gateway returned with the reply to THIS request, if any.
70
+ receipt: dict[str, Any] | None = None
71
+
72
+ @property
73
+ def text(self) -> str:
74
+ """The exact outgoing body, as text."""
75
+ return self.content.decode("utf-8")
76
+
77
+ @property
78
+ def state(self) -> str:
79
+ return "ok" if self.coverage is Coverage.FULL and not self.warnings else "degraded"
80
+
81
+ def info(self) -> ReviewInfo:
82
+ return ReviewInfo(
83
+ state="ok" if self.state == "ok" else "degraded",
84
+ coverage=self.coverage.value,
85
+ warnings=self.warnings,
86
+ )
87
+
88
+ def review(self) -> RelayReview:
89
+ return RelayReview.create(self.method, self.url, self.content, self.headers)
90
+
91
+
92
+ def build_relay(
93
+ client: Any,
94
+ body: dict[str, Any],
95
+ extra_headers: dict[str, str],
96
+ should_tokenize: bool,
97
+ *,
98
+ method: str,
99
+ path: str,
100
+ engine_available: Callable[[], bool],
101
+ require_pinned_engine_identity: Callable[[], Any],
102
+ state: dict[str, Any] | None = None,
103
+ ) -> BuiltRelay:
104
+ """Finalise the body and headers of one relay request. No network, no review.
105
+
106
+ ``state`` is the per-request state ``_prepare_messages`` returned. When it
107
+ is given, coverage and notices are read from it and not from the client's
108
+ "latest request" attributes, so two overlapping requests on one client
109
+ cannot take each other's coverage or lose each other's notices.
110
+ """
111
+ if not should_tokenize:
112
+ raise PlaintextRelayNotSupportedError(
113
+ "tokenize=False is not supported for relay requests; enable on-device tokenization."
114
+ )
115
+ notices: list[SendWarning] = []
116
+ own = state if isinstance(state, dict) and "coverage" in state else None
117
+ # Keys, identifiers, schema literals and media: the last pass over the body.
118
+ # It replaces what can be replaced and records the rest on this request.
119
+ settle_tok = bind_tokeniser(client, state if isinstance(state, dict) else None)
120
+ body = settle_structure(body, settle_tok)
121
+ places = list(settle_tok.state.get("unreplaced_places", ())) # type: ignore[attr-defined]
122
+ coverage = (own["coverage"] if own is not None else client._last_coverage) or Coverage.FULL
123
+ if assert_no_thai_khmer(body):
124
+ coverage = Coverage.RULES_ONLY
125
+ client._last_coverage = coverage
126
+ guard_reduced_coverage("tokenise text", kind=KIND_NO_NER)
127
+ notices.append(unchecked_content_warning("unverified_script"))
128
+ value_not_replaced = (
129
+ own.get("value_not_replaced", False)
130
+ if own is not None
131
+ else getattr(client, "_last_value_not_replaced", False)
132
+ )
133
+ if places:
134
+ notices.extend(place_warnings(places))
135
+ elif value_not_replaced:
136
+ notices.append(unchecked_content_warning("value_not_replaced"))
137
+ headers = dict(extra_headers)
138
+ trusted_engine = engine_available()
139
+ ident: dict[str, str] = {}
140
+ if trusted_engine:
141
+ try:
142
+ candidate = require_pinned_engine_identity()
143
+ if isinstance(candidate, dict) and candidate.get("version") and candidate.get("hash"):
144
+ ident = candidate
145
+ else:
146
+ trusted_engine = False
147
+ except EngineIdentityError:
148
+ trusted_engine = False
149
+ # The engine is in use but would not take at least one string of this
150
+ # request, and the structured rules prepared it instead. Such a request was
151
+ # not prepared by the engine: it states no engine identity, and its notice
152
+ # says what ran (not "the name model is missing").
153
+ engine_refused = bool(trusted_engine and own is not None and own.get("rules_prepared"))
154
+ if engine_refused:
155
+ trusted_engine = False
156
+ ident = {}
157
+ if not trusted_engine:
158
+ if coverage is not Coverage.RULES_ONLY:
159
+ guard_reduced_coverage("tokenise text", kind=KIND_NO_NER)
160
+ coverage = Coverage.RULES_ONLY
161
+ client._last_coverage = coverage
162
+ # _prepare_messages may have populated identity headers before the
163
+ # final pin check. Never carry those claims into review or egress when
164
+ # this request cannot prove the loaded engine.
165
+ headers.pop("X-Cloak-Engine-Version", None)
166
+ headers.pop("X-Cloak-Engine-Hash", None)
167
+ if engine_refused:
168
+ notices.insert(0, reduced_coverage_warning("engine_refused", _ENGINE_REFUSED_SENTENCE))
169
+ else:
170
+ reason = engine_reason() or ("engine_missing", _NO_ENGINE_SENTENCE)
171
+ notices.insert(
172
+ 0, reduced_coverage_warning(reason[0], reason[1] or _NO_ENGINE_SENTENCE)
173
+ )
174
+ elif coverage is not Coverage.FULL and (
175
+ own is None or own.get("detector_coverage", coverage) is not Coverage.FULL
176
+ ):
177
+ # The engine ran; only its name model is missing. Not "rules ran instead".
178
+ # (Not said when the detector reported full coverage and the label was
179
+ # lowered only because a value or a script was left as written: those
180
+ # have their own notice.)
181
+ notices.insert(0, SendWarning(REDUCED_COVERAGE, "model_missing", _MODEL_MESSAGE))
182
+ headers["X-Cloak-Coverage"] = coverage.value
183
+ headers["X-CloakAPI-Tokenized"] = f"true;coverage={coverage.value}"
184
+ content = serialise_body(body)
185
+ last_mode = (own.get("surrogate_mode") if own is not None else None) or client._last_surrogate_mode
186
+ actual_mode = getattr(last_mode, "mode", last_mode)
187
+ headers["X-CloakAPI-Mode"] = (
188
+ "max" if coverage is Coverage.RULES_ONLY and actual_mode == "max" else client.tier
189
+ )
190
+ if ident.get("version") and (coverage is not Coverage.RULES_ONLY or ident.get("hash")):
191
+ headers["X-Cloak-Engine-Version"] = ident["version"]
192
+ if ident.get("hash"):
193
+ headers["X-Cloak-Engine-Hash"] = ident["hash"]
194
+ warnings = tuple(notices)
195
+ client._last_warnings = warnings
196
+ print_unchecked_content(warnings)
197
+ return BuiltRelay(
198
+ method=method,
199
+ path=path,
200
+ url=absolutt_url(client._base_url, path),
201
+ content=content,
202
+ headers=headers,
203
+ coverage=coverage,
204
+ warnings=warnings,
205
+ )
206
+
207
+
208
+ def refuse_plaintext_tier(client: Any) -> None:
209
+ if str(client.tier) == "off": # defense-in-depth for runtime mutation
210
+ raise PlaintextRelayNotSupportedError(
211
+ "tier='off' is a plaintext relay; under Level-1-only the gateway "
212
+ "relays only client-tokenised requests. Use tier 'balanced' or 'max'."
213
+ )
cloakapi/_release.py ADDED
@@ -0,0 +1,30 @@
1
+ """The release set this package was cut for.
2
+
3
+ One place for the numbers that tie this package to the other CloakAPI packages
4
+ of the same release: the rule set it expects and the native engine files it
5
+ trusts when one is shipped inside a wheel. ``repo/release-set.json`` is the
6
+ source; ``repo/shared/testing/release-set-check.mjs python`` and
7
+ ``tests/test_release_set.py`` fail when this file disagrees with it, and
8
+ ``hatch_build.py`` refuses to build a wheel around any other engine file.
9
+
10
+ Plain string constants on purpose: the build hook and the lockstep check read
11
+ them without importing the package.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ RELEASE_SET = "R-1.6.1"
17
+
18
+ #: Rule set this SDK's own rules and its native engine must both carry.
19
+ RULESET_VERSION = "1.6.1"
20
+ RULESET_SHA256 = "8660b7abe553154802c9d27888a144863bcceaa3703517b8401cc56c590df2f9"
21
+
22
+ #: SHA-256 of the native engine file per platform (``cloak-engine-ffi`` 0.3.0).
23
+ NATIVE_SHA256_LINUX_X86_64 = "b2aec8d6d90ead4328258270c41a79dda4e1f71efa79025f308ce8e09f080744"
24
+ NATIVE_SHA256_LINUX_AARCH64 = "a8bfb99346cdaf6e47b13214e7f1f872f95e0c2fe040cf6bb3e6a82f1d26ed30"
25
+
26
+ #: Wheel platform arch -> the one engine file a wheel for it may carry.
27
+ NATIVE_ENGINE_SHA256 = {
28
+ "x86_64": NATIVE_SHA256_LINUX_X86_64,
29
+ "aarch64": NATIVE_SHA256_LINUX_AARCH64,
30
+ }