intentseal 0.1.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.
- intentseal-0.1.0/LICENSE +202 -0
- intentseal-0.1.0/NOTICE +4 -0
- intentseal-0.1.0/PKG-INFO +125 -0
- intentseal-0.1.0/README.md +94 -0
- intentseal-0.1.0/intentseal/__init__.py +7 -0
- intentseal-0.1.0/intentseal/guard.py +255 -0
- intentseal-0.1.0/intentseal/langchain.py +52 -0
- intentseal-0.1.0/intentseal/remote.py +73 -0
- intentseal-0.1.0/intentseal.egg-info/PKG-INFO +125 -0
- intentseal-0.1.0/intentseal.egg-info/SOURCES.txt +13 -0
- intentseal-0.1.0/intentseal.egg-info/dependency_links.txt +1 -0
- intentseal-0.1.0/intentseal.egg-info/requires.txt +8 -0
- intentseal-0.1.0/intentseal.egg-info/top_level.txt +1 -0
- intentseal-0.1.0/pyproject.toml +41 -0
- intentseal-0.1.0/setup.cfg +4 -0
intentseal-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
|
202
|
+
|
intentseal-0.1.0/NOTICE
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: intentseal
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: IntentSeal: a prompt-injection firewall for AI agents. Inspect what your agent reads, check what it does.
|
|
5
|
+
Author-email: Sankalp Wanjari <sankalpwanjari85@gmail.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/sankalp2515/intentseal
|
|
8
|
+
Project-URL: Source, https://github.com/sankalp2515/intentseal
|
|
9
|
+
Project-URL: Documentation, https://github.com/sankalp2515/intentseal/blob/main/docs/INTEGRATION.md
|
|
10
|
+
Project-URL: Issues, https://github.com/sankalp2515/intentseal/issues
|
|
11
|
+
Keywords: prompt injection,llm security,ai agents,guardrails,firewall,rag,langchain
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Security
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
License-File: NOTICE
|
|
24
|
+
Requires-Dist: intentseal-core==0.1.0
|
|
25
|
+
Requires-Dist: httpx>=0.27
|
|
26
|
+
Provides-Extra: embedded
|
|
27
|
+
Requires-Dist: intentseal-core[classifiers,engine,ocr]==0.1.0; extra == "embedded"
|
|
28
|
+
Provides-Extra: langchain
|
|
29
|
+
Requires-Dist: langchain-core>=0.3; extra == "langchain"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# IntentSeal
|
|
33
|
+
|
|
34
|
+
**A prompt-injection firewall for AI agents.** IntentSeal inspects everything your agent reads (user messages, web
|
|
35
|
+
pages, PDFs, Word files, emails, Markdown, HTML, API responses, OCR text, source code, images, retrieved RAG passages)
|
|
36
|
+
before the model sees it, and checks every tool call before it runs. It keeps the agent inside the user's intent.
|
|
37
|
+
|
|
38
|
+
- **Detects and neutralises** instruction override, role change, secret extraction, tool abuse, credential theft,
|
|
39
|
+
context poisoning, multi-step jailbreaks, encoded instructions and indirect prompt injection. Every decision is
|
|
40
|
+
labelled with the attack type.
|
|
41
|
+
- **Sees what the model sees**: hidden text in PDFs and Word files, CSS-hidden HTML, HTML-only email parts, look-alike
|
|
42
|
+
letters and encoded payloads (base64, hex, %-encoding, Unicode tags) are decoded and checked.
|
|
43
|
+
- **Rules decide, AI advises**: fast structural rules and a local classifier first, an AI judge only when needed, and
|
|
44
|
+
deterministic action rules (allow-lists, pinned fields, outbound secret scan, "did the user ask for this?") that a
|
|
45
|
+
model can never override.
|
|
46
|
+
- **Any model provider**: the SDK checks content and tools, not the model call, so it works with OpenAI, Anthropic,
|
|
47
|
+
Gemini, Groq, Mistral, local models or anything else.
|
|
48
|
+
|
|
49
|
+
> Status: alpha (0.1). APIs may change between minor versions.
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install intentseal # talks to an IntentSeal gateway (remote mode): small install
|
|
55
|
+
pip install "intentseal[embedded]" # runs the engine in your process (PDF/Word/HTML extraction, classifier, OCR)
|
|
56
|
+
pip install "intentseal[langchain]" # LangChain / LangGraph tool helper
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Python 3.11+.
|
|
60
|
+
|
|
61
|
+
## Quickstart (embedded)
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
intentseal init --agent my-agent --template research-assistant # writes ~/.intentseal/policies/my-agent.yaml
|
|
65
|
+
intentseal init --list # other templates (RAG, support, coding agent)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Add one provider key for the AI judge to `~/.intentseal/.env` or your project's `.env` (`GROQ_API_KEY`,
|
|
69
|
+
`GEMINI_API_KEY`, `NVIDIA_API_KEY` or `OPENROUTER_API_KEY`). Then:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from intentseal import Guard
|
|
73
|
+
|
|
74
|
+
guard = Guard("my-agent")
|
|
75
|
+
|
|
76
|
+
with guard.session(user_id="u-42", task=user_message):
|
|
77
|
+
page = guard.inspect(html, source="web") # cleaned content, or a short safe notice
|
|
78
|
+
doc = guard.inspect(pdf_bytes, source="pdf", filename="q3.pdf") # raw bytes: hidden text is seen too
|
|
79
|
+
|
|
80
|
+
@guard.tool() # arguments checked before it runs
|
|
81
|
+
def send_message(to: str, subject: str, body: str): ...
|
|
82
|
+
|
|
83
|
+
send_message(to="someone@elsewhere.example", subject="...", body="...") # blocked: returns a notice
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The policy (`~/.intentseal/policies/my-agent.yaml`) lists your agent's tools with their risk, allow-lists (recipients,
|
|
87
|
+
domains, paths), pinned fields and the words that count as the user asking for an action. Any tool not listed is
|
|
88
|
+
blocked in enforce mode; `mode: monitor` records everything and blocks nothing.
|
|
89
|
+
|
|
90
|
+
## Remote mode (a shared gateway, Console, audit trail)
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
guard = Guard("my-agent", remote="https://intentseal.example.com", api_key=os.environ["INTENTSEAL_KEY"])
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Same API; decisions come from the gateway, which also offers an OpenAI- and Anthropic-compatible **LLM proxy** (protect
|
|
97
|
+
an agent by changing its base URL only), a Security Console (decisions, approvals, session replay, policy) and SIEM
|
|
98
|
+
export.
|
|
99
|
+
|
|
100
|
+
## RAG
|
|
101
|
+
|
|
102
|
+
Check files when you index them and passages when you retrieve them:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
r = guard.inspect_result(open(path, "rb").read(), source="rag", filename=path)
|
|
106
|
+
if r.enforced.value in ("BLOCK", "ESCALATE"):
|
|
107
|
+
quarantine(path) # never enters the index
|
|
108
|
+
else:
|
|
109
|
+
index(r.cleaned_content)
|
|
110
|
+
|
|
111
|
+
with guard.session(user_id=user.id, task=question):
|
|
112
|
+
passages = [guard.inspect(c.text, source="rag") for c in retriever.search(question)]
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## LangChain / LangGraph
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
tools = guard.protect_tools([search_tool, email_tool]) # inputs checked, outputs inspected
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Links
|
|
122
|
+
|
|
123
|
+
Source, documentation, evaluation results and the gateway: https://github.com/sankalp2515/intentseal
|
|
124
|
+
|
|
125
|
+
Licensed under the Apache License 2.0.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# IntentSeal
|
|
2
|
+
|
|
3
|
+
**A prompt-injection firewall for AI agents.** IntentSeal inspects everything your agent reads (user messages, web
|
|
4
|
+
pages, PDFs, Word files, emails, Markdown, HTML, API responses, OCR text, source code, images, retrieved RAG passages)
|
|
5
|
+
before the model sees it, and checks every tool call before it runs. It keeps the agent inside the user's intent.
|
|
6
|
+
|
|
7
|
+
- **Detects and neutralises** instruction override, role change, secret extraction, tool abuse, credential theft,
|
|
8
|
+
context poisoning, multi-step jailbreaks, encoded instructions and indirect prompt injection. Every decision is
|
|
9
|
+
labelled with the attack type.
|
|
10
|
+
- **Sees what the model sees**: hidden text in PDFs and Word files, CSS-hidden HTML, HTML-only email parts, look-alike
|
|
11
|
+
letters and encoded payloads (base64, hex, %-encoding, Unicode tags) are decoded and checked.
|
|
12
|
+
- **Rules decide, AI advises**: fast structural rules and a local classifier first, an AI judge only when needed, and
|
|
13
|
+
deterministic action rules (allow-lists, pinned fields, outbound secret scan, "did the user ask for this?") that a
|
|
14
|
+
model can never override.
|
|
15
|
+
- **Any model provider**: the SDK checks content and tools, not the model call, so it works with OpenAI, Anthropic,
|
|
16
|
+
Gemini, Groq, Mistral, local models or anything else.
|
|
17
|
+
|
|
18
|
+
> Status: alpha (0.1). APIs may change between minor versions.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install intentseal # talks to an IntentSeal gateway (remote mode): small install
|
|
24
|
+
pip install "intentseal[embedded]" # runs the engine in your process (PDF/Word/HTML extraction, classifier, OCR)
|
|
25
|
+
pip install "intentseal[langchain]" # LangChain / LangGraph tool helper
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Python 3.11+.
|
|
29
|
+
|
|
30
|
+
## Quickstart (embedded)
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
intentseal init --agent my-agent --template research-assistant # writes ~/.intentseal/policies/my-agent.yaml
|
|
34
|
+
intentseal init --list # other templates (RAG, support, coding agent)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Add one provider key for the AI judge to `~/.intentseal/.env` or your project's `.env` (`GROQ_API_KEY`,
|
|
38
|
+
`GEMINI_API_KEY`, `NVIDIA_API_KEY` or `OPENROUTER_API_KEY`). Then:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from intentseal import Guard
|
|
42
|
+
|
|
43
|
+
guard = Guard("my-agent")
|
|
44
|
+
|
|
45
|
+
with guard.session(user_id="u-42", task=user_message):
|
|
46
|
+
page = guard.inspect(html, source="web") # cleaned content, or a short safe notice
|
|
47
|
+
doc = guard.inspect(pdf_bytes, source="pdf", filename="q3.pdf") # raw bytes: hidden text is seen too
|
|
48
|
+
|
|
49
|
+
@guard.tool() # arguments checked before it runs
|
|
50
|
+
def send_message(to: str, subject: str, body: str): ...
|
|
51
|
+
|
|
52
|
+
send_message(to="someone@elsewhere.example", subject="...", body="...") # blocked: returns a notice
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The policy (`~/.intentseal/policies/my-agent.yaml`) lists your agent's tools with their risk, allow-lists (recipients,
|
|
56
|
+
domains, paths), pinned fields and the words that count as the user asking for an action. Any tool not listed is
|
|
57
|
+
blocked in enforce mode; `mode: monitor` records everything and blocks nothing.
|
|
58
|
+
|
|
59
|
+
## Remote mode (a shared gateway, Console, audit trail)
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
guard = Guard("my-agent", remote="https://intentseal.example.com", api_key=os.environ["INTENTSEAL_KEY"])
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Same API; decisions come from the gateway, which also offers an OpenAI- and Anthropic-compatible **LLM proxy** (protect
|
|
66
|
+
an agent by changing its base URL only), a Security Console (decisions, approvals, session replay, policy) and SIEM
|
|
67
|
+
export.
|
|
68
|
+
|
|
69
|
+
## RAG
|
|
70
|
+
|
|
71
|
+
Check files when you index them and passages when you retrieve them:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
r = guard.inspect_result(open(path, "rb").read(), source="rag", filename=path)
|
|
75
|
+
if r.enforced.value in ("BLOCK", "ESCALATE"):
|
|
76
|
+
quarantine(path) # never enters the index
|
|
77
|
+
else:
|
|
78
|
+
index(r.cleaned_content)
|
|
79
|
+
|
|
80
|
+
with guard.session(user_id=user.id, task=question):
|
|
81
|
+
passages = [guard.inspect(c.text, source="rag") for c in retriever.search(question)]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## LangChain / LangGraph
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
tools = guard.protect_tools([search_tool, email_tool]) # inputs checked, outputs inspected
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Links
|
|
91
|
+
|
|
92
|
+
Source, documentation, evaluation results and the gateway: https://github.com/sankalp2515/intentseal
|
|
93
|
+
|
|
94
|
+
Licensed under the Apache License 2.0.
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Python SDK (SDK-1…SDK-5): protect an existing agent with a few lines.
|
|
2
|
+
|
|
3
|
+
guard = Guard("research-agent") # SDK-1 embedded mode (engine in-process)
|
|
4
|
+
guard = Guard("research-agent", remote="https://gw", api_key=...) # SDK-6 remote mode (Gateway API)
|
|
5
|
+
tools = guard.protect_tools(langchain_tools) # SDK-7 LangChain / LangGraph tools
|
|
6
|
+
with guard.session(user_id="u1", task="Summarise my inbox", trusted={"customer_id": "C-1042"}): # SDK-4
|
|
7
|
+
text = guard.inspect(raw_bytes, source="pdf", filename="report.pdf") # SDK-2
|
|
8
|
+
send_email(to=..., body=...) # decorated with @guard.tool() # SDK-3
|
|
9
|
+
|
|
10
|
+
Failure policy (SDK-5): if the engine itself fails, tools marked `failure: closed` in the policy (write/send)
|
|
11
|
+
are blocked, and `failure: open` tools (read-only) proceed; both are logged.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import contextvars
|
|
18
|
+
import functools
|
|
19
|
+
import inspect
|
|
20
|
+
import logging
|
|
21
|
+
from contextlib import contextmanager
|
|
22
|
+
from typing import TYPE_CHECKING, Any, Callable, Iterator
|
|
23
|
+
|
|
24
|
+
from intentseal_core.policy import AgentPolicy, load_policy, policy_exists, policy_path
|
|
25
|
+
from intentseal_core.session import Session
|
|
26
|
+
from intentseal_core.types import ActionResult, Decision, InspectResult
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from intentseal_core.engine import Engine
|
|
30
|
+
|
|
31
|
+
log = logging.getLogger("intentseal.sdk")
|
|
32
|
+
_current: contextvars.ContextVar[Session | None] = contextvars.ContextVar("intentseal_session", default=None)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ToolBlocked(Exception):
|
|
36
|
+
"""Raised by a guarded tool when `on_block="raise"`."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, result: ActionResult) -> None:
|
|
39
|
+
super().__init__(result.notice or result.reason)
|
|
40
|
+
self.result = result
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Guard:
|
|
44
|
+
def __init__(self, agent_id: str, engine: Engine | None = None, mode: str | None = None,
|
|
45
|
+
policy: AgentPolicy | None = None, secrets: list[str] | None = None,
|
|
46
|
+
engine_options: dict[str, Any] | None = None, warm_up: bool = True,
|
|
47
|
+
remote: str | None = None, api_key: str | None = None, http_client: Any = None) -> None:
|
|
48
|
+
self.agent_id = agent_id
|
|
49
|
+
if remote or http_client is not None: # SDK-6: decisions come from the Gateway API
|
|
50
|
+
from .remote import RemoteEngine
|
|
51
|
+
engine = RemoteEngine(remote or "http://gateway", api_key or "", client=http_client)
|
|
52
|
+
policy = policy or engine.policy(agent_id) # failure policy (SDK-5) uses the gateway's tool settings
|
|
53
|
+
warm_up = False
|
|
54
|
+
if engine is None:
|
|
55
|
+
# Embedded mode runs the engine in-process; remote mode (above) needs only the light install.
|
|
56
|
+
try:
|
|
57
|
+
from intentseal_core.engine import Engine
|
|
58
|
+
from intentseal_core.llm.router import Router
|
|
59
|
+
except ImportError as e:
|
|
60
|
+
raise ImportError(
|
|
61
|
+
f"Embedded mode needs the engine: pip install 'intentseal[embedded]' (missing: {e.name}). "
|
|
62
|
+
"Or use a gateway: Guard(agent_id, remote='https://<gateway>', api_key=...).") from e
|
|
63
|
+
if policy is None and remote is None and http_client is None and not policy_exists(agent_id):
|
|
64
|
+
log.warning("No policy file for agent '%s' (%s): the default policy runs in monitor mode, so nothing is "
|
|
65
|
+
"enforced. Create one with `intentseal init --agent %s`, or pass mode='enforce'.",
|
|
66
|
+
agent_id, policy_path(agent_id), agent_id)
|
|
67
|
+
self._policy = policy or load_policy(agent_id)
|
|
68
|
+
if mode:
|
|
69
|
+
self._policy.mode = mode
|
|
70
|
+
if engine is None:
|
|
71
|
+
router = Router() # shares the persistent store (settings.db_path) for usage, cache and decisions
|
|
72
|
+
opts = {"tier0_timeout_s": 10.0, **(engine_options or {})} # extraction incl. OCR on CPU
|
|
73
|
+
engine = Engine(policy_loader=self._load, router=router, store=router.store, **opts)
|
|
74
|
+
self.engine = engine
|
|
75
|
+
if secrets:
|
|
76
|
+
self.engine.register_secrets(secrets)
|
|
77
|
+
if warm_up:
|
|
78
|
+
_warm_up()
|
|
79
|
+
|
|
80
|
+
def _load(self, agent_id: str) -> AgentPolicy:
|
|
81
|
+
return self._policy if agent_id == self.agent_id else load_policy(agent_id)
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def policy(self) -> AgentPolicy:
|
|
85
|
+
return self._policy
|
|
86
|
+
|
|
87
|
+
# ---- sessions (SDK-4) --------------------------------------------------------------------
|
|
88
|
+
@contextmanager
|
|
89
|
+
def session(self, user_id: str | None = None, task: str = "", trusted: dict[str, Any] | None = None) -> Iterator[Session]:
|
|
90
|
+
s = self.engine.start_session(self.agent_id, user_ref=user_id, task=task, trusted=trusted)
|
|
91
|
+
token = _current.set(s)
|
|
92
|
+
try:
|
|
93
|
+
yield s
|
|
94
|
+
finally:
|
|
95
|
+
_current.reset(token)
|
|
96
|
+
|
|
97
|
+
def start_session(self, user_id: str | None = None, task: str = "", trusted: dict[str, Any] | None = None) -> Session:
|
|
98
|
+
s = self.engine.start_session(self.agent_id, user_ref=user_id, task=task, trusted=trusted)
|
|
99
|
+
_current.set(s)
|
|
100
|
+
return s
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def current_session(self) -> Session | None:
|
|
104
|
+
return _current.get()
|
|
105
|
+
|
|
106
|
+
def _sid(self, session_id: str | None) -> str | None:
|
|
107
|
+
if session_id:
|
|
108
|
+
return session_id
|
|
109
|
+
s = _current.get()
|
|
110
|
+
return s.session_id if s else None
|
|
111
|
+
|
|
112
|
+
def register_secrets(self, values: list[str]) -> None:
|
|
113
|
+
self.engine.register_secrets(values)
|
|
114
|
+
|
|
115
|
+
# ---- inbound content (SDK-2) -------------------------------------------------------------
|
|
116
|
+
async def ainspect_result(self, content: bytes | str, source: str, filename: str | None = None,
|
|
117
|
+
session_id: str | None = None) -> InspectResult:
|
|
118
|
+
return await self.engine.inspect(content, source, self.agent_id, session_id=self._sid(session_id),
|
|
119
|
+
filename=filename)
|
|
120
|
+
|
|
121
|
+
async def ainspect(self, content: bytes | str, source: str, filename: str | None = None,
|
|
122
|
+
session_id: str | None = None) -> str:
|
|
123
|
+
"""Return what the agent should see: cleaned content, or a safe notice when blocked."""
|
|
124
|
+
try:
|
|
125
|
+
r = await self.ainspect_result(content, source, filename, session_id)
|
|
126
|
+
except Exception as e: # inspection unavailable (e.g. remote gateway down): never pass content unseen
|
|
127
|
+
log.warning("inspection failed for %s (%s); content withheld", source, type(e).__name__)
|
|
128
|
+
return f"[Content from {source} was withheld: the security check is unavailable]"
|
|
129
|
+
if r.enforced == Decision.ALLOW and isinstance(content, str):
|
|
130
|
+
return content # allowed text passes unchanged (G3b); files return their extracted view
|
|
131
|
+
if r.enforced == Decision.BLOCK:
|
|
132
|
+
return f"[Content from {source} was withheld by the security policy: {r.reason}]"
|
|
133
|
+
if r.enforced == Decision.ESCALATE:
|
|
134
|
+
return f"[Content from {source} is held for human review: {r.reason}]"
|
|
135
|
+
return r.cleaned_content
|
|
136
|
+
|
|
137
|
+
def inspect(self, content: bytes | str, source: str, filename: str | None = None, session_id: str | None = None) -> str:
|
|
138
|
+
return _run(self.ainspect(content, source, filename, session_id))
|
|
139
|
+
|
|
140
|
+
def inspect_result(self, content: bytes | str, source: str, filename: str | None = None,
|
|
141
|
+
session_id: str | None = None) -> InspectResult:
|
|
142
|
+
return _run(self.ainspect_result(content, source, filename, session_id))
|
|
143
|
+
|
|
144
|
+
# ---- the model's answer -------------------------------------------------------------------
|
|
145
|
+
async def acheck_output(self, text: str, session_id: str | None = None) -> str:
|
|
146
|
+
"""What the user may see of the model's final answer: protected values (canaries, registered secrets, known
|
|
147
|
+
secret formats, also encoded) are redacted, or the answer is withheld when they cannot be."""
|
|
148
|
+
try:
|
|
149
|
+
r = await self.engine.check_output(text, self.agent_id, session_id=self._sid(session_id))
|
|
150
|
+
except Exception as e: # the check is unavailable: the answer is not known to be safe
|
|
151
|
+
log.warning("answer check failed (%s); answer withheld", type(e).__name__)
|
|
152
|
+
return "[The answer was withheld: the security check is unavailable]"
|
|
153
|
+
return r.cleaned_content
|
|
154
|
+
|
|
155
|
+
def check_output(self, text: str, session_id: str | None = None) -> str:
|
|
156
|
+
return _run(self.acheck_output(text, session_id))
|
|
157
|
+
|
|
158
|
+
# ---- actions (SDK-3, SDK-5) --------------------------------------------------------------
|
|
159
|
+
async def acheck(self, tool: str, args: dict[str, Any], session_id: str | None = None) -> ActionResult:
|
|
160
|
+
try:
|
|
161
|
+
return await self.engine.check_action(tool, args, self.agent_id, session_id=self._sid(session_id))
|
|
162
|
+
except Exception as e: # engine failure: apply the tool's failure policy
|
|
163
|
+
tp = self._policy.tools.get(tool)
|
|
164
|
+
closed = (tp.failure if tp else "closed") == "closed"
|
|
165
|
+
log.warning("guard check failed for %s (%s); failing %s", tool, type(e).__name__, "closed" if closed else "open")
|
|
166
|
+
d = Decision.BLOCK if closed else Decision.ALLOW
|
|
167
|
+
return ActionResult(decision=d, enforced=d, final_args=None if closed else dict(args), degraded=True,
|
|
168
|
+
policy_rule=f"guard-failure-fail-{'closed' if closed else 'open'}",
|
|
169
|
+
reason=f"guard unavailable ({type(e).__name__})",
|
|
170
|
+
notice=f"Action '{tool}' was not performed: the security check is unavailable." if closed else None)
|
|
171
|
+
|
|
172
|
+
def check(self, tool: str, args: dict[str, Any], session_id: str | None = None) -> ActionResult:
|
|
173
|
+
return _run(self.acheck(tool, args, session_id))
|
|
174
|
+
|
|
175
|
+
def tool(self, name: str | None = None, pinned: dict[str, Any] | None = None,
|
|
176
|
+
on_block: str = "notice") -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
177
|
+
"""Decorate a tool function. Arguments are checked (and pinned fields rewritten) before it runs.
|
|
178
|
+
|
|
179
|
+
pinned: extra trusted values for this tool's arguments, merged into the session's trusted context.
|
|
180
|
+
on_block: "notice" returns the guard's notice string; "raise" raises ToolBlocked.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
def deco(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
184
|
+
tool_name = name or fn.__name__
|
|
185
|
+
sig = inspect.signature(fn)
|
|
186
|
+
|
|
187
|
+
def bind(args: tuple, kwargs: dict) -> dict[str, Any]:
|
|
188
|
+
b = sig.bind_partial(*args, **kwargs)
|
|
189
|
+
return dict(b.arguments)
|
|
190
|
+
|
|
191
|
+
def apply_pins() -> None:
|
|
192
|
+
s = _current.get()
|
|
193
|
+
if s is not None and pinned:
|
|
194
|
+
s.trusted.update(pinned)
|
|
195
|
+
s.note_trusted(" ".join(str(v) for v in pinned.values()))
|
|
196
|
+
|
|
197
|
+
def blocked(r: ActionResult) -> Any:
|
|
198
|
+
if on_block == "raise":
|
|
199
|
+
raise ToolBlocked(r)
|
|
200
|
+
return r.notice or f"Action '{tool_name}' was blocked by the security policy."
|
|
201
|
+
|
|
202
|
+
if inspect.iscoroutinefunction(fn):
|
|
203
|
+
@functools.wraps(fn)
|
|
204
|
+
async def awrapper(*args: Any, **kwargs: Any) -> Any:
|
|
205
|
+
apply_pins()
|
|
206
|
+
r = await self.acheck(tool_name, bind(args, kwargs))
|
|
207
|
+
if r.enforced != Decision.ALLOW:
|
|
208
|
+
return blocked(r)
|
|
209
|
+
return await fn(**(r.final_args or {}))
|
|
210
|
+
return awrapper
|
|
211
|
+
|
|
212
|
+
@functools.wraps(fn)
|
|
213
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
214
|
+
apply_pins()
|
|
215
|
+
r = self.check(tool_name, bind(args, kwargs))
|
|
216
|
+
if r.enforced != Decision.ALLOW:
|
|
217
|
+
return blocked(r)
|
|
218
|
+
return fn(**(r.final_args or {}))
|
|
219
|
+
return wrapper
|
|
220
|
+
|
|
221
|
+
return deco
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def protect_tools(self, tools: Any, inspect_outputs: bool = True) -> list[Any]:
|
|
225
|
+
"""SDK-7: wrap LangChain / LangGraph tools (see intentseal.langchain)."""
|
|
226
|
+
from .langchain import protect_tools
|
|
227
|
+
return protect_tools(self, tools, inspect_outputs)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _warm_up() -> None:
|
|
231
|
+
"""Load the local classifier and OCR once so the first real inspection is not slowed down."""
|
|
232
|
+
try:
|
|
233
|
+
import io
|
|
234
|
+
|
|
235
|
+
from PIL import Image, ImageDraw
|
|
236
|
+
|
|
237
|
+
from intentseal_core import extract as _extract
|
|
238
|
+
from intentseal_core.classifier import local_classifier
|
|
239
|
+
|
|
240
|
+
local_classifier()
|
|
241
|
+
img = Image.new("RGB", (160, 40), "white")
|
|
242
|
+
ImageDraw.Draw(img).text((5, 10), "warm up", fill="black")
|
|
243
|
+
buf = io.BytesIO()
|
|
244
|
+
img.save(buf, format="PNG")
|
|
245
|
+
_extract.extract(buf.getvalue(), fmt="image")
|
|
246
|
+
except Exception as e: # warm-up is best effort
|
|
247
|
+
log.info("warm-up skipped (%s)", type(e).__name__)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _run(coro: Any) -> Any:
|
|
251
|
+
try:
|
|
252
|
+
asyncio.get_running_loop()
|
|
253
|
+
except RuntimeError:
|
|
254
|
+
return asyncio.run(coro)
|
|
255
|
+
raise RuntimeError("Guard sync methods cannot run inside an event loop; use the async variants (ainspect/acheck).")
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""LangChain / LangGraph helper (SDK-7): protect an existing tool list in one line.
|
|
2
|
+
|
|
3
|
+
from intentseal import Guard
|
|
4
|
+
guard = Guard("research-agent")
|
|
5
|
+
tools = guard.protect_tools(tools) # or: protect_tools(guard, tools)
|
|
6
|
+
with guard.session(user_id="u1", task=task):
|
|
7
|
+
agent.invoke(...)
|
|
8
|
+
|
|
9
|
+
Every call is checked by the action guard before the tool runs (pinned fields rewritten, blocked calls return the
|
|
10
|
+
guard's notice to the model instead of running), and every tool result is inspected as untrusted content before
|
|
11
|
+
the model sees it. The wrapped tools keep their name, description and argument schema, so prompts and
|
|
12
|
+
tool-calling behaviour do not change.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import TYPE_CHECKING, Any, Iterable
|
|
18
|
+
|
|
19
|
+
from intentseal_core.types import Decision
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from langchain_core.tools import BaseTool
|
|
23
|
+
|
|
24
|
+
from .guard import Guard
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def protect_tools(guard: "Guard", tools: Iterable["BaseTool"], inspect_outputs: bool = True) -> list["BaseTool"]:
|
|
28
|
+
from langchain_core.tools import StructuredTool
|
|
29
|
+
|
|
30
|
+
def wrap(t: "BaseTool") -> "BaseTool":
|
|
31
|
+
async def acall(**kwargs: Any) -> Any:
|
|
32
|
+
r = await guard.acheck(t.name, kwargs)
|
|
33
|
+
if r.enforced != Decision.ALLOW:
|
|
34
|
+
return r.notice or f"Action '{t.name}' was blocked by the security policy."
|
|
35
|
+
out = await t.ainvoke(r.final_args if r.final_args is not None else kwargs)
|
|
36
|
+
if inspect_outputs and isinstance(out, str):
|
|
37
|
+
out = await guard.ainspect(out, source=f"tool:{t.name}")
|
|
38
|
+
return out
|
|
39
|
+
|
|
40
|
+
def call(**kwargs: Any) -> Any:
|
|
41
|
+
r = guard.check(t.name, kwargs)
|
|
42
|
+
if r.enforced != Decision.ALLOW:
|
|
43
|
+
return r.notice or f"Action '{t.name}' was blocked by the security policy."
|
|
44
|
+
out = t.invoke(r.final_args if r.final_args is not None else kwargs)
|
|
45
|
+
if inspect_outputs and isinstance(out, str):
|
|
46
|
+
out = guard.inspect(out, source=f"tool:{t.name}")
|
|
47
|
+
return out
|
|
48
|
+
|
|
49
|
+
return StructuredTool(name=t.name, description=t.description, args_schema=t.args_schema,
|
|
50
|
+
func=call, coroutine=acall, return_direct=getattr(t, "return_direct", False))
|
|
51
|
+
|
|
52
|
+
return [wrap(t) for t in tools]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Remote mode (SDK-6): the Guard talks to a Gateway API instead of running the engine in-process.
|
|
2
|
+
|
|
3
|
+
guard = Guard("research-agent", remote="https://gateway.example", api_key=os.environ["INTENTSEAL_API_KEY"])
|
|
4
|
+
|
|
5
|
+
`RemoteEngine` implements the part of the Engine interface the Guard uses (start_session, inspect, check_action,
|
|
6
|
+
register_secrets), so every SDK feature works the same way; decisions are made by the gateway's engine.
|
|
7
|
+
Differences from embedded mode:
|
|
8
|
+
- the session's trusted context (pinned fields) is fixed when the session is created;
|
|
9
|
+
- canaries/secrets are configured on the gateway (INTENTSEAL_CANARIES), not registered from the client;
|
|
10
|
+
- if the gateway is unreachable, content is withheld (fail closed) and tools follow their failure policy (SDK-5).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import base64
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Any, Iterable
|
|
19
|
+
|
|
20
|
+
import httpx
|
|
21
|
+
|
|
22
|
+
from intentseal_core.policy import AgentPolicy
|
|
23
|
+
from intentseal_core.session import Session
|
|
24
|
+
from intentseal_core.types import ActionResult, InspectResult
|
|
25
|
+
|
|
26
|
+
log = logging.getLogger("intentseal.sdk.remote")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RemoteEngine:
|
|
30
|
+
def __init__(self, base_url: str, api_key: str, timeout_s: float = 30.0, client: httpx.Client | None = None) -> None:
|
|
31
|
+
self.http = client or httpx.Client(base_url=base_url.rstrip("/"), timeout=timeout_s)
|
|
32
|
+
self.headers = {"Authorization": f"Bearer {api_key}"}
|
|
33
|
+
|
|
34
|
+
def _post(self, path: str, body: dict[str, Any]) -> dict[str, Any]:
|
|
35
|
+
r = self.http.post(path, json=body, headers=self.headers)
|
|
36
|
+
r.raise_for_status()
|
|
37
|
+
return r.json()
|
|
38
|
+
|
|
39
|
+
def policy(self, agent_id: str) -> AgentPolicy:
|
|
40
|
+
r = self.http.get(f"/v1/policies/{agent_id}", headers=self.headers)
|
|
41
|
+
r.raise_for_status()
|
|
42
|
+
return AgentPolicy.model_validate(r.json())
|
|
43
|
+
|
|
44
|
+
def register_secrets(self, values: Iterable[str]) -> None:
|
|
45
|
+
if list(values):
|
|
46
|
+
log.info("remote mode: secrets/canaries are configured on the gateway (INTENTSEAL_CANARIES), not sent from the client")
|
|
47
|
+
|
|
48
|
+
def start_session(self, agent_id: str, user_ref: str | None = None, task: str = "",
|
|
49
|
+
trusted: dict[str, Any] | None = None) -> Session:
|
|
50
|
+
sid = self._post("/v1/sessions", {"agent_id": agent_id, "user_ref": user_ref, "task": task,
|
|
51
|
+
"trusted": dict(trusted or {})})["session_id"]
|
|
52
|
+
return Session(agent_id=agent_id, session_id=sid, user_ref=user_ref, task=task, trusted=dict(trusted or {}))
|
|
53
|
+
|
|
54
|
+
async def inspect(self, content: str | bytes, source: str, agent_id: str, session_id: str | None = None,
|
|
55
|
+
filename: str | None = None, fmt: str | None = None, task: str | None = None) -> InspectResult:
|
|
56
|
+
body: dict[str, Any] = {"source": source, "agent_id": agent_id, "session_id": session_id, "filename": filename}
|
|
57
|
+
if isinstance(content, bytes):
|
|
58
|
+
body["content_b64"] = base64.b64encode(content).decode()
|
|
59
|
+
else:
|
|
60
|
+
body["content"] = content
|
|
61
|
+
return InspectResult.model_validate(await asyncio.to_thread(self._post, "/v1/inspect", body))
|
|
62
|
+
|
|
63
|
+
async def check_action(self, tool: str, args: dict[str, Any], agent_id: str,
|
|
64
|
+
session_id: str | None = None) -> ActionResult:
|
|
65
|
+
body = {"tool": tool, "args": args, "agent_id": agent_id, "session_id": session_id}
|
|
66
|
+
return ActionResult.model_validate(await asyncio.to_thread(self._post, "/v1/actions/check", body))
|
|
67
|
+
|
|
68
|
+
async def check_output(self, text: str, agent_id: str, session_id: str | None = None) -> InspectResult:
|
|
69
|
+
body = {"agent_id": agent_id, "session_id": session_id, "text": text}
|
|
70
|
+
return InspectResult.model_validate(await asyncio.to_thread(self._post, "/v1/outputs/check", body))
|
|
71
|
+
|
|
72
|
+
def close(self) -> None:
|
|
73
|
+
self.http.close()
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: intentseal
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: IntentSeal: a prompt-injection firewall for AI agents. Inspect what your agent reads, check what it does.
|
|
5
|
+
Author-email: Sankalp Wanjari <sankalpwanjari85@gmail.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/sankalp2515/intentseal
|
|
8
|
+
Project-URL: Source, https://github.com/sankalp2515/intentseal
|
|
9
|
+
Project-URL: Documentation, https://github.com/sankalp2515/intentseal/blob/main/docs/INTEGRATION.md
|
|
10
|
+
Project-URL: Issues, https://github.com/sankalp2515/intentseal/issues
|
|
11
|
+
Keywords: prompt injection,llm security,ai agents,guardrails,firewall,rag,langchain
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Security
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
License-File: NOTICE
|
|
24
|
+
Requires-Dist: intentseal-core==0.1.0
|
|
25
|
+
Requires-Dist: httpx>=0.27
|
|
26
|
+
Provides-Extra: embedded
|
|
27
|
+
Requires-Dist: intentseal-core[classifiers,engine,ocr]==0.1.0; extra == "embedded"
|
|
28
|
+
Provides-Extra: langchain
|
|
29
|
+
Requires-Dist: langchain-core>=0.3; extra == "langchain"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# IntentSeal
|
|
33
|
+
|
|
34
|
+
**A prompt-injection firewall for AI agents.** IntentSeal inspects everything your agent reads (user messages, web
|
|
35
|
+
pages, PDFs, Word files, emails, Markdown, HTML, API responses, OCR text, source code, images, retrieved RAG passages)
|
|
36
|
+
before the model sees it, and checks every tool call before it runs. It keeps the agent inside the user's intent.
|
|
37
|
+
|
|
38
|
+
- **Detects and neutralises** instruction override, role change, secret extraction, tool abuse, credential theft,
|
|
39
|
+
context poisoning, multi-step jailbreaks, encoded instructions and indirect prompt injection. Every decision is
|
|
40
|
+
labelled with the attack type.
|
|
41
|
+
- **Sees what the model sees**: hidden text in PDFs and Word files, CSS-hidden HTML, HTML-only email parts, look-alike
|
|
42
|
+
letters and encoded payloads (base64, hex, %-encoding, Unicode tags) are decoded and checked.
|
|
43
|
+
- **Rules decide, AI advises**: fast structural rules and a local classifier first, an AI judge only when needed, and
|
|
44
|
+
deterministic action rules (allow-lists, pinned fields, outbound secret scan, "did the user ask for this?") that a
|
|
45
|
+
model can never override.
|
|
46
|
+
- **Any model provider**: the SDK checks content and tools, not the model call, so it works with OpenAI, Anthropic,
|
|
47
|
+
Gemini, Groq, Mistral, local models or anything else.
|
|
48
|
+
|
|
49
|
+
> Status: alpha (0.1). APIs may change between minor versions.
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install intentseal # talks to an IntentSeal gateway (remote mode): small install
|
|
55
|
+
pip install "intentseal[embedded]" # runs the engine in your process (PDF/Word/HTML extraction, classifier, OCR)
|
|
56
|
+
pip install "intentseal[langchain]" # LangChain / LangGraph tool helper
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Python 3.11+.
|
|
60
|
+
|
|
61
|
+
## Quickstart (embedded)
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
intentseal init --agent my-agent --template research-assistant # writes ~/.intentseal/policies/my-agent.yaml
|
|
65
|
+
intentseal init --list # other templates (RAG, support, coding agent)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Add one provider key for the AI judge to `~/.intentseal/.env` or your project's `.env` (`GROQ_API_KEY`,
|
|
69
|
+
`GEMINI_API_KEY`, `NVIDIA_API_KEY` or `OPENROUTER_API_KEY`). Then:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from intentseal import Guard
|
|
73
|
+
|
|
74
|
+
guard = Guard("my-agent")
|
|
75
|
+
|
|
76
|
+
with guard.session(user_id="u-42", task=user_message):
|
|
77
|
+
page = guard.inspect(html, source="web") # cleaned content, or a short safe notice
|
|
78
|
+
doc = guard.inspect(pdf_bytes, source="pdf", filename="q3.pdf") # raw bytes: hidden text is seen too
|
|
79
|
+
|
|
80
|
+
@guard.tool() # arguments checked before it runs
|
|
81
|
+
def send_message(to: str, subject: str, body: str): ...
|
|
82
|
+
|
|
83
|
+
send_message(to="someone@elsewhere.example", subject="...", body="...") # blocked: returns a notice
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The policy (`~/.intentseal/policies/my-agent.yaml`) lists your agent's tools with their risk, allow-lists (recipients,
|
|
87
|
+
domains, paths), pinned fields and the words that count as the user asking for an action. Any tool not listed is
|
|
88
|
+
blocked in enforce mode; `mode: monitor` records everything and blocks nothing.
|
|
89
|
+
|
|
90
|
+
## Remote mode (a shared gateway, Console, audit trail)
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
guard = Guard("my-agent", remote="https://intentseal.example.com", api_key=os.environ["INTENTSEAL_KEY"])
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Same API; decisions come from the gateway, which also offers an OpenAI- and Anthropic-compatible **LLM proxy** (protect
|
|
97
|
+
an agent by changing its base URL only), a Security Console (decisions, approvals, session replay, policy) and SIEM
|
|
98
|
+
export.
|
|
99
|
+
|
|
100
|
+
## RAG
|
|
101
|
+
|
|
102
|
+
Check files when you index them and passages when you retrieve them:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
r = guard.inspect_result(open(path, "rb").read(), source="rag", filename=path)
|
|
106
|
+
if r.enforced.value in ("BLOCK", "ESCALATE"):
|
|
107
|
+
quarantine(path) # never enters the index
|
|
108
|
+
else:
|
|
109
|
+
index(r.cleaned_content)
|
|
110
|
+
|
|
111
|
+
with guard.session(user_id=user.id, task=question):
|
|
112
|
+
passages = [guard.inspect(c.text, source="rag") for c in retriever.search(question)]
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## LangChain / LangGraph
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
tools = guard.protect_tools([search_tool, email_tool]) # inputs checked, outputs inspected
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Links
|
|
122
|
+
|
|
123
|
+
Source, documentation, evaluation results and the gateway: https://github.com/sankalp2515/intentseal
|
|
124
|
+
|
|
125
|
+
Licensed under the Apache License 2.0.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
NOTICE
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
intentseal/__init__.py
|
|
6
|
+
intentseal/guard.py
|
|
7
|
+
intentseal/langchain.py
|
|
8
|
+
intentseal/remote.py
|
|
9
|
+
intentseal.egg-info/PKG-INFO
|
|
10
|
+
intentseal.egg-info/SOURCES.txt
|
|
11
|
+
intentseal.egg-info/dependency_links.txt
|
|
12
|
+
intentseal.egg-info/requires.txt
|
|
13
|
+
intentseal.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
intentseal
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "intentseal"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "IntentSeal: a prompt-injection firewall for AI agents. Inspect what your agent reads, check what it does."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
license-files = ["LICENSE", "NOTICE"]
|
|
13
|
+
authors = [{ name = "Sankalp Wanjari", email = "sankalpwanjari85@gmail.com" }]
|
|
14
|
+
keywords = ["prompt injection", "llm security", "ai agents", "guardrails", "firewall", "rag", "langchain"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Security",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
24
|
+
]
|
|
25
|
+
# Remote mode (a shared IntentSeal gateway) needs only this. The publish workflow keeps the two versions equal.
|
|
26
|
+
dependencies = ["intentseal-core==0.1.0", "httpx>=0.27"]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
embedded = ["intentseal-core[engine,classifiers,ocr]==0.1.0"] # run the engine in-process
|
|
30
|
+
langchain = ["langchain-core>=0.3"]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/sankalp2515/intentseal"
|
|
34
|
+
Source = "https://github.com/sankalp2515/intentseal"
|
|
35
|
+
Documentation = "https://github.com/sankalp2515/intentseal/blob/main/docs/INTEGRATION.md"
|
|
36
|
+
Issues = "https://github.com/sankalp2515/intentseal/issues"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["."]
|
|
40
|
+
include = ["intentseal*"]
|
|
41
|
+
exclude = ["intentseal_core*"]
|