shieldcortex 0.3.0__tar.gz → 0.5.0__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 (50) hide show
  1. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/.github/workflows/test.yml +1 -1
  2. shieldcortex-0.5.0/CHANGELOG.md +159 -0
  3. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/PKG-INFO +108 -11
  4. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/README.md +107 -10
  5. shieldcortex-0.5.0/examples/langchain_callback.py +42 -0
  6. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/pyproject.toml +1 -1
  7. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/src/shieldcortex/_http.py +19 -0
  8. shieldcortex-0.5.0/src/shieldcortex/_version.py +1 -0
  9. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/src/shieldcortex/async_client.py +27 -26
  10. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/src/shieldcortex/client.py +27 -26
  11. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/src/shieldcortex/integrations/crewai.py +6 -6
  12. shieldcortex-0.5.0/src/shieldcortex/integrations/langchain.py +345 -0
  13. shieldcortex-0.5.0/tests/integrations/__init__.py +0 -0
  14. shieldcortex-0.5.0/tests/integrations/test_langchain.py +713 -0
  15. shieldcortex-0.5.0/tests/test_packaging.py +17 -0
  16. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/tests/test_parity.py +2 -0
  17. shieldcortex-0.5.0/tests/test_path_params.py +167 -0
  18. shieldcortex-0.3.0/CHANGELOG.md +0 -57
  19. shieldcortex-0.3.0/examples/langchain_callback.py +0 -34
  20. shieldcortex-0.3.0/src/shieldcortex/_version.py +0 -1
  21. shieldcortex-0.3.0/src/shieldcortex/integrations/langchain.py +0 -141
  22. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/.github/workflows/publish.yml +0 -0
  23. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/.gitignore +0 -0
  24. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/LICENSE +0 -0
  25. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/examples/audit_export.py +0 -0
  26. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/examples/basic_scan.py +0 -0
  27. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/examples/batch_scanning.py +0 -0
  28. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/examples/crewai_memory.py +0 -0
  29. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/src/shieldcortex/__init__.py +0 -0
  30. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/src/shieldcortex/_payloads.py +0 -0
  31. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/src/shieldcortex/errors.py +0 -0
  32. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/src/shieldcortex/integrations/__init__.py +0 -0
  33. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/src/shieldcortex/pagination.py +0 -0
  34. /shieldcortex-0.3.0/tests/__init__.py → /shieldcortex-0.5.0/src/shieldcortex/py.typed +0 -0
  35. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/src/shieldcortex/types.py +0 -0
  36. {shieldcortex-0.3.0/tests/integrations → shieldcortex-0.5.0/tests}/__init__.py +0 -0
  37. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/tests/conftest.py +0 -0
  38. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/tests/endpoint_manifest.py +0 -0
  39. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/tests/fixtures.py +0 -0
  40. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/tests/integrations/test_crewai.py +0 -0
  41. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/tests/test_audit_exports.py +0 -0
  42. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/tests/test_client.py +0 -0
  43. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/tests/test_errors.py +0 -0
  44. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/tests/test_incidents_recall.py +0 -0
  45. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/tests/test_license.py +0 -0
  46. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/tests/test_pagination.py +0 -0
  47. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/tests/test_skills_threats.py +0 -0
  48. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/tests/test_sync.py +0 -0
  49. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/tests/test_types.py +0 -0
  50. {shieldcortex-0.3.0 → shieldcortex-0.5.0}/tests/test_verify.py +0 -0
@@ -18,7 +18,7 @@ jobs:
18
18
  with:
19
19
  python-version: ${{ matrix.python-version }}
20
20
  - name: Install dependencies
21
- run: pip install -e ".[dev]"
21
+ run: pip install -e ".[dev,langchain]"
22
22
  - name: Lint
23
23
  run: ruff check src/
24
24
  - name: Type check
