openterms-py 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.
- openterms_py-0.1.0/LICENSE +21 -0
- openterms_py-0.1.0/MANIFEST.in +4 -0
- openterms_py-0.1.0/PKG-INFO +552 -0
- openterms_py-0.1.0/README.md +517 -0
- openterms_py-0.1.0/openterms/__init__.py +33 -0
- openterms_py-0.1.0/openterms/cache.py +65 -0
- openterms_py-0.1.0/openterms/client.py +497 -0
- openterms_py-0.1.0/openterms/models.py +148 -0
- openterms_py-0.1.0/openterms/py.typed +0 -0
- openterms_py-0.1.0/openterms_py.egg-info/PKG-INFO +552 -0
- openterms_py-0.1.0/openterms_py.egg-info/SOURCES.txt +16 -0
- openterms_py-0.1.0/openterms_py.egg-info/dependency_links.txt +1 -0
- openterms_py-0.1.0/openterms_py.egg-info/requires.txt +11 -0
- openterms_py-0.1.0/openterms_py.egg-info/top_level.txt +1 -0
- openterms_py-0.1.0/pyproject.toml +61 -0
- openterms_py-0.1.0/setup.cfg +4 -0
- openterms_py-0.1.0/setup.py +8 -0
- openterms_py-0.1.0/tests/test_client.py +486 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OpenTerms
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openterms-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the OpenTerms Protocol — query machine-readable AI agent permissions.
|
|
5
|
+
Author-email: OpenTerms <hello@openterms.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://openterms.com
|
|
8
|
+
Project-URL: Documentation, https://openterms.com/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/jstibal/openterms-py
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/jstibal/openterms-py/issues
|
|
11
|
+
Keywords: openTerms,ai,agents,permissions,llm,langchain,crewai
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: requests>=2.28
|
|
26
|
+
Provides-Extra: async
|
|
27
|
+
Requires-Dist: httpx>=0.24; extra == "async"
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
31
|
+
Requires-Dist: responses>=0.23; extra == "dev"
|
|
32
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
33
|
+
Requires-Dist: ruff>=0.1; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# openterms-py
|
|
37
|
+
|
|
38
|
+
Python SDK for the [OpenTerms Protocol](https://openterms.com).
|
|
39
|
+
|
|
40
|
+
Query machine-readable AI agent permissions from `openterms.json` files before
|
|
41
|
+
your agent acts on a domain.
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
pip install openterms-py
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Core API
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import openterms
|
|
53
|
+
|
|
54
|
+
# Fetch the full openterms.json (cached in memory, TTL 1h by default)
|
|
55
|
+
terms = openterms.fetch("github.com")
|
|
56
|
+
|
|
57
|
+
# Check a single permission
|
|
58
|
+
result = openterms.check("github.com", "api_access")
|
|
59
|
+
# result.decision → "allow" | "deny" | "not_specified"
|
|
60
|
+
# bool(result) → True when decision is "allow"
|
|
61
|
+
|
|
62
|
+
# Get the discovery block (MCP servers, OpenAPI specs)
|
|
63
|
+
disc = openterms.discover("github.com")
|
|
64
|
+
|
|
65
|
+
# Generate a local compliance receipt
|
|
66
|
+
rec = openterms.receipt("github.com", "api_access", result.decision)
|
|
67
|
+
print(rec.to_dict())
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Installation
|
|
73
|
+
|
|
74
|
+
Requires Python 3.9+ and [`requests`](https://pypi.org/project/requests/)
|
|
75
|
+
(installed automatically).
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install openterms-py
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Optional async support via [`httpx`](https://pypi.org/project/httpx/):
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pip install "openterms-py[async]"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Functions
|
|
90
|
+
|
|
91
|
+
### `fetch(domain) → dict | None`
|
|
92
|
+
|
|
93
|
+
Fetches `/.well-known/openterms.json` from the domain, falling back to
|
|
94
|
+
`/openterms.json`. Returns the parsed JSON dict or `None` if unreachable.
|
|
95
|
+
|
|
96
|
+
Results are cached in memory. The TTL is taken from the server's
|
|
97
|
+
`Cache-Control: max-age=N` header, or the configured default (3600s).
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
terms = openterms.fetch("stripe.com")
|
|
101
|
+
if terms:
|
|
102
|
+
print(terms.get("service"))
|
|
103
|
+
print(terms.get("permissions"))
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
### `check(domain, action) → CheckResult`
|
|
109
|
+
|
|
110
|
+
Returns allow/deny for a single permission key. Evaluates to `True` in
|
|
111
|
+
boolean context when the decision is `"allow"`.
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
result = openterms.check("stripe.com", "api_access")
|
|
115
|
+
|
|
116
|
+
if result:
|
|
117
|
+
print("Access allowed")
|
|
118
|
+
else:
|
|
119
|
+
print(f"Blocked: {result.decision}") # "deny" or "not_specified"
|
|
120
|
+
|
|
121
|
+
# Access all fields
|
|
122
|
+
print(result.domain) # "stripe.com"
|
|
123
|
+
print(result.action) # "api_access"
|
|
124
|
+
print(result.decision) # "allow" | "deny" | "not_specified"
|
|
125
|
+
print(result.raw_value) # the raw value from permissions block
|
|
126
|
+
print(result.source) # "cache" | "network"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Common permission keys: `scrape_data`, `api_access`, `read_content`,
|
|
130
|
+
`index_content`, `train_on_content`, `execute_code`, `access_user_data`.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
### `discover(domain) → DiscoveryResult | None`
|
|
135
|
+
|
|
136
|
+
Returns the `discovery` block from the domain's `openterms.json`, or
|
|
137
|
+
`None` if absent.
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
disc = openterms.discover("acme-corp.com")
|
|
141
|
+
if disc:
|
|
142
|
+
for server in disc.mcp_servers:
|
|
143
|
+
print(server.url, server.transport, server.description)
|
|
144
|
+
for spec in disc.api_specs:
|
|
145
|
+
print(spec.url, spec.type)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`DiscoveryResult` fields:
|
|
149
|
+
- `mcp_servers` — list of `McpServer(url, transport, description)`
|
|
150
|
+
- `api_specs` — list of `ApiSpec(url, type, description)`
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
### `receipt(domain, action, decision) → Receipt`
|
|
155
|
+
|
|
156
|
+
Generates a minimal ORS compliance receipt. Local artifact only — nothing
|
|
157
|
+
is sent to any server.
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
result = openterms.check("github.com", "scrape_data")
|
|
161
|
+
rec = openterms.receipt("github.com", "scrape_data", result.decision)
|
|
162
|
+
|
|
163
|
+
print(rec.to_dict())
|
|
164
|
+
# {
|
|
165
|
+
# "domain": "github.com",
|
|
166
|
+
# "action": "scrape_data",
|
|
167
|
+
# "decision": "deny",
|
|
168
|
+
# "timestamp": "2026-04-11T10:40:00Z",
|
|
169
|
+
# "openterms_hash": "a3f2...c91d"
|
|
170
|
+
# }
|
|
171
|
+
|
|
172
|
+
# Log it, write to a file, store in your DB — your choice
|
|
173
|
+
import json
|
|
174
|
+
with open("compliance_log.jsonl", "a") as f:
|
|
175
|
+
f.write(json.dumps(rec.to_dict()) + "\n")
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
### `configure(default_ttl, timeout, user_agent)`
|
|
181
|
+
|
|
182
|
+
Adjust the shared client settings. Clears the existing cache.
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
openterms.configure(
|
|
186
|
+
default_ttl=600, # 10-minute cache
|
|
187
|
+
timeout=5, # 5-second HTTP timeout
|
|
188
|
+
)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### `clear_cache(domain=None)`
|
|
192
|
+
|
|
193
|
+
Flush cached entries. Pass a domain to evict a single entry, or call with
|
|
194
|
+
no args to flush everything.
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
openterms.clear_cache("github.com") # evict one domain
|
|
198
|
+
openterms.clear_cache() # flush all
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Plain Python example
|
|
204
|
+
|
|
205
|
+
No framework, just a permission gate before an HTTP call.
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
import requests
|
|
209
|
+
import openterms
|
|
210
|
+
|
|
211
|
+
TARGET_DOMAIN = "data-provider.com"
|
|
212
|
+
|
|
213
|
+
def fetch_data_if_permitted(url: str) -> dict | None:
|
|
214
|
+
result = openterms.check(TARGET_DOMAIN, "api_access")
|
|
215
|
+
|
|
216
|
+
# Record the decision
|
|
217
|
+
rec = openterms.receipt(TARGET_DOMAIN, "api_access", result.decision)
|
|
218
|
+
print("Receipt:", rec.to_dict())
|
|
219
|
+
|
|
220
|
+
if not result:
|
|
221
|
+
print(f"api_access is {result.decision} for {TARGET_DOMAIN}. Aborting.")
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
resp = requests.get(url, timeout=10)
|
|
225
|
+
resp.raise_for_status()
|
|
226
|
+
return resp.json()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
data = fetch_data_if_permitted("https://data-provider.com/api/items")
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## LangChain integration
|
|
235
|
+
|
|
236
|
+
Gate a web-interaction tool behind an OpenTerms permission check.
|
|
237
|
+
|
|
238
|
+
### Option 1 — Custom Tool with permission guard
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
from langchain_core.tools import tool
|
|
242
|
+
import openterms
|
|
243
|
+
|
|
244
|
+
@tool
|
|
245
|
+
def fetch_page_content(url: str) -> str:
|
|
246
|
+
"""Fetch the text content of a web page.
|
|
247
|
+
|
|
248
|
+
Only proceeds if the domain's openterms.json permits scraping.
|
|
249
|
+
"""
|
|
250
|
+
from urllib.parse import urlparse
|
|
251
|
+
import requests
|
|
252
|
+
|
|
253
|
+
domain = urlparse(url).hostname or url
|
|
254
|
+
|
|
255
|
+
result = openterms.check(domain, "scrape_data")
|
|
256
|
+
|
|
257
|
+
# Log the compliance receipt
|
|
258
|
+
rec = openterms.receipt(domain, "scrape_data", result.decision)
|
|
259
|
+
print(f"[OpenTerms] receipt: {rec.to_dict()}")
|
|
260
|
+
|
|
261
|
+
if not result:
|
|
262
|
+
return (
|
|
263
|
+
f"Cannot fetch {url}: scrape_data is '{result.decision}' "
|
|
264
|
+
f"for {domain} per their openterms.json."
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
resp = requests.get(url, timeout=10)
|
|
268
|
+
resp.raise_for_status()
|
|
269
|
+
return resp.text[:4000]
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# Use in an agent
|
|
273
|
+
from langchain_anthropic import ChatAnthropic
|
|
274
|
+
from langgraph.prebuilt import create_react_agent
|
|
275
|
+
|
|
276
|
+
llm = ChatAnthropic(model="claude-3-5-sonnet-20241022")
|
|
277
|
+
agent = create_react_agent(llm, tools=[fetch_page_content])
|
|
278
|
+
|
|
279
|
+
result = agent.invoke({
|
|
280
|
+
"messages": [("user", "Summarise the content at https://example.com")]
|
|
281
|
+
})
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Option 2 — Pre-action callback on any browser tool
|
|
285
|
+
|
|
286
|
+
Wrap an existing tool class to inject the permission check transparently:
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
from langchain_core.tools import BaseTool
|
|
290
|
+
from langchain_core.callbacks import CallbackManagerForToolRun
|
|
291
|
+
from typing import Optional, Type, Any
|
|
292
|
+
from pydantic import BaseModel
|
|
293
|
+
import openterms
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class OpenTermsGuard(BaseTool):
|
|
297
|
+
"""Wraps any web tool and gates execution on OpenTerms permission."""
|
|
298
|
+
|
|
299
|
+
name: str = "openTerms_guarded_browser"
|
|
300
|
+
description: str = "Fetch a URL, checking OpenTerms permissions first."
|
|
301
|
+
permission: str = "scrape_data"
|
|
302
|
+
wrapped_tool: Any # the underlying LangChain browser/fetch tool
|
|
303
|
+
|
|
304
|
+
class ArgsSchema(BaseModel):
|
|
305
|
+
url: str
|
|
306
|
+
|
|
307
|
+
args_schema: Type[BaseModel] = ArgsSchema
|
|
308
|
+
|
|
309
|
+
def _run(
|
|
310
|
+
self,
|
|
311
|
+
url: str,
|
|
312
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
313
|
+
) -> str:
|
|
314
|
+
from urllib.parse import urlparse
|
|
315
|
+
domain = urlparse(url).hostname or url
|
|
316
|
+
result = openterms.check(domain, self.permission)
|
|
317
|
+
|
|
318
|
+
rec = openterms.receipt(domain, self.permission, result.decision)
|
|
319
|
+
print(f"[OpenTerms] {rec.to_dict()}")
|
|
320
|
+
|
|
321
|
+
if not result:
|
|
322
|
+
return (
|
|
323
|
+
f"Blocked by OpenTerms: '{self.permission}' is "
|
|
324
|
+
f"'{result.decision}' for {domain}."
|
|
325
|
+
)
|
|
326
|
+
return self.wrapped_tool.run(url)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# Usage
|
|
330
|
+
from langchain_community.tools import BrowserTool # or any fetch tool
|
|
331
|
+
browser = BrowserTool()
|
|
332
|
+
guarded = OpenTermsGuard(wrapped_tool=browser)
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Option 3 — Discover MCP servers for a domain before connecting
|
|
336
|
+
|
|
337
|
+
```python
|
|
338
|
+
import openterms
|
|
339
|
+
|
|
340
|
+
def get_mcp_servers_for_domain(domain: str) -> list[dict]:
|
|
341
|
+
disc = openterms.discover(domain)
|
|
342
|
+
if not disc:
|
|
343
|
+
return []
|
|
344
|
+
return [
|
|
345
|
+
{"url": s.url, "transport": s.transport}
|
|
346
|
+
for s in disc.mcp_servers
|
|
347
|
+
]
|
|
348
|
+
|
|
349
|
+
servers = get_mcp_servers_for_domain("acme-corp.com")
|
|
350
|
+
# [{"url": "https://acme-corp.com/mcp/sse", "transport": "sse"}]
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## CrewAI integration
|
|
356
|
+
|
|
357
|
+
Gate a CrewAI agent's web tasks behind OpenTerms permission checks.
|
|
358
|
+
|
|
359
|
+
### Option 1 — Custom tool for CrewAI
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
from crewai_tools import BaseTool
|
|
363
|
+
import openterms
|
|
364
|
+
import requests
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class OpenTermsWebTool(BaseTool):
|
|
368
|
+
name: str = "web_fetch_with_permissions"
|
|
369
|
+
description: str = (
|
|
370
|
+
"Fetch content from a URL. "
|
|
371
|
+
"Checks the domain's OpenTerms permissions before proceeding. "
|
|
372
|
+
"Returns an error string if the domain denies the requested action."
|
|
373
|
+
)
|
|
374
|
+
permission: str = "scrape_data"
|
|
375
|
+
|
|
376
|
+
def _run(self, url: str) -> str:
|
|
377
|
+
from urllib.parse import urlparse
|
|
378
|
+
domain = urlparse(url).hostname or url
|
|
379
|
+
|
|
380
|
+
result = openterms.check(domain, self.permission)
|
|
381
|
+
|
|
382
|
+
# Store receipt for audit trail
|
|
383
|
+
rec = openterms.receipt(domain, self.permission, result.decision)
|
|
384
|
+
# In production: write rec.to_dict() to your audit log
|
|
385
|
+
|
|
386
|
+
if not result:
|
|
387
|
+
return (
|
|
388
|
+
f"OpenTerms check failed for {domain}: "
|
|
389
|
+
f"{self.permission} = {result.decision}. "
|
|
390
|
+
"Do not proceed with this URL."
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
resp = requests.get(url, timeout=10)
|
|
394
|
+
resp.raise_for_status()
|
|
395
|
+
return resp.text[:4000]
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
# Use in a CrewAI agent
|
|
399
|
+
from crewai import Agent, Task, Crew
|
|
400
|
+
|
|
401
|
+
web_tool = OpenTermsWebTool(permission="scrape_data")
|
|
402
|
+
|
|
403
|
+
researcher = Agent(
|
|
404
|
+
role="Web Researcher",
|
|
405
|
+
goal="Research topics from web sources that permit scraping.",
|
|
406
|
+
backstory="You respect site permissions and only access allowed content.",
|
|
407
|
+
tools=[web_tool],
|
|
408
|
+
verbose=True,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
task = Task(
|
|
412
|
+
description="Find and summarise pricing information from competitor websites.",
|
|
413
|
+
expected_output="A bullet-point comparison of competitor pricing.",
|
|
414
|
+
agent=researcher,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
crew = Crew(agents=[researcher], tasks=[task], verbose=True)
|
|
418
|
+
result = crew.kickoff()
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### Option 2 — Callback hook for CrewAI task lifecycle
|
|
422
|
+
|
|
423
|
+
Use a `before_kickoff` step to pre-validate all domains a task will touch:
|
|
424
|
+
|
|
425
|
+
```python
|
|
426
|
+
from crewai import Crew, Agent, Task, Process
|
|
427
|
+
from typing import Union
|
|
428
|
+
import openterms
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def check_domain_permissions(
|
|
432
|
+
domains: list[str],
|
|
433
|
+
action: str,
|
|
434
|
+
) -> dict[str, str]:
|
|
435
|
+
"""
|
|
436
|
+
Returns {domain: decision} for all domains.
|
|
437
|
+
Raises ValueError if any domain explicitly denies the action.
|
|
438
|
+
"""
|
|
439
|
+
results = {}
|
|
440
|
+
denied = []
|
|
441
|
+
for domain in domains:
|
|
442
|
+
r = openterms.check(domain, action)
|
|
443
|
+
results[domain] = r.decision
|
|
444
|
+
if r.decision == "deny":
|
|
445
|
+
denied.append(domain)
|
|
446
|
+
if denied:
|
|
447
|
+
raise ValueError(
|
|
448
|
+
f"OpenTerms: {action!r} denied for: {', '.join(denied)}"
|
|
449
|
+
)
|
|
450
|
+
return results
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
# Before running your Crew, validate the target domains
|
|
454
|
+
target_domains = ["competitor-a.com", "competitor-b.com"]
|
|
455
|
+
|
|
456
|
+
try:
|
|
457
|
+
permissions = check_domain_permissions(target_domains, "scrape_data")
|
|
458
|
+
print(f"All domains permitted: {permissions}")
|
|
459
|
+
# safe to proceed
|
|
460
|
+
# crew.kickoff(...)
|
|
461
|
+
except ValueError as e:
|
|
462
|
+
print(f"Aborting: {e}")
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Option 3 — API discovery for CrewAI MCP tool selection
|
|
466
|
+
|
|
467
|
+
```python
|
|
468
|
+
import openterms
|
|
469
|
+
from crewai import Agent
|
|
470
|
+
|
|
471
|
+
def build_agent_for_domain(domain: str) -> Agent:
|
|
472
|
+
disc = openterms.discover(domain)
|
|
473
|
+
|
|
474
|
+
tools = []
|
|
475
|
+
if disc and disc.api_specs:
|
|
476
|
+
# Dynamically load tools from discovered OpenAPI specs
|
|
477
|
+
for spec in disc.api_specs:
|
|
478
|
+
print(f"Found API spec: {spec.url} ({spec.type})")
|
|
479
|
+
# Load spec and generate tools here (e.g. via openapi-core)
|
|
480
|
+
|
|
481
|
+
return Agent(
|
|
482
|
+
role="Domain Specialist",
|
|
483
|
+
goal=f"Interact with {domain} using its declared API.",
|
|
484
|
+
backstory=f"You have been given the API specs for {domain}.",
|
|
485
|
+
tools=tools,
|
|
486
|
+
)
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
## Models reference
|
|
492
|
+
|
|
493
|
+
```python
|
|
494
|
+
# CheckResult
|
|
495
|
+
result.domain # str
|
|
496
|
+
result.action # str
|
|
497
|
+
result.decision # "allow" | "deny" | "not_specified"
|
|
498
|
+
result.raw_value # Any — the raw permissions value (bool, dict, None)
|
|
499
|
+
result.source # "cache" | "network"
|
|
500
|
+
bool(result) # True iff decision == "allow"
|
|
501
|
+
|
|
502
|
+
# DiscoveryResult
|
|
503
|
+
disc.mcp_servers # list[McpServer]
|
|
504
|
+
disc.api_specs # list[ApiSpec]
|
|
505
|
+
|
|
506
|
+
# McpServer
|
|
507
|
+
server.url # str
|
|
508
|
+
server.transport # str ("sse" | "stdio" | "streamable-http")
|
|
509
|
+
server.description # str | None
|
|
510
|
+
|
|
511
|
+
# ApiSpec
|
|
512
|
+
spec.url # str
|
|
513
|
+
spec.type # str ("openapi_3" | "swagger_2" | "graphql_schema")
|
|
514
|
+
spec.description # str | None
|
|
515
|
+
|
|
516
|
+
# Receipt
|
|
517
|
+
rec.domain # str
|
|
518
|
+
rec.action # str
|
|
519
|
+
rec.decision # "allow" | "deny" | "not_specified"
|
|
520
|
+
rec.timestamp # str (ISO 8601 UTC)
|
|
521
|
+
rec.openterms_hash # str (SHA-256 hex, empty if domain was unreachable)
|
|
522
|
+
rec.to_dict() # → dict
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
---
|
|
526
|
+
|
|
527
|
+
## Advanced configuration
|
|
528
|
+
|
|
529
|
+
```python
|
|
530
|
+
import openterms
|
|
531
|
+
|
|
532
|
+
# Shorter cache, stricter timeout
|
|
533
|
+
openterms.configure(default_ttl=300, timeout=5)
|
|
534
|
+
|
|
535
|
+
# Per-request: bypass cache by clearing first
|
|
536
|
+
openterms.clear_cache("github.com")
|
|
537
|
+
result = openterms.check("github.com", "api_access")
|
|
538
|
+
|
|
539
|
+
# Use your own client instance (e.g. for testing with a mock cache)
|
|
540
|
+
from openterms.client import OpenTermsClient
|
|
541
|
+
from openterms.cache import TermsCache
|
|
542
|
+
|
|
543
|
+
custom_cache = TermsCache()
|
|
544
|
+
client = OpenTermsClient(default_ttl=0, cache=custom_cache)
|
|
545
|
+
result = client.check("github.com", "api_access")
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
## License
|
|
551
|
+
|
|
552
|
+
MIT
|