shieldcortex 0.2.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.
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/.github/workflows/test.yml +1 -1
- shieldcortex-0.5.0/CHANGELOG.md +159 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/PKG-INFO +108 -11
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/README.md +107 -10
- shieldcortex-0.5.0/examples/langchain_callback.py +42 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/pyproject.toml +1 -1
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/src/shieldcortex/_http.py +19 -0
- shieldcortex-0.5.0/src/shieldcortex/_version.py +1 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/src/shieldcortex/async_client.py +27 -26
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/src/shieldcortex/client.py +27 -26
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/src/shieldcortex/integrations/crewai.py +6 -6
- shieldcortex-0.5.0/src/shieldcortex/integrations/langchain.py +345 -0
- shieldcortex-0.5.0/tests/integrations/__init__.py +0 -0
- shieldcortex-0.5.0/tests/integrations/test_langchain.py +713 -0
- shieldcortex-0.5.0/tests/test_packaging.py +17 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/tests/test_parity.py +2 -0
- shieldcortex-0.5.0/tests/test_path_params.py +167 -0
- shieldcortex-0.2.0/CHANGELOG.md +0 -57
- shieldcortex-0.2.0/examples/langchain_callback.py +0 -34
- shieldcortex-0.2.0/src/shieldcortex/_version.py +0 -1
- shieldcortex-0.2.0/src/shieldcortex/integrations/langchain.py +0 -141
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/.github/workflows/publish.yml +0 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/.gitignore +0 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/LICENSE +0 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/examples/audit_export.py +0 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/examples/basic_scan.py +0 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/examples/batch_scanning.py +0 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/examples/crewai_memory.py +0 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/src/shieldcortex/__init__.py +0 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/src/shieldcortex/_payloads.py +0 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/src/shieldcortex/errors.py +0 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/src/shieldcortex/integrations/__init__.py +0 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/src/shieldcortex/pagination.py +0 -0
- /shieldcortex-0.2.0/tests/__init__.py → /shieldcortex-0.5.0/src/shieldcortex/py.typed +0 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/src/shieldcortex/types.py +0 -0
- {shieldcortex-0.2.0/tests/integrations → shieldcortex-0.5.0/tests}/__init__.py +0 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/tests/conftest.py +0 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/tests/endpoint_manifest.py +0 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/tests/fixtures.py +0 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/tests/integrations/test_crewai.py +0 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/tests/test_audit_exports.py +0 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/tests/test_client.py +0 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/tests/test_errors.py +0 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/tests/test_incidents_recall.py +0 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/tests/test_license.py +0 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/tests/test_pagination.py +0 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/tests/test_skills_threats.py +0 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/tests/test_sync.py +0 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/tests/test_types.py +0 -0
- {shieldcortex-0.2.0 → shieldcortex-0.5.0}/tests/test_verify.py +0 -0
|
@@ -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
|
+
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
|
|
135
|
+
## Memory Guard (CrewAI and others)
|
|
123
136
|
|
|
124
|
-
|
|
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
|
-
|
|
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
|
|
146
|
-
from shieldcortex
|
|
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
|
-
|
|
149
|
-
handler = ShieldCortexCallbackHandler(client, raise_on_block=True)
|
|
169
|
+
model = ChatOpenAI()
|
|
150
170
|
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
96
|
+
## Memory Guard (CrewAI and others)
|
|
84
97
|
|
|
85
|
-
|
|
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
|
-
|
|
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
|
|
107
|
-
from shieldcortex
|
|
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
|
-
|
|
110
|
-
handler = ShieldCortexCallbackHandler(client, raise_on_block=True)
|
|
130
|
+
model = ChatOpenAI()
|
|
111
131
|
|
|
112
|
-
|
|
113
|
-
|
|
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())
|
|
@@ -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"
|