@@ -0,0 +1,159 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ## 0.5.0 — 2026-10-17
6
+
7
+ Minor release: opt-in fail-closed and QUARANTINE blocking for the LangChain
8
+ handlers, structured tool outputs scanned, and path parameters URL-encoded.
9
+ 0.4.0 was never tagged or published, so this is the first published release
10
+ after 0.3.0 and also carries every 0.4.0 change listed below.
11
+
12
+ Defaults are unchanged for API errors and QUARANTINE: a `ShieldCortexError`
13
+ from a scan is still logged and skipped, and QUARANTINE still does not act,
14
+ unless you opt in with the new options below.
15
+
16
+ ### Added
17
+
18
+ - LangChain handlers: `fail_closed` (default `False`). With
19
+ `raise_on_block=True`, a `ShieldCortexError` from a scan (401, 429, 5xx,
20
+ timeout, and every other API error) raises `ContentBlockedError` with
21
+ `result=None` and the original error as `__cause__`, so the run stops
22
+ (except for sync-only tools under `ainvoke` with the async handler; see
23
+ Known limitation). `fail_closed=True` without `raise_on_block=True` raises `ValueError` at
24
+ construction.
25
+ - `ContentBlockedError.result` is now typed `ScanResult | None`. It is only
26
+ `None` with `fail_closed=True`, so default-option callers never see `None`
27
+ at runtime, but type checkers will ask them to narrow before reading
28
+ `error.result.firewall`:
29
+ `if error.result is not None: print(error.result.firewall.reason)`.
30
+ - LangChain handlers: `block_on_quarantine` (default `False`). When `True`,
31
+ a QUARANTINE verdict is treated exactly like BLOCK.
32
+
33
+ ### Fixed
34
+
35
+ - LangChain `on_tool_end` now scans structured tool outputs (ToolMessage
36
+ content, content blocks, dicts) instead of skipping them. Non-string
37
+ outputs are reduced to text (`.content` string, joined text parts of a
38
+ content-block list, or JSON with a `str()` fallback); non-text blocks in a
39
+ content list are dropped, not JSON-encoded; `None` and outputs
40
+ that reduce to empty text are not scanned. String outputs are scanned as
41
+ before.
42
+ - Path parameters (ids, device UUIDs, export manifest ids) are now
43
+ percent-encoded as a single URL segment, so `/`, `?`, `#`, `%`, spaces and
44
+ non-ASCII characters can no longer change the route. Empty, `.` and `..`
45
+ values raise `ValueError` before any request is sent.
46
+
47
+ ### Known limitation
48
+
49
+ - LangChain answers a bare (non-chat) LLM call that is fully served from its
50
+ LLM cache without firing `on_llm_start` or `on_llm_end`, so neither the
51
+ prompt nor the cached output is scanned and no block can stop that call.
52
+ The 0.4.0 bare-LLM prompt scanning applies only to calls that reach the
53
+ model. Set `cache=False` on LLMs whose calls must be scanned (the default,
54
+ `cache=None`, uses any cache set with `set_llm_cache`), or scan outside the
55
+ model call. Chat models fire their start callback before the cache lookup,
56
+ so cached chat calls still have their input scanned. Regression tests pin
57
+ this behaviour.
58
+ - With the async handler, a sync-only tool (no coroutine) called through
59
+ `ainvoke` runs in a worker thread where LangChain logs and swallows every
60
+ exception the handler raises. Neither BLOCK verdicts nor `fail_closed`
61
+ scan-error raises stop that tool: it runs and `ainvoke` returns its output.
62
+ Give the tool an async implementation, or pass the sync handler (with a
63
+ sync client) for those calls; its exceptions propagate on that route.
64
+
65
+ ## 0.4.0 — 2026-10-16
66
+
67
+ Minor release: the LangChain integration now stops the run on BLOCK, scans
68
+ bare LLM and chat prompts, and adds two public names
69
+ (`ShieldCortexSyncCallbackHandler`, `ContentBlockedError`). No breaking changes.
70
+
71
+ ### Fixed
72
+
73
+ - LangChain async blocks now propagate when `raise_on_block=True`.
74
+ - Bare LLM and chat model inputs are scanned by their start callbacks.
75
+ Chat message text covers string content, string blocks, and dict blocks
76
+ whose `text` is a string and whose `type` is `"text"` or absent. Empty
77
+ prompts and empty chat text are skipped; chain and tool scans are unchanged.
78
+
79
+ ### Added
80
+
81
+ - `ShieldCortexSyncCallbackHandler` for synchronous LangChain calls and
82
+ `ContentBlockedError` for catching blocked content with its scan result.
83
+ - Ship a PEP 561 `py.typed` marker so type checkers treat the installed
84
+ package as typed (the `Typing :: Typed` classifier was already declared).
85
+
86
+ ### Documentation
87
+
88
+ - LangChain: output is scanned when the model finishes, so with `stream` /
89
+ `astream` a BLOCK on output stops the run only after the chunks were
90
+ delivered. Only `ShieldCortexError` from the scan call is logged and
91
+ skipped; other exceptions (for example connection errors) stop the run
92
+ when `raise_on_block=True`.
93
+ - README: describe `ShieldCortexMemoryGuard` as a framework-agnostic
94
+ pre-write guard (it does not import or hook into CrewAI; the `[crewai]`
95
+ extra only installs CrewAI itself).
96
+ - README: document the sync client's context manager
97
+ (`with ShieldCortex(...) as client:`) and `client.close()`.
98
+
99
+ ## 0.3.0 — 2026-08-15
100
+
101
+ - Version-only release, coordinated with core v4.52.0. No API changes:
102
+ only the package version (`pyproject.toml`, `_version.py`) changed
103
+ from 0.2.0.
104
+
105
+ ## 0.2.0 — 2026-08-11
106
+
107
+ SDK lockstep release: the Python SDK now covers the full customer surface
108
+ documented in the SDK lockstep contract, in lockstep with the TypeScript SDK.
109
+
110
+ ### Added
111
+
112
+ - **Verification** — `submit_verification()`, `list_verifications()`,
113
+ `get_verification()`, `get_verification_stats()`, `delete_verification()`
114
+ - **Skills** — `ingest_skill_scans()`, `list_skill_scans()`
115
+ - **Threats** — `report_threat()` (OpenClaw realtime compat shim)
116
+ - **Incidents** — `replay_incidents()`
117
+ - **Recall** — `explain_recall()`
118
+ - **Sync** — `get_sync_health()`, `push_memories()`, `list_synced_memories()`,
119
+ `push_memory_graph()`. Note: `list_synced_memories()` omits the
120
+ `include_deleted` param unless truthy — the server Boolean-coerces any
121
+ present value (even `"false"`) as enabling it. Every other boolean query
122
+ param was audited for the same coercion trap; none affected.
123
+ - **Licence** — `get_license()`, `regenerate_license()`
124
+ - **Audit** — `ingest_audit_events()`, `get_iron_dome_stats()`,
125
+ `get_iron_dome_events()`, and the export-manifest surface
126
+ (`list_audit_exports()`, `get_audit_export_manifest()`,
127
+ `verify_audit_export()`, `list_audit_export_verifications()`)
128
+ - Cross-SDK endpoint parity manifest and drift guard
129
+ (`tests/endpoint_manifest.py`, kept in lockstep with the TS SDK's
130
+ `tests/endpoint-manifest.ts`), including a sync/async surface parity test
131
+ - Shared request builders (`_payloads.py`) so the sync and async clients
132
+ construct identical wire payloads and cannot drift
133
+
134
+ ### Changed
135
+
136
+ - **BREAKING:** `export_audit_logs()` (both clients) now returns
137
+ `AuditExportResult` — the raw file body in `.content` plus the parsed
138
+ `X-ShieldCortex-Export-*` integrity headers in `.headers` — instead of a
139
+ bare `str`. Previously the integrity headers were discarded, so exports
140
+ structurally could not be verified. An absent `sha256`/`signature` header
141
+ is `None` (never `""`): the export is unverifiable. Migration:
142
+ `client.export_audit_logs(...)` → `client.export_audit_logs(...).content`.
143
+
144
+ ### Deprecated
145
+
146
+ - `create_checkout_session()` / `create_portal_session()` now emit
147
+ `DeprecationWarning`: self-serve plans were retired 2026-07 (Free +
148
+ Enterprise model); the endpoints remain for grandfathered licence holders.
149
+
150
+ ## 0.1.1 — 2026-03-12
151
+
152
+ - Added Iron Dome patterns/policies client support; fixed the default-policy
153
+ verb (PUT).
154
+
155
+ ## 0.1.0 — 2026-02-15
156
+
157
+ - Initial release: scanning, audit, quarantine, keys, teams, invites,
158
+ billing, devices, alerts, webhooks, firewall rules; sync + async clients;
159
+ CrewAI and LangChain integrations; Python 3.9+ compatibility fixes.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: shieldcortex
3
- Version: 0.3.0
3
+ Version: 0.5.0
4
4
  Summary: Official Python SDK for the ShieldCortex API — AI memory security scanning
