agent-policy-layer 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_policy_layer-0.1.0.dist-info/METADATA +591 -0
- agent_policy_layer-0.1.0.dist-info/RECORD +18 -0
- agent_policy_layer-0.1.0.dist-info/WHEEL +4 -0
- agent_policy_layer-0.1.0.dist-info/entry_points.txt +2 -0
- agent_policy_layer-0.1.0.dist-info/licenses/LICENSE +202 -0
- apl/__init__.py +163 -0
- apl/adapters/__init__.py +19 -0
- apl/adapters/langgraph.py +526 -0
- apl/cli.py +911 -0
- apl/declarative.py +525 -0
- apl/instrument.py +1190 -0
- apl/layer.py +777 -0
- apl/logging.py +381 -0
- apl/server.py +525 -0
- apl/templates.py +465 -0
- apl/transports/__init__.py +12 -0
- apl/transports/http.py +751 -0
- apl/types.py +480 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-policy-layer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agent Policy Layer - Portable, composable policies for AI agents
|
|
5
|
+
Project-URL: Homepage, https://github.com/nimonkaranurag/agent_policy_layer
|
|
6
|
+
Author: Anurag Ravi Nimonkar
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: agents,ai,guardrails,langgraph,llm,mcp,policies,safety
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Classifier: Topic :: Security
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: aiohttp>=3.8
|
|
22
|
+
Requires-Dist: click>=8.0
|
|
23
|
+
Requires-Dist: pyyaml>=6.0
|
|
24
|
+
Requires-Dist: rich>=13.0
|
|
25
|
+
Provides-Extra: all
|
|
26
|
+
Requires-Dist: apl[dev,langgraph]; extra == 'all'
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: black>=23.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.1; extra == 'dev'
|
|
33
|
+
Requires-Dist: types-pyyaml; extra == 'dev'
|
|
34
|
+
Provides-Extra: langgraph
|
|
35
|
+
Requires-Dist: langgraph>=0.0.1; extra == 'langgraph'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
<div align="center">
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
*APL restrains your agents - when you need him to!* 🚔
|
|
42
|
+
|
|
43
|
+
**Portable, composable policies for AI agents.**
|
|
44
|
+
|
|
45
|
+
[Installation](#-installation) •
|
|
46
|
+
[Quick Start](#-quick-start-2-minutes) •
|
|
47
|
+
[How It Works](#-how-it-works) •
|
|
48
|
+
[Examples](#-examples) •
|
|
49
|
+
[API Reference](#-api-reference)
|
|
50
|
+
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
### WITHOUT APL 😰
|
|
56
|
+

|
|
57
|
+

|
|
58
|
+
|
|
59
|
+
### WITH APL 🛡️
|
|
60
|
+

|
|
61
|
+

|
|
62
|
+

|
|
63
|
+
|
|
64
|
+
## The Problem
|
|
65
|
+
|
|
66
|
+
You've built an HR agent for your enterprise. It works great in happy paths - updates employee records, applies for time-offs - great! But then:
|
|
67
|
+
|
|
68
|
+
- 😱 It leaks a customer's SSN in a response
|
|
69
|
+
- 💸 It burns through your token budget in one conversation
|
|
70
|
+
- 🗑️ It deletes production data without asking
|
|
71
|
+
- 🚫 It goes off-topic into areas you didn't intend
|
|
72
|
+
|
|
73
|
+
You need **guardrails** that can enforce your enterprise's policies.
|
|
74
|
+
|
|
75
|
+
But existing solutions are:
|
|
76
|
+
|
|
77
|
+
| Problem | Why It Hurts |
|
|
78
|
+
|---------|--------------|
|
|
79
|
+
| **Framework-specific** | Locked into LangGraph? Can't use that CrewAI policy. |
|
|
80
|
+
| **Code-embedded** | Policies buried in your agent code/prompts. Hard to update. |
|
|
81
|
+
| **Boolean only** | Just allow/deny. Can't modify or escalate. |
|
|
82
|
+
| **No composition** | What happens when 3 policies disagree? |
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## The Solution: APL
|
|
87
|
+
|
|
88
|
+
APL is a **protocol** for agent policies — like MCP, but for constraints instead of capabilities.
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
92
|
+
│ Your Agent │
|
|
93
|
+
│ │
|
|
94
|
+
│ "Delete all files" │
|
|
95
|
+
│ │ │
|
|
96
|
+
│ ▼ │
|
|
97
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
98
|
+
│ │ APL Policy Layer │ │
|
|
99
|
+
│ │ │ │
|
|
100
|
+
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
|
|
101
|
+
│ │ │ PII │ │ Budget │ │ Confirm │ │ │
|
|
102
|
+
│ │ │ Filter │ │ Limiter │ │ Delete │ │ │
|
|
103
|
+
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
|
|
104
|
+
│ │ │ │ │ │ │
|
|
105
|
+
│ │ ▼ ▼ ▼ │ │
|
|
106
|
+
│ │ ALLOW ALLOW ESCALATE │ │
|
|
107
|
+
│ │ │ │ │
|
|
108
|
+
│ │ Final: ESCALATE ◄────┘ │ │
|
|
109
|
+
│ │ "Confirm delete?" │ │
|
|
110
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
111
|
+
│ │ │
|
|
112
|
+
│ ▼ │
|
|
113
|
+
│ 🛡️ Action blocked until user confirms
|
|
114
|
+
└─────────────────────────────────────────────────────────────┘
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Key features:**
|
|
118
|
+
|
|
119
|
+
| Feature | Description |
|
|
120
|
+
|---------|-------------|
|
|
121
|
+
| **🔌 Runtime-agnostic** | Works with OpenAI, Anthropic, LangGraph, LangChain, or custom agents |
|
|
122
|
+
| **🎯 Rich verdicts** | Not just allow/deny — also `modify`, `escalate`, `observe` |
|
|
123
|
+
| **📝 Declarative policies** | Write policies in YAML, no Python required |
|
|
124
|
+
| **🔥 Hot-swappable** | Update policies without redeploying your agent |
|
|
125
|
+
| **⚡ Auto-instrumentation** | One line to protect all your LLM calls |
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 📦 Installation
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
pip install apl
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
That's it. No Docker, no external services.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 🚀 Quick Start (2 minutes)
|
|
140
|
+
|
|
141
|
+
### Option A: Auto-Instrumentation (Easiest)
|
|
142
|
+
|
|
143
|
+
**One line protects all your OpenAI/Anthropic calls automatically:**
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
import apl
|
|
147
|
+
|
|
148
|
+
# This patches OpenAI, Anthropic, LiteLLM, and LangChain
|
|
149
|
+
apl.auto_instrument(
|
|
150
|
+
policy_servers=["stdio://./my_policy.py"]
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Now use your LLM normally — APL intercepts automatically
|
|
154
|
+
from openai import OpenAI
|
|
155
|
+
client = OpenAI()
|
|
156
|
+
|
|
157
|
+
response = client.chat.completions.create(
|
|
158
|
+
model="gpt-4",
|
|
159
|
+
messages=[{"role": "user", "content": "What's my SSN? It's 123-45-6789"}]
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# If your policy redacts PII, the response is already clean!
|
|
163
|
+
print(response.choices[0].message.content)
|
|
164
|
+
# → "Your SSN is [REDACTED]"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Option B: Create a Policy Server
|
|
168
|
+
|
|
169
|
+
**Step 1:** Create `my_policy.py`:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from apl import PolicyServer, Verdict
|
|
173
|
+
import re
|
|
174
|
+
|
|
175
|
+
server = PolicyServer("my-policies")
|
|
176
|
+
|
|
177
|
+
@server.policy(
|
|
178
|
+
name="redact-ssn",
|
|
179
|
+
events=["output.pre_send"],
|
|
180
|
+
)
|
|
181
|
+
async def redact_ssn(event):
|
|
182
|
+
text = event.payload.output_text or ""
|
|
183
|
+
|
|
184
|
+
if re.search(r'\d{3}-\d{2}-\d{4}', text):
|
|
185
|
+
redacted = re.sub(r'\d{3}-\d{2}-\d{4}', '[REDACTED]', text)
|
|
186
|
+
return Verdict.modify(
|
|
187
|
+
target="output",
|
|
188
|
+
operation="replace",
|
|
189
|
+
value=redacted,
|
|
190
|
+
reasoning="SSN detected and redacted"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
return Verdict.allow()
|
|
194
|
+
|
|
195
|
+
if __name__ == "__main__":
|
|
196
|
+
server.run()
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Step 2:** Run it:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
apl serve my_policy.py --http 8080
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Step 3:** Test it:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
curl -X POST http://localhost:8080/evaluate \
|
|
209
|
+
-H "Content-Type: application/json" \
|
|
210
|
+
-d '{
|
|
211
|
+
"type": "output.pre_send",
|
|
212
|
+
"payload": {"output_text": "Your SSN is 123-45-6789"}
|
|
213
|
+
}'
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
```json
|
|
217
|
+
{
|
|
218
|
+
"composed_verdict": {
|
|
219
|
+
"decision": "modify",
|
|
220
|
+
"modification": {
|
|
221
|
+
"target": "output",
|
|
222
|
+
"value": "Your SSN is [REDACTED]"
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## 🔄 How It Works
|
|
231
|
+
|
|
232
|
+
### The Data Flow
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
236
|
+
│ │
|
|
237
|
+
│ 1. USER INPUT 2. AGENT PROCESSES 3. AGENT RESPONDS │
|
|
238
|
+
│ ───────────── ───────────────── ───────────────── │
|
|
239
|
+
│ │
|
|
240
|
+
│ "What's my SSN?" ──► Agent calls LLM ──► "Your SSN is 123-45-6789" │
|
|
241
|
+
│ │ │ │
|
|
242
|
+
│ │ │ │
|
|
243
|
+
│ ▼ ▼ │
|
|
244
|
+
│ ┌─────────────────┐ ┌─────────────────┐ │
|
|
245
|
+
│ │ APL HOOK: │ │ APL HOOK: │ │
|
|
246
|
+
│ │ llm.pre_request │ │ output.pre_send │ │
|
|
247
|
+
│ └────────┬────────┘ └────────┬────────┘ │
|
|
248
|
+
│ │ │ │
|
|
249
|
+
│ │ │ │
|
|
250
|
+
│ ▼ ▼ │
|
|
251
|
+
│ ┌─────────────────────────────────────────────┐ │
|
|
252
|
+
│ │ POLICY SERVERS │ │
|
|
253
|
+
│ │ │ │
|
|
254
|
+
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
|
|
255
|
+
│ │ │ Budget │ │ PII │ │ Topic │ │ │
|
|
256
|
+
│ │ │ Check │ │ Filter │ │ Guard │ │ │
|
|
257
|
+
│ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │
|
|
258
|
+
│ │ │ │ │ │ │
|
|
259
|
+
│ │ ▼ ▼ ▼ │ │
|
|
260
|
+
│ │ ALLOW MODIFY ALLOW │ │
|
|
261
|
+
│ │ │ │ │
|
|
262
|
+
│ │ ▼ │ │
|
|
263
|
+
│ │ Composed: MODIFY (redact SSN) │ │
|
|
264
|
+
│ └─────────────────────────────────────────────┘ │
|
|
265
|
+
│ │ │
|
|
266
|
+
│ ▼ │
|
|
267
|
+
│ "Your SSN is [REDACTED]" │
|
|
268
|
+
│ │
|
|
269
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Event Types
|
|
273
|
+
|
|
274
|
+
APL intercepts at key moments in the agent lifecycle:
|
|
275
|
+
|
|
276
|
+
| Event | When | Use Cases |
|
|
277
|
+
|-------|------|-----------|
|
|
278
|
+
| `input.received` | User message arrives | Injection detection, input validation |
|
|
279
|
+
| `llm.pre_request` | Before calling LLM | Budget checks, prompt modification |
|
|
280
|
+
| `llm.post_response` | After LLM responds | Hallucination detection |
|
|
281
|
+
| `tool.pre_invoke` | Before tool execution | Permission checks, arg validation |
|
|
282
|
+
| `tool.post_invoke` | After tool returns | Result validation |
|
|
283
|
+
| `output.pre_send` | Before sending to user | **PII redaction**, content filtering |
|
|
284
|
+
|
|
285
|
+
### Verdict Types
|
|
286
|
+
|
|
287
|
+
Policies don't just allow or deny — they can guide:
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
# ✅ Allow the action
|
|
291
|
+
Verdict.allow()
|
|
292
|
+
|
|
293
|
+
# ❌ Block the action
|
|
294
|
+
Verdict.deny(reasoning="Contains prohibited content")
|
|
295
|
+
|
|
296
|
+
# 🔄 Modify and continue
|
|
297
|
+
Verdict.modify(
|
|
298
|
+
target="output",
|
|
299
|
+
operation="replace",
|
|
300
|
+
value="[REDACTED]",
|
|
301
|
+
reasoning="PII detected"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# ⚠️ Require human approval
|
|
305
|
+
Verdict.escalate(
|
|
306
|
+
type="human_confirm",
|
|
307
|
+
prompt="Delete production database?",
|
|
308
|
+
options=["Proceed", "Cancel"]
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# 👁️ Just observe (for audit logging)
|
|
312
|
+
Verdict.observe(
|
|
313
|
+
reasoning="Logged for compliance",
|
|
314
|
+
trace={"action": "sensitive_query"}
|
|
315
|
+
)
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## 📁 Examples
|
|
321
|
+
|
|
322
|
+
### 1. PII Filter (Redaction)
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
from apl import PolicyServer, Verdict
|
|
326
|
+
import re
|
|
327
|
+
|
|
328
|
+
server = PolicyServer("pii-filter")
|
|
329
|
+
|
|
330
|
+
PATTERNS = {
|
|
331
|
+
"ssn": r'\b\d{3}-\d{2}-\d{4}\b',
|
|
332
|
+
"credit_card": r'\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b',
|
|
333
|
+
"email": r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b',
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
@server.policy(name="redact-pii", events=["output.pre_send"])
|
|
337
|
+
async def redact_pii(event):
|
|
338
|
+
text = event.payload.output_text or ""
|
|
339
|
+
|
|
340
|
+
for name, pattern in PATTERNS.items():
|
|
341
|
+
text = re.sub(pattern, f'[{name.upper()} REDACTED]', text)
|
|
342
|
+
|
|
343
|
+
if text != event.payload.output_text:
|
|
344
|
+
return Verdict.modify(target="output", operation="replace", value=text)
|
|
345
|
+
|
|
346
|
+
return Verdict.allow()
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### 2. Budget Limiter
|
|
350
|
+
|
|
351
|
+
```python
|
|
352
|
+
from apl import PolicyServer, Verdict
|
|
353
|
+
|
|
354
|
+
server = PolicyServer("budget")
|
|
355
|
+
|
|
356
|
+
@server.policy(name="token-budget", events=["llm.pre_request"])
|
|
357
|
+
async def check_budget(event):
|
|
358
|
+
used = event.metadata.token_count
|
|
359
|
+
budget = event.metadata.token_budget or 100_000
|
|
360
|
+
|
|
361
|
+
if used >= budget:
|
|
362
|
+
return Verdict.deny(reasoning=f"Token budget exceeded: {used:,}/{budget:,}")
|
|
363
|
+
|
|
364
|
+
if used >= budget * 0.8:
|
|
365
|
+
return Verdict.observe(reasoning=f"Token usage at {used/budget:.0%}")
|
|
366
|
+
|
|
367
|
+
return Verdict.allow()
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### 3. Destructive Action Confirmation
|
|
371
|
+
|
|
372
|
+
```python
|
|
373
|
+
from apl import PolicyServer, Verdict
|
|
374
|
+
|
|
375
|
+
server = PolicyServer("safety")
|
|
376
|
+
|
|
377
|
+
@server.policy(name="confirm-delete", events=["tool.pre_invoke"])
|
|
378
|
+
async def confirm_delete(event):
|
|
379
|
+
tool = event.payload.tool_name or ""
|
|
380
|
+
|
|
381
|
+
if "delete" in tool.lower() or "drop" in tool.lower():
|
|
382
|
+
return Verdict.escalate(
|
|
383
|
+
type="human_confirm",
|
|
384
|
+
prompt=f"⚠️ Destructive action: {tool}\n\nProceed?",
|
|
385
|
+
options=["Proceed", "Cancel"]
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
return Verdict.allow()
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### 4. Declarative YAML Policy (No Python!)
|
|
392
|
+
|
|
393
|
+
```yaml
|
|
394
|
+
# compliance.yaml
|
|
395
|
+
name: corporate-compliance
|
|
396
|
+
version: 1.0.0
|
|
397
|
+
|
|
398
|
+
policies:
|
|
399
|
+
- name: block-competitor-info
|
|
400
|
+
events:
|
|
401
|
+
- output.pre_send
|
|
402
|
+
rules:
|
|
403
|
+
- when:
|
|
404
|
+
payload.output_text:
|
|
405
|
+
contains: "competitor revenue"
|
|
406
|
+
then:
|
|
407
|
+
decision: deny
|
|
408
|
+
reasoning: "Cannot share competitor financial information"
|
|
409
|
+
|
|
410
|
+
- name: confirm-data-export
|
|
411
|
+
events:
|
|
412
|
+
- tool.pre_invoke
|
|
413
|
+
rules:
|
|
414
|
+
- when:
|
|
415
|
+
payload.tool_name:
|
|
416
|
+
matches: ".*export.*"
|
|
417
|
+
metadata.user_region:
|
|
418
|
+
in: [EU, EEA, UK]
|
|
419
|
+
then:
|
|
420
|
+
decision: escalate
|
|
421
|
+
escalation:
|
|
422
|
+
type: human_confirm
|
|
423
|
+
prompt: "🇪🇺 GDPR: Confirm data export for EU user?"
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
```bash
|
|
427
|
+
apl serve compliance.yaml --http 8080
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## 🧩 Integration Patterns
|
|
433
|
+
|
|
434
|
+
### Pattern 1: Auto-Instrumentation (Recommended)
|
|
435
|
+
|
|
436
|
+
```python
|
|
437
|
+
import apl
|
|
438
|
+
|
|
439
|
+
# Patches OpenAI, Anthropic, LiteLLM, LangChain automatically
|
|
440
|
+
apl.auto_instrument(
|
|
441
|
+
policy_servers=[
|
|
442
|
+
"stdio://./policies/pii_filter.py",
|
|
443
|
+
"http://compliance.internal:8080",
|
|
444
|
+
],
|
|
445
|
+
user_id="user-123",
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# All LLM calls are now protected
|
|
449
|
+
from openai import OpenAI
|
|
450
|
+
client = OpenAI()
|
|
451
|
+
response = client.chat.completions.create(...) # ← APL intercepts this
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Pattern 2: Manual Integration
|
|
455
|
+
|
|
456
|
+
```python
|
|
457
|
+
from apl import PolicyLayer, EventPayload, SessionMetadata
|
|
458
|
+
|
|
459
|
+
policies = PolicyLayer()
|
|
460
|
+
policies.add_server("stdio://./my_policy.py")
|
|
461
|
+
|
|
462
|
+
# Call this before sending output
|
|
463
|
+
verdict = await policies.evaluate(
|
|
464
|
+
event_type="output.pre_send",
|
|
465
|
+
payload=EventPayload(output_text=response_text),
|
|
466
|
+
metadata=SessionMetadata(session_id="...", user_id="...")
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
if verdict.decision == "modify":
|
|
470
|
+
response_text = verdict.modification.value
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Pattern 3: LangGraph Wrapper
|
|
474
|
+
|
|
475
|
+
```python
|
|
476
|
+
from langgraph.graph import StateGraph
|
|
477
|
+
from apl.adapters.langgraph import APLGraphWrapper
|
|
478
|
+
|
|
479
|
+
# Build your graph
|
|
480
|
+
graph = StateGraph(MyState)
|
|
481
|
+
graph.add_node("agent", agent_node)
|
|
482
|
+
graph.add_node("tools", tool_node)
|
|
483
|
+
|
|
484
|
+
# Wrap it with APL
|
|
485
|
+
wrapper = APLGraphWrapper()
|
|
486
|
+
wrapper.add_server("stdio://./my_policy.py")
|
|
487
|
+
wrapped_graph = wrapper.wrap(graph)
|
|
488
|
+
|
|
489
|
+
# Use wrapped_graph — policies evaluated automatically
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
## 📖 API Reference
|
|
495
|
+
|
|
496
|
+
### CLI Commands
|
|
497
|
+
|
|
498
|
+
```
|
|
499
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
500
|
+
│ Command Description │
|
|
501
|
+
├──────────────────────────────────────────────────────────────┤
|
|
502
|
+
│ serve Run a policy server │
|
|
503
|
+
│ test Test a policy with sample events │
|
|
504
|
+
│ validate Validate a policy file │
|
|
505
|
+
│ init Create a new policy project │
|
|
506
|
+
│ info Show system information │
|
|
507
|
+
└──────────────────────────────────────────────────────────────┘
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
```bash
|
|
511
|
+
# Run a policy server with HTTP
|
|
512
|
+
apl serve ./policy.py --http 8080
|
|
513
|
+
|
|
514
|
+
# Test a policy
|
|
515
|
+
apl test ./policy.py -e output.pre_send
|
|
516
|
+
|
|
517
|
+
# Create a new project
|
|
518
|
+
apl init my-policy --template pii
|
|
519
|
+
|
|
520
|
+
# Validate without running
|
|
521
|
+
apl validate ./policy.yaml
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### HTTP API
|
|
525
|
+
|
|
526
|
+
| Endpoint | Method | Description |
|
|
527
|
+
|----------|--------|-------------|
|
|
528
|
+
| `/evaluate` | POST | Evaluate policies for an event |
|
|
529
|
+
| `/health` | GET | Health check |
|
|
530
|
+
| `/metrics` | GET | Prometheus metrics |
|
|
531
|
+
| `/manifest` | GET | Server manifest |
|
|
532
|
+
|
|
533
|
+
### Python API
|
|
534
|
+
|
|
535
|
+
```python
|
|
536
|
+
from apl import (
|
|
537
|
+
# Core
|
|
538
|
+
PolicyServer, # Create policy servers
|
|
539
|
+
PolicyLayer, # Connect to policy servers
|
|
540
|
+
Verdict, # Policy responses
|
|
541
|
+
|
|
542
|
+
# Auto-instrumentation
|
|
543
|
+
auto_instrument, # Patch LLM clients
|
|
544
|
+
uninstrument, # Remove patches
|
|
545
|
+
|
|
546
|
+
# Types
|
|
547
|
+
EventType, # Lifecycle events
|
|
548
|
+
EventPayload, # Event-specific data
|
|
549
|
+
SessionMetadata, # Session context
|
|
550
|
+
Message, # Chat message format
|
|
551
|
+
)
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
## 🏗️ Architecture
|
|
557
|
+
|
|
558
|
+
```
|
|
559
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
560
|
+
│ YOUR APPLICATION │
|
|
561
|
+
│ │
|
|
562
|
+
│ ┌───────────────────────────────────────────────────────────────┐ │
|
|
563
|
+
│ │ APL Policy Layer │ │
|
|
564
|
+
│ │ │ │
|
|
565
|
+
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
|
566
|
+
│ │ │ Client │ │ Client │ │ Client │ │ │
|
|
567
|
+
│ │ │ (stdio) │ │ (HTTP) │ │ (WebSocket)│ │ │
|
|
568
|
+
│ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │
|
|
569
|
+
│ └─────────┼─────────────────┼─────────────────┼─────────────────┘ │
|
|
570
|
+
└─────────────┼─────────────────┼─────────────────┼───────────────────┘
|
|
571
|
+
│ │ │
|
|
572
|
+
▼ ▼ ▼
|
|
573
|
+
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
574
|
+
│ Policy Server │ │ Policy Server │ │ Policy Server │
|
|
575
|
+
│ (Local Python) │ │ (Remote HTTP) │ │ (YAML) │
|
|
576
|
+
│ │ │ │ │ │
|
|
577
|
+
│ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │
|
|
578
|
+
│ │ Policy 1 │ │ │ │ Policy A │ │ │ │ Rule 1 │ │
|
|
579
|
+
│ │ Policy 2 │ │ │ │ Policy B │ │ │ │ Rule 2 │ │
|
|
580
|
+
│ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │
|
|
581
|
+
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
582
|
+
```
|
|
583
|
+
---------------------
|
|
584
|
+
|
|
585
|
+
<div align="center">
|
|
586
|
+
|
|
587
|
+
**🛡️**
|
|
588
|
+
|
|
589
|
+
*Secure your agents. Sleep better at night.*
|
|
590
|
+
|
|
591
|
+
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
apl/__init__.py,sha256=AK8ZBKNGD-oACVTzoPyugl4ZjTrTppLshCwbEkn5Vzg,5069
|
|
2
|
+
apl/cli.py,sha256=eG9wjShH42x6qPOf2nCfpqhcLTeac8_YHygRn1TuzMA,25586
|
|
3
|
+
apl/declarative.py,sha256=Sbc2KKTsaxte27uQGHs8q9xqHAjxPYRxiKZu3c0iQNw,14004
|
|
4
|
+
apl/instrument.py,sha256=Pdr2U4Nsu3CbpgLDMFL9zvniHVtgpyz_KUoNMZOQ1ys,33100
|
|
5
|
+
apl/layer.py,sha256=6gTk3vJvQBCeVhEwJOyilYlJh9pWphRy2sDD5RwGUbI,23595
|
|
6
|
+
apl/logging.py,sha256=3m3sl6DooOV2cIwXVASlebrOPnVzEWN27UPRcaYgJuA,11057
|
|
7
|
+
apl/server.py,sha256=dhz6nVI2CUlCYa3o6c3jQ4R5l034I70Ml1lvjlw2yOs,16152
|
|
8
|
+
apl/templates.py,sha256=VzlmcXIrn8wzEtuQep2aUHbuZ2zn43kGWkvGsPfPgfY,11282
|
|
9
|
+
apl/types.py,sha256=WOB_2rAp-C0E8hBCYBKmliSQTQY_RqmxDlXhk2uDuVA,12277
|
|
10
|
+
apl/adapters/__init__.py,sha256=YEJrrznUmXgJgeXIF-RLxNK1t3iaFnk8gDvbAh5MpIQ,480
|
|
11
|
+
apl/adapters/langgraph.py,sha256=XVBfDkQQvxUlmtfNFPUcj1jTqJagh-QhVl9B9h7aQjw,14439
|
|
12
|
+
apl/transports/__init__.py,sha256=xyxkfOb93WuTnUy3VXH2ES4WReJjo6wjl6DydqbTLEI,359
|
|
13
|
+
apl/transports/http.py,sha256=WJ7Y5Tguv3WRFgEl1_ooU43U0R3uVSvqTmjrJ630rHs,20523
|
|
14
|
+
agent_policy_layer-0.1.0.dist-info/METADATA,sha256=UzDac3vEI4OTtEJsTp94WiUT5klMKAlQeyFVIapEMos,22265
|
|
15
|
+
agent_policy_layer-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
16
|
+
agent_policy_layer-0.1.0.dist-info/entry_points.txt,sha256=-k9qW1UWpOWzFYP9zQHbzaDTnDHsSL6cNePOyS6WOSU,37
|
|
17
|
+
agent_policy_layer-0.1.0.dist-info/licenses/LICENSE,sha256=V4H80HdjfRvTVDuCKEUPKijUi8pfnMQOqs8YNnRPvR8,11350
|
|
18
|
+
agent_policy_layer-0.1.0.dist-info/RECORD,,
|