contextwall-sdk 0.1.1__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.
@@ -0,0 +1,29 @@
1
+ # Secrets — never commit these
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+
6
+ # Python
7
+ __pycache__/
8
+ *.pyc
9
+ *.pyo
10
+ *.pyd
11
+ .pytest_cache/
12
+ .ruff_cache/
13
+ .mypy_cache/
14
+ dist/
15
+ build/
16
+ *.egg-info/
17
+ .venv/
18
+ venv/
19
+
20
+ # Runtime data
21
+ .ctxfw/
22
+ *.db
23
+ *.pid
24
+
25
+ # Editor
26
+ .vscode/
27
+ .idea/
28
+ *.swp
29
+ .DS_Store
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: contextwall-sdk
3
+ Version: 0.1.1
4
+ Summary: ContextWall SDK - drop-in wrapper for Anthropic and OpenAI with context firewall enforcement
5
+ Author-email: Context Firewall <sumesh.punakkal@gmail.com>
6
+ License: Apache-2.0
7
+ Keywords: ai,anthropic,context-firewall,governance,openai,security
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: httpx>=0.27
10
+ Requires-Dist: pydantic>=2.0
11
+ Provides-Extra: all
12
+ Requires-Dist: anthropic>=0.25; extra == 'all'
13
+ Requires-Dist: openai>=1.0; extra == 'all'
14
+ Provides-Extra: anthropic
15
+ Requires-Dist: anthropic>=0.25; extra == 'anthropic'
16
+ Provides-Extra: dev
17
+ Requires-Dist: anthropic>=0.25; extra == 'dev'
18
+ Requires-Dist: httpx>=0.27; extra == 'dev'
19
+ Requires-Dist: openai>=1.0; extra == 'dev'
20
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
21
+ Requires-Dist: pytest>=8.0; extra == 'dev'
22
+ Provides-Extra: openai
23
+ Requires-Dist: openai>=1.0; extra == 'openai'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # contextwall-sdk
27
+
28
+ Drop-in wrapper for Anthropic and OpenAI that routes calls through a local [ContextWall](https://contextwall.io) daemon, enforcing your context firewall policy with no other code changes.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install 'contextwall-sdk[anthropic]'
34
+ pip install 'contextwall-sdk[openai]'
35
+ pip install 'contextwall-sdk[all]'
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ```python
41
+ from contextwall_sdk import SafeAnthropic
42
+
43
+ client = SafeAnthropic(api_key="sk-ant-...", ctxfw_url="http://localhost:8080")
44
+ # use exactly like the standard Anthropic client
45
+ ```
46
+
47
+ ```python
48
+ from contextwall_sdk import SafeOpenAI
49
+
50
+ client = SafeOpenAI(api_key="sk-...", ctxfw_url="http://localhost:8080")
51
+ ```
52
+
53
+ Blocked requests raise `contextwall_sdk.ContextWallBlockedError` with the policy violation detail.
54
+
55
+ ## Daemon setup
56
+
57
+ The SDK requires a running ContextWall daemon. See the [quickstart](https://contextwall.io/quickstart) or the [GitHub repo](https://github.com/bytewise-ca/context-wall).
@@ -0,0 +1,32 @@
1
+ # contextwall-sdk
2
+
3
+ Drop-in wrapper for Anthropic and OpenAI that routes calls through a local [ContextWall](https://contextwall.io) daemon, enforcing your context firewall policy with no other code changes.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install 'contextwall-sdk[anthropic]'
9
+ pip install 'contextwall-sdk[openai]'
10
+ pip install 'contextwall-sdk[all]'
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```python
16
+ from contextwall_sdk import SafeAnthropic
17
+
18
+ client = SafeAnthropic(api_key="sk-ant-...", ctxfw_url="http://localhost:8080")
19
+ # use exactly like the standard Anthropic client
20
+ ```
21
+
22
+ ```python
23
+ from contextwall_sdk import SafeOpenAI
24
+
25
+ client = SafeOpenAI(api_key="sk-...", ctxfw_url="http://localhost:8080")
26
+ ```
27
+
28
+ Blocked requests raise `contextwall_sdk.ContextWallBlockedError` with the policy violation detail.
29
+
30
+ ## Daemon setup
31
+
32
+ The SDK requires a running ContextWall daemon. See the [quickstart](https://contextwall.io/quickstart) or the [GitHub repo](https://github.com/bytewise-ca/context-wall).
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.21"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "contextwall-sdk"
7
+ version = "0.1.1"
8
+ description = "ContextWall SDK - drop-in wrapper for Anthropic and OpenAI with context firewall enforcement"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "Apache-2.0" }
12
+ authors = [
13
+ { name = "Context Firewall", email = "sumesh.punakkal@gmail.com" },
14
+ ]
15
+ keywords = ["ai", "security", "context-firewall", "anthropic", "openai", "governance"]
16
+
17
+ dependencies = [
18
+ "httpx>=0.27",
19
+ "pydantic>=2.0",
20
+ ]
21
+
22
+ [project.optional-dependencies]
23
+ anthropic = ["anthropic>=0.25"]
24
+ openai = ["openai>=1.0"]
25
+ all = ["anthropic>=0.25", "openai>=1.0"]
26
+ dev = ["pytest>=8.0", "pytest-asyncio>=0.24", "anthropic>=0.25", "openai>=1.0", "httpx>=0.27"]
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["src/contextwall_sdk"]
30
+
31
+ [tool.hatch.metadata]
32
+ allow-direct-references = true
33
+
34
+ [tool.pytest.ini_options]
35
+ asyncio_mode = "auto"
36
+
37
+ [tool.ruff]
38
+ target-version = "py311"
39
+ line-length = 100
@@ -0,0 +1,59 @@
1
+ """CRE SDK - drop-in context firewall for Anthropic and OpenAI agents.
2
+
3
+ Quick start::
4
+
5
+ pip install 'contextwall-sdk[anthropic]'
6
+
7
+ from contextwall_sdk import SafeAnthropic, CREBlockedError
8
+
9
+ client = SafeAnthropic() # reads CRE_KEY and CRE_URL from env
10
+
11
+ try:
12
+ response = client.messages.create(
13
+ model="claude-opus-4-5",
14
+ max_tokens=1024,
15
+ messages=[{"role": "user", "content": "Hello"}],
16
+ )
17
+ except CREBlockedError as e:
18
+ print(f"Blocked: {e.violations}")
19
+
20
+ Provisioning a key (admin)::
21
+
22
+ from contextwall_sdk import CREClient
23
+
24
+ cre = CREClient(api_key="...", base_url="http://localhost:8080")
25
+ result = cre.keys.create(
26
+ project_id="my-agent",
27
+ upstream_key="sk-ant-...",
28
+ )
29
+ print(result.key) # sk-cre-xxx - store this securely
30
+ """
31
+
32
+ from .exceptions import CREError, CREBlockedError, CREUnavailableError, CREAuthError
33
+ from ._anthropic import SafeAnthropic, AsyncSafeAnthropic
34
+ from ._openai import SafeOpenAI, AsyncSafeOpenAI
35
+ from .client import CREClient, AsyncCREClient, Source, ProxyKeyResult, HealthStatus, AnalyticsSummary
36
+
37
+ __version__ = "0.1.0"
38
+
39
+ __all__ = [
40
+ # Exceptions
41
+ "CREError",
42
+ "CREBlockedError",
43
+ "CREUnavailableError",
44
+ "CREAuthError",
45
+ # Anthropic wrappers
46
+ "SafeAnthropic",
47
+ "AsyncSafeAnthropic",
48
+ # OpenAI wrappers
49
+ "SafeOpenAI",
50
+ "AsyncSafeOpenAI",
51
+ # Admin client
52
+ "CREClient",
53
+ "AsyncCREClient",
54
+ # Response models
55
+ "Source",
56
+ "ProxyKeyResult",
57
+ "HealthStatus",
58
+ "AnalyticsSummary",
59
+ ]
@@ -0,0 +1,269 @@
1
+ """SafeAnthropic - drop-in Anthropic client with CRE enforcement.
2
+
3
+ Usage::
4
+
5
+ # Before
6
+ import anthropic
7
+ client = anthropic.Anthropic(api_key="sk-ant-...")
8
+
9
+ # After (one line change, everything else identical)
10
+ from contextwall_sdk import SafeAnthropic
11
+ client = SafeAnthropic(cre_key="sk-cre-...", cre_url="http://localhost:8080")
12
+
13
+ # Same API
14
+ response = client.messages.create(
15
+ model="claude-opus-4-5",
16
+ max_tokens=1024,
17
+ messages=[{"role": "user", "content": "Hello"}],
18
+ )
19
+
20
+ Environment variables (no code changes needed)::
21
+
22
+ CRE_KEY=sk-cre-...
23
+ CRE_URL=http://localhost:8080
24
+
25
+ from contextwall_sdk import SafeAnthropic
26
+ client = SafeAnthropic() # reads from env
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import os
32
+ from typing import Any, Iterator, AsyncIterator
33
+
34
+ from .exceptions import CREBlockedError, CREUnavailableError, CREAuthError
35
+
36
+
37
+ def _check_cre_block(body: Any) -> None:
38
+ """Raise CREBlockedError if the response body is a CRE policy violation."""
39
+ if not isinstance(body, dict):
40
+ return
41
+ err = body.get("error", {})
42
+ if isinstance(err, dict) and err.get("type") == "cre_policy_violation":
43
+ raise CREBlockedError(
44
+ blocked_reason=err.get("message", "policy violation"),
45
+ violations=err.get("violations", []),
46
+ raw_body=body,
47
+ )
48
+
49
+
50
+ def _wrap_exception(exc: Exception, cre_url: str) -> None:
51
+ """Convert underlying SDK exceptions into CRE-specific ones where applicable."""
52
+ try:
53
+ import httpx
54
+ if isinstance(exc, httpx.ConnectError):
55
+ raise CREUnavailableError(cre_url, cause=exc) from exc
56
+ except ImportError:
57
+ pass
58
+
59
+ # Anthropic SDK exception inspection
60
+ exc_type = type(exc).__name__
61
+ if exc_type in ("AuthenticationError",):
62
+ raise CREAuthError() from exc
63
+
64
+ if exc_type in ("BadRequestError", "APIStatusError"):
65
+ body = getattr(exc, "body", None)
66
+ _check_cre_block(body)
67
+
68
+
69
+ class _StreamContextWrapper:
70
+ """Wraps an Anthropic stream context manager to catch CRE blocks on entry."""
71
+
72
+ def __init__(self, stream_cm: Any, cre_url: str) -> None:
73
+ self._cm = stream_cm
74
+ self._cre_url = cre_url
75
+
76
+ def __enter__(self) -> Any:
77
+ try:
78
+ return self._cm.__enter__()
79
+ except Exception as exc:
80
+ _wrap_exception(exc, self._cre_url)
81
+ raise
82
+
83
+ def __exit__(self, *args: Any) -> Any:
84
+ return self._cm.__exit__(*args)
85
+
86
+ def __getattr__(self, name: str) -> Any:
87
+ return getattr(self._cm, name)
88
+
89
+
90
+ class _AsyncStreamContextWrapper:
91
+ def __init__(self, stream_cm: Any, cre_url: str) -> None:
92
+ self._cm = stream_cm
93
+ self._cre_url = cre_url
94
+
95
+ async def __aenter__(self) -> Any:
96
+ try:
97
+ return await self._cm.__aenter__()
98
+ except Exception as exc:
99
+ _wrap_exception(exc, self._cre_url)
100
+ raise
101
+
102
+ async def __aexit__(self, *args: Any) -> Any:
103
+ return await self._cm.__aexit__(*args)
104
+
105
+ def __getattr__(self, name: str) -> Any:
106
+ return getattr(self._cm, name)
107
+
108
+
109
+ class _MessagesWrapper:
110
+ """Wraps anthropic.resources.Messages to surface CRE-specific errors."""
111
+
112
+ def __init__(self, messages: Any, cre_url: str, fallback: bool) -> None:
113
+ self._messages = messages
114
+ self._cre_url = cre_url
115
+ self._fallback = fallback
116
+
117
+ def create(self, **kwargs: Any) -> Any:
118
+ try:
119
+ return self._messages.create(**kwargs)
120
+ except Exception as exc:
121
+ _wrap_exception(exc, self._cre_url)
122
+ raise
123
+
124
+ def stream(self, **kwargs: Any) -> _StreamContextWrapper:
125
+ # stream() returns a context manager; errors from CRE surface on __enter__
126
+ return _StreamContextWrapper(self._messages.stream(**kwargs), self._cre_url)
127
+
128
+ def __getattr__(self, name: str) -> Any:
129
+ return getattr(self._messages, name)
130
+
131
+
132
+ class _AsyncMessagesWrapper:
133
+ def __init__(self, messages: Any, cre_url: str, fallback: bool) -> None:
134
+ self._messages = messages
135
+ self._cre_url = cre_url
136
+ self._fallback = fallback
137
+
138
+ async def create(self, **kwargs: Any) -> Any:
139
+ try:
140
+ return await self._messages.create(**kwargs)
141
+ except Exception as exc:
142
+ _wrap_exception(exc, self._cre_url)
143
+ raise
144
+
145
+ def stream(self, **kwargs: Any) -> _AsyncStreamContextWrapper:
146
+ return _AsyncStreamContextWrapper(self._messages.stream(**kwargs), self._cre_url)
147
+
148
+ def __getattr__(self, name: str) -> Any:
149
+ return getattr(self._messages, name)
150
+
151
+
152
+ class SafeAnthropic:
153
+ """Drop-in replacement for ``anthropic.Anthropic`` with CRE enforcement.
154
+
155
+ Args:
156
+ cre_key: Your ``sk-cre-xxx`` key. Falls back to
157
+ ``CRE_KEY`` then ``ANTHROPIC_API_KEY`` env vars.
158
+ cre_url: CRE daemon URL. Falls back to ``CRE_URL`` env var,
159
+ then ``http://localhost:8080``.
160
+ fallback_on_unavailable: If True and CRE is unreachable, raises
161
+ ``CREUnavailableError`` (default False = fail fast).
162
+ **kwargs: Passed through to ``anthropic.Anthropic()``.
163
+
164
+ Raises:
165
+ CREBlockedError: When CRE blocks the request (policy violation).
166
+ CREUnavailableError: When CRE cannot be reached (and fallback is off).
167
+ CREAuthError: When the CRE key is rejected.
168
+ ImportError: If ``anthropic`` package is not installed.
169
+ """
170
+
171
+ def __init__(
172
+ self,
173
+ cre_key: str | None = None,
174
+ cre_url: str | None = None,
175
+ fallback_on_unavailable: bool = False,
176
+ **kwargs: Any,
177
+ ) -> None:
178
+ try:
179
+ import anthropic
180
+ except ImportError as e:
181
+ raise ImportError(
182
+ "anthropic package is required: pip install 'contextwall-sdk[anthropic]'"
183
+ ) from e
184
+
185
+ self._cre_url = (
186
+ cre_url or os.environ.get("CRE_URL") or "http://localhost:8080"
187
+ ).rstrip("/")
188
+ self._fallback = fallback_on_unavailable
189
+
190
+ key = (
191
+ cre_key
192
+ or os.environ.get("CRE_KEY")
193
+ or os.environ.get("ANTHROPIC_API_KEY")
194
+ or ""
195
+ )
196
+
197
+ self._client = anthropic.Anthropic(
198
+ api_key=key,
199
+ base_url=f"{self._cre_url}/proxy/anthropic",
200
+ **kwargs,
201
+ )
202
+
203
+ @property
204
+ def messages(self) -> _MessagesWrapper:
205
+ return _MessagesWrapper(self._client.messages, self._cre_url, self._fallback)
206
+
207
+ @property
208
+ def beta(self) -> Any:
209
+ return self._client.beta
210
+
211
+ def __getattr__(self, name: str) -> Any:
212
+ return getattr(self._client, name)
213
+
214
+
215
+ class AsyncSafeAnthropic:
216
+ """Async version of SafeAnthropic. Drop-in for ``anthropic.AsyncAnthropic``.
217
+
218
+ Example::
219
+
220
+ client = AsyncSafeAnthropic()
221
+ response = await client.messages.create(
222
+ model="claude-opus-4-5",
223
+ max_tokens=1024,
224
+ messages=[{"role": "user", "content": "Hello"}],
225
+ )
226
+ """
227
+
228
+ def __init__(
229
+ self,
230
+ cre_key: str | None = None,
231
+ cre_url: str | None = None,
232
+ fallback_on_unavailable: bool = False,
233
+ **kwargs: Any,
234
+ ) -> None:
235
+ try:
236
+ import anthropic
237
+ except ImportError as e:
238
+ raise ImportError(
239
+ "anthropic package is required: pip install 'contextwall-sdk[anthropic]'"
240
+ ) from e
241
+
242
+ self._cre_url = (
243
+ cre_url or os.environ.get("CRE_URL") or "http://localhost:8080"
244
+ ).rstrip("/")
245
+ self._fallback = fallback_on_unavailable
246
+
247
+ key = (
248
+ cre_key
249
+ or os.environ.get("CRE_KEY")
250
+ or os.environ.get("ANTHROPIC_API_KEY")
251
+ or ""
252
+ )
253
+
254
+ self._client = anthropic.AsyncAnthropic(
255
+ api_key=key,
256
+ base_url=f"{self._cre_url}/proxy/anthropic",
257
+ **kwargs,
258
+ )
259
+
260
+ @property
261
+ def messages(self) -> _AsyncMessagesWrapper:
262
+ return _AsyncMessagesWrapper(self._client.messages, self._cre_url, self._fallback)
263
+
264
+ @property
265
+ def beta(self) -> Any:
266
+ return self._client.beta
267
+
268
+ def __getattr__(self, name: str) -> Any:
269
+ return getattr(self._client, name)