5
5
  Project-URL: Homepage, https://shieldcortex.ai
6
6
  Project-URL: Documentation, https://shieldcortex.ai/docs
@@ -74,7 +74,7 @@ pip install shieldcortex
74
74
  With framework integrations:
75
75
 
76
76
  ```bash
77
- pip install shieldcortex[crewai] # CrewAI memory guard
77
+ pip install shieldcortex[crewai] # also installs CrewAI (the memory guard itself does not need it)
78
78
  pip install shieldcortex[langchain] # LangChain callback handler
79
79
  ```
80
80
 
@@ -96,6 +96,19 @@ else:
96
96
  # don't store — content failed security checks
97
97
  ```
98
98
 
99
+ The sync client holds an `httpx` connection pool. Use it as a context manager
100
+ so the pool is closed when you are done:
101
+
102
+ ```python
103
+ from shieldcortex import ShieldCortex
104
+
105
+ with ShieldCortex(api_key="sc_live_...") as client:
106
+ result = client.scan("user input to remember")
107
+ ```
108
+
109
+ In long-lived processes that keep a client around instead, call
110
+ `client.close()` when you are finished with it.
111
+
99
112
  ## Async Support
100
113
 
101
114
  ```python
@@ -119,9 +132,9 @@ result = client.scan_batch([
119
132
  print(f"Scanned: {result.total_scanned}, Threats: {result.threats}")
120
133
  ```
121
134
 
122
- ## CrewAI Integration
135
+ ## Memory Guard (CrewAI and others)
123
136
 
124
- Add a security gate between your CrewAI agent and its memory store. The guard scans all content before it's saved — blocking prompt injection and credential leaks:
137
+ `ShieldCortexMemoryGuard` is a framework-agnostic pre-write check: call `guard.check()` before writing content to any memory store — from a CrewAI agent, another framework, or your own code. It scans the content and raises `MemoryBlockedError` if it is blocked (or, by default, quarantined). It does not hook into CrewAI automatically and does not import CrewAI; the `[crewai]` extra only installs CrewAI itself.
125
138
 
126
139
  ```python
127
140
  from shieldcortex import ShieldCortex
@@ -139,17 +152,101 @@ except MemoryBlockedError as e:
139
152
 
140
153
  ## LangChain Integration
141
154
 
142
- Automatically scan all LLM inputs and outputs as they flow through your chain — no changes to your existing code:
155
+ The callbacks scan chain inputs, LLM prompts, text in chat messages, tool inputs,
156
+ LLM outputs, and tool outputs, whenever LangChain fires the matching callback
157
+ (see **LLM cache** below for a case where it does not). Use the async handler
158
+ for `ainvoke` and `astream`, or the sync handler for `invoke`:
143
159
 
144
160
  ```python
145
- from shieldcortex import AsyncShieldCortex
146
- from shieldcortex.integrations.langchain import ShieldCortexCallbackHandler
161
+ from langchain_openai import ChatOpenAI # or any LangChain chat model / LLM
162
+ from shieldcortex import AsyncShieldCortex, ShieldCortex
163
+ from shieldcortex.integrations.langchain import (
164
+ ContentBlockedError,
165
+ ShieldCortexCallbackHandler,
166
+ ShieldCortexSyncCallbackHandler,
167
+ )
147
168
 
148
- client = AsyncShieldCortex(api_key="sc_live_...")
149
- handler = ShieldCortexCallbackHandler(client, raise_on_block=True)
169
+ model = ChatOpenAI()
150
170
 
151
- # Scans inputs on chain start, outputs on LLM end, and tool I/O
152
- llm = ChatOpenAI(callbacks=[handler])
171
+ async with AsyncShieldCortex(api_key="sc_live_...") as client:
172
+ handler = ShieldCortexCallbackHandler(client, raise_on_block=True)
173
+ try:
174
+ reply = await model.ainvoke("Hello", config={"callbacks": [handler]})
175
+ except ContentBlockedError as error: # also a ValueError
176
+ print(error.result.firewall.reason, error.source_identifier)
177
+
178
+ with ShieldCortex(api_key="sc_live_...") as client:
179
+ handler = ShieldCortexSyncCallbackHandler(client, raise_on_block=True)
180
+ reply = model.invoke("Hello", config={"callbacks": [handler]})
181
+ ```
182
+
183
+ By default only `BLOCK` verdicts act. With `raise_on_block=True`, a block stops
184
+ the run with `ContentBlockedError`; otherwise it logs a warning. Use the matching
185
+ handler for the call mode: LangChain cannot propagate an async handler's block
186
+ from `invoke`.
187
+
188
+ - **Chat message text** means string content, string blocks, and dict blocks
189
+ whose `text` is a string and whose `type` is `"text"` or absent. Images and
190
+ other block types are not scanned. Empty prompts are skipped.
191
+ - **Streaming:** LLM output is scanned when the model finishes. With `stream`
192
+ or `astream`, every chunk has already reached your code by then, so an
193
+ output block stops the run after delivery and cannot prevent exposure.
194
+ Buffer the stream yourself if blocked output must never be shown. Input
195
+ scans still run before the model starts.
196
+ - **Tool outputs** are reduced to text before scanning: strings as is; objects
197
+ with string `.content` (such as `ToolMessage`) by that content; a list
198
+ `.content` by its string items and the string `text` of its dict items,
199
+ joined by newlines; anything else as JSON (`default=str`), falling back to
200
+ `str()`. Other blocks in a list `.content`, such as images or dicts without
201
+ a string `text` (for example `{"payload": "..."}`), are dropped rather than
202
+ JSON-encoded, so their contents are not scanned. `None` and non-string outputs that reduce
203
+ to empty text are not scanned.
204
+ - **LLM cache:** a bare (non-chat) LLM call answered entirely from a
205
+ LangChain LLM cache fires no callbacks: LangChain checks the cache before
206
+ `on_llm_start` and returns the cached text without `on_llm_end`. Neither
207
+ the prompt nor the cached output is scanned, and no block or `fail_closed`
208
+ error can stop that call. Set `cache=False` on LLMs whose calls must be
209
+ scanned (the default, `cache=None`, uses any cache set with
210
+ `set_llm_cache`), or scan outside the model call. Chat models fire
211
+ `on_chat_model_start` before their cache lookup, so a cached chat call
212
+ still has its input scanned.
213
+ - **Sync-only tools under `ainvoke`:** LangChain runs a tool that has no
214
+ coroutine in a worker thread and logs and swallows every exception the
215
+ async handler raises there. Neither a `BLOCK` verdict nor a `fail_closed`
216
+ scan error stops that tool: it runs and `ainvoke` returns its output. Give
217
+ the tool an async implementation (`coroutine=` on `StructuredTool`, or
218
+ `_arun`), or pass the sync handler with a sync client for those calls,
219
+ e.g. `await tool.ainvoke(args, config={"callbacks": [sync_handler]})`; its
220
+ exceptions do propagate on that route.
221
+ - **Scan errors:** by default a `ShieldCortexError` from the scan call (any
222
+ HTTP error such as 401, 429 or 5xx, or a timeout) is logged and that scan is
223
+ skipped, so the content passes. Other exceptions, such as connection errors,
224
+ are not caught: with `raise_on_block=True` they stop the run, otherwise
225
+ LangChain logs them.
226
+
227
+ Two opt-in options, available on both handlers:
228
+
229
+ | Option | Default | Effect |
230
+ |---|---|---|
231
+ | `fail_closed` | `False` | With `raise_on_block=True`, a `ShieldCortexError` from a scan raises `ContentBlockedError` instead of being logged and skipped (except for sync-only tools under `ainvoke` with the async handler, see above). `error.result` is `None` and `error.__cause__` is the original error. Requires `raise_on_block=True`; otherwise the constructor raises `ValueError`. |
232
+ | `block_on_quarantine` | `False` | Treat `QUARANTINE` verdicts exactly like `BLOCK` (warn, or raise with `raise_on_block=True`). |
233
+
234
+ ```python
235
+ handler = ShieldCortexSyncCallbackHandler(
236
+ client, raise_on_block=True, fail_closed=True, block_on_quarantine=True
237
+ )
238
+ ```
239
+
240
+ `ContentBlockedError.result` is typed `ScanResult | None`. Without
241
+ `fail_closed` it is never `None` at runtime, but type checkers will still ask
242
+ you to narrow it before reading `error.result.firewall`:
243
+
244
+ ```python
245
+ except ContentBlockedError as error:
246
+ if error.result is None: # fail_closed: the scan itself failed
247
+ print("scan failed:", error.__cause__)
248
+ else:
249
+ print(error.result.firewall.reason)
153
250
  ```
154
251
 
155
252
  ## Audit Logs
@@ -35,7 +35,7 @@ pip install shieldcortex
35
35
  With framework integrations:
36
36
 
37
37
  ```bash
38
- pip install shieldcortex[crewai] # CrewAI memory guard
38
+ pip install shieldcortex[crewai] # also installs CrewAI (the memory guard itself does not need it)
39
39
  pip install shieldcortex[langchain] # LangChain callback handler
40
40
  ```
41
41
 
@@ -57,6 +57,19 @@ else:
57
57
  # don't store — content failed security checks
58
58
  ```
59
59
 
60
+ The sync client holds an `httpx` connection pool. Use it as a context manager
61
+ so the pool is closed when you are done:
62
+
63
+ ```python
64
+ from shieldcortex import ShieldCortex
65
+
66
+ with ShieldCortex(api_key="sc_live_...") as client:
67
+ result = client.scan("user input to remember")
68
+ ```
69
+
70
+ In long-lived processes that keep a client around instead, call
71
+ `client.close()` when you are finished with it.
72
+
60
73
  ## Async Support
61
74
 
62
75
  ```python
@@ -80,9 +93,9 @@ result = client.scan_batch([
80
93
  print(f"Scanned: {result.total_scanned}, Threats: {result.threats}")
81
94
  ```
82
95
 
83
- ## CrewAI Integration
96
+ ## Memory Guard (CrewAI and others)
84
97
 
85
- Add a security gate between your CrewAI agent and its memory store. The guard scans all content before it's saved — blocking prompt injection and credential leaks:
98
+ `ShieldCortexMemoryGuard` is a framework-agnostic pre-write check: call `guard.check()` before writing content to any memory store — from a CrewAI agent, another framework, or your own code. It scans the content and raises `MemoryBlockedError` if it is blocked (or, by default, quarantined). It does not hook into CrewAI automatically and does not import CrewAI; the `[crewai]` extra only installs CrewAI itself.
86
99
 
87
100
  ```python
88
101
  from shieldcortex import ShieldCortex
@@ -100,17 +113,101 @@ except MemoryBlockedError as e:
100
113
 
101
114
  ## LangChain Integration
102
115
 
103
- Automatically scan all LLM inputs and outputs as they flow through your chain — no changes to your existing code:
116
+ The callbacks scan chain inputs, LLM prompts, text in chat messages, tool inputs,
117
+ LLM outputs, and tool outputs, whenever LangChain fires the matching callback
118
+ (see **LLM cache** below for a case where it does not). Use the async handler
119
+ for `ainvoke` and `astream`, or the sync handler for `invoke`:
104
120
 
105
121
  ```python
106
- from shieldcortex import AsyncShieldCortex
107
- from shieldcortex.integrations.langchain import ShieldCortexCallbackHandler
122
+ from langchain_openai import ChatOpenAI # or any LangChain chat model / LLM
123
+ from shieldcortex import AsyncShieldCortex, ShieldCortex
124
+ from shieldcortex.integrations.langchain import (
125
+ ContentBlockedError,
126
+ ShieldCortexCallbackHandler,
127
+ ShieldCortexSyncCallbackHandler,
128
+ )
108
129
 
109
- client = AsyncShieldCortex(api_key="sc_live_...")
110
- handler = ShieldCortexCallbackHandler(client, raise_on_block=True)
130
+ model = ChatOpenAI()
111
131
 
112
- # Scans inputs on chain start, outputs on LLM end, and tool I/O
113
- llm = ChatOpenAI(callbacks=[handler])
132
+ async with AsyncShieldCortex(api_key="sc_live_...") as client:
133
+ handler = ShieldCortexCallbackHandler(client, raise_on_block=True)
134
+ try:
135
+ reply = await model.ainvoke("Hello", config={"callbacks": [handler]})
136
+ except ContentBlockedError as error: # also a ValueError
137
+ print(error.result.firewall.reason, error.source_identifier)
138
+
139
+ with ShieldCortex(api_key="sc_live_...") as client:
140
+ handler = ShieldCortexSyncCallbackHandler(client, raise_on_block=True)
141
+ reply = model.invoke("Hello", config={"callbacks": [handler]})
142
+ ```
143
+
144
+ By default only `BLOCK` verdicts act. With `raise_on_block=True`, a block stops
145
+ the run with `ContentBlockedError`; otherwise it logs a warning. Use the matching
146
+ handler for the call mode: LangChain cannot propagate an async handler's block
147
+ from `invoke`.
148
+
149
+ - **Chat message text** means string content, string blocks, and dict blocks
150
+ whose `text` is a string and whose `type` is `"text"` or absent. Images and
151
+ other block types are not scanned. Empty prompts are skipped.
152
+ - **Streaming:** LLM output is scanned when the model finishes. With `stream`
153
+ or `astream`, every chunk has already reached your code by then, so an
154
+ output block stops the run after delivery and cannot prevent exposure.
155
+ Buffer the stream yourself if blocked output must never be shown. Input
156
+ scans still run before the model starts.
157
+ - **Tool outputs** are reduced to text before scanning: strings as is; objects
158
+ with string `.content` (such as `ToolMessage`) by that content; a list
159
+ `.content` by its string items and the string `text` of its dict items,
160
+ joined by newlines; anything else as JSON (`default=str`), falling back to
161
+ `str()`. Other blocks in a list `.content`, such as images or dicts without
162
+ a string `text` (for example `{"payload": "..."}`), are dropped rather than
163
+ JSON-encoded, so their contents are not scanned. `None` and non-string outputs that reduce
164
+ to empty text are not scanned.
165
+ - **LLM cache:** a bare (non-chat) LLM call answered entirely from a
166
+ LangChain LLM cache fires no callbacks: LangChain checks the cache before
167
+ `on_llm_start` and returns the cached text without `on_llm_end`. Neither
168
+ the prompt nor the cached output is scanned, and no block or `fail_closed`
169
+ error can stop that call. Set `cache=False` on LLMs whose calls must be
170
+ scanned (the default, `cache=None`, uses any cache set with
171
+ `set_llm_cache`), or scan outside the model call. Chat models fire
172
+ `on_chat_model_start` before their cache lookup, so a cached chat call
173
+ still has its input scanned.
174
+ - **Sync-only tools under `ainvoke`:** LangChain runs a tool that has no
175
+ coroutine in a worker thread and logs and swallows every exception the
176
+ async handler raises there. Neither a `BLOCK` verdict nor a `fail_closed`
177
+ scan error stops that tool: it runs and `ainvoke` returns its output. Give
178
+ the tool an async implementation (`coroutine=` on `StructuredTool`, or
179
+ `_arun`), or pass the sync handler with a sync client for those calls,
180
+ e.g. `await tool.ainvoke(args, config={"callbacks": [sync_handler]})`; its
181
+ exceptions do propagate on that route.
182
+ - **Scan errors:** by default a `ShieldCortexError` from the scan call (any
183
+ HTTP error such as 401, 429 or 5xx, or a timeout) is logged and that scan is
184
+ skipped, so the content passes. Other exceptions, such as connection errors,
185
+ are not caught: with `raise_on_block=True` they stop the run, otherwise
186
+ LangChain logs them.
187
+
188
+ Two opt-in options, available on both handlers:
189
+
190
+ | Option | Default | Effect |
191
+ |---|---|---|
192
+ | `fail_closed` | `False` | With `raise_on_block=True`, a `ShieldCortexError` from a scan raises `ContentBlockedError` instead of being logged and skipped (except for sync-only tools under `ainvoke` with the async handler, see above). `error.result` is `None` and `error.__cause__` is the original error. Requires `raise_on_block=True`; otherwise the constructor raises `ValueError`. |
193
+ | `block_on_quarantine` | `False` | Treat `QUARANTINE` verdicts exactly like `BLOCK` (warn, or raise with `raise_on_block=True`). |
194
+
195
+ ```python
196
+ handler = ShieldCortexSyncCallbackHandler(
197
+ client, raise_on_block=True, fail_closed=True, block_on_quarantine=True
198
+ )
199
+ ```
200
+
201
+ `ContentBlockedError.result` is typed `ScanResult | None`. Without
202
+ `fail_closed` it is never `None` at runtime, but type checkers will still ask
203
+ you to narrow it before reading `error.result.firewall`:
204
+
205
+ ```python
206
+ except ContentBlockedError as error:
207
+ if error.result is None: # fail_closed: the scan itself failed
208
+ print("scan failed:", error.__cause__)
209
+ else:
210
+ print(error.result.firewall.reason)
114
211
  ```
115
212
 
116
213
  ## Audit Logs
@@ -0,0 +1,42 @@
1
+ """Scan LangChain inputs and outputs with the matching callback handler.
2
+
3
+ Install with ``pip install shieldcortex[langchain]``. The handlers scan chain
4
+ inputs, LLM prompts, text in chat messages, tool inputs, LLM outputs, and
5
+ string tool outputs. Only BLOCK verdicts act. Streamed output is scanned
6
+ after the chunks are delivered, so an output block cannot hide them.
7
+ """
8
+
9
+ import asyncio
10
+
11
+ from langchain_core.language_models.fake_chat_models import FakeListChatModel
12
+
13
+ from shieldcortex import AsyncShieldCortex, ShieldCortex
14
+ from shieldcortex.integrations.langchain import (
15
+ ContentBlockedError,
16
+ ShieldCortexCallbackHandler,
17
+ ShieldCortexSyncCallbackHandler,
18
+ )
19
+
20
+
21
+ async def main() -> None:
22
+ model = FakeListChatModel(responses=["Hello!"])
23
+
24
+ async with AsyncShieldCortex(api_key="sc_live_YOUR_KEY_HERE") as client:
25
+ handler = ShieldCortexCallbackHandler(client, raise_on_block=True)
26
+ try:
27
+ print(await model.ainvoke("Hello", config={"callbacks": [handler]}))
28
+ except ContentBlockedError as error: # also a ValueError
29
+ print(f"Blocked {error.source_identifier}: {error.result.firewall.reason}")
30
+
31
+ # For invoke(), use the sync client and handler. The async handler's block
32
+ # cannot propagate from a sync call.
33
+ with ShieldCortex(api_key="sc_live_YOUR_KEY_HERE") as client:
34
+ handler = ShieldCortexSyncCallbackHandler(client, raise_on_block=True)
35
+ try:
36
+ print(model.invoke("Hello", config={"callbacks": [handler]}))
37
+ except ContentBlockedError as error:
38
+ print(f"Blocked {error.source_identifier}: {error.result.firewall.reason}")
39
+
40
+
41
+ if __name__ == "__main__":
42
+ asyncio.run(main())
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "shieldcortex"
7
- version = "0.3.0"
7
+ version = "0.5.0"
8
8
  description = "Official Python SDK for the ShieldCortex API — AI memory security scanning"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -10,6 +10,7 @@ import dataclasses
10
10
  import re
11
11
  from collections.abc import Callable
12
12
  from typing import Any, TypeVar, get_type_hints
13
+ from urllib.parse import quote
13
14
 
14
15
  import httpx
15
16
 
@@ -121,6 +122,24 @@ def serialize_query(obj: Any) -> dict[str, str]:
121
122
  return params
122
123
 
123
124
 
125
+ # ── Path parameters ───────────────────────────────────────────────────────────
126
+
127
+
128
+ def _seg(value: object) -> str:
129
+ """Encode a caller-supplied value as exactly one URL path segment.
130
+
131
+ Reserved characters (``/``, ``?``, ``#``, ``%``, space, non-ASCII) are
132
+ percent-encoded. Empty, ``.`` and ``..`` are rejected before any request
133
+ is sent: URL parsing would otherwise collapse or climb the route.
134
+ """
135
+ segment = str(value)
136
+ if segment == "":
137
+ raise ValueError("path parameter must not be empty")
138
+ if segment in (".", ".."):
139
+ raise ValueError("path parameter must not be '.' or '..'")
140
+ return quote(segment, safe="")
141
+
142
+
124
143
  # ── Deserialisation ───────────────────────────────────────────────────────────
125
144
 
126
145
  # Cache for resolved type hints per class
@@ -0,0 +1 @@
1
+ __version__ = "0.5.0"