frisk-screen 0.0.1__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.
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ build/
6
+ dist/
7
+ .pytest_cache/
8
+ .venv/
9
+ venv/
10
+ .env
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Frisk contributors
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,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: frisk-screen
3
+ Version: 0.0.1
4
+ Summary: Pre-transaction risk screening for autonomous AI agents.
5
+ Project-URL: Homepage, https://github.com/Jiangw2718i/frisk
6
+ Project-URL: Source, https://github.com/Jiangw2718i/frisk
7
+ Author: Frisk contributors
8
+ License: MIT License
9
+
10
+ Copyright (c) 2026 Frisk contributors
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+ License-File: LICENSE
30
+ Keywords: agent-payments,ai-agents,mcp,risk,trust,x402
31
+ Classifier: Development Status :: 3 - Alpha
32
+ Classifier: Intended Audience :: Developers
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Programming Language :: Python :: 3.10
35
+ Classifier: Programming Language :: Python :: 3.11
36
+ Classifier: Programming Language :: Python :: 3.12
37
+ Classifier: Programming Language :: Python :: 3.13
38
+ Classifier: Topic :: Security
39
+ Requires-Python: >=3.10
40
+ Description-Content-Type: text/markdown
41
+
42
+ # Frisk
43
+
44
+ Pre-transaction risk screening for autonomous AI agents.
45
+
46
+ Before your agent pays an x402 seller or calls an unfamiliar tool, ask Frisk
47
+ whether the counterparty is trustworthy and whether the transaction fits your
48
+ policy. Frisk returns a verdict — `allow`, `review`, or `block` — with a
49
+ trust score and human-readable reasons. It is advisory: your agent stays in
50
+ control of the decision.
51
+
52
+ ## Install
53
+
54
+ ```bash
55
+ pip install frisk-screen
56
+ ```
57
+
58
+ ## Usage
59
+
60
+ ```python
61
+ from frisk import Client, Policy
62
+
63
+ client = Client() # lite mode, no key required
64
+
65
+ result = client.screen(
66
+ "0x9a3f1b2c3d4e5f60718293a4b5c6d7e8f9a0bc12",
67
+ endpoint="https://api.seller.x402/quote",
68
+ amount=2.50,
69
+ asset="USDC",
70
+ policy=Policy(max_per_call=5.00),
71
+ )
72
+
73
+ if not result.allowed:
74
+ print(result.verdict, result.trust_score, result.reasons)
75
+ # hold the payment for review
76
+ ```
77
+
78
+ ## Lite mode vs. hosted
79
+
80
+ | | Lite (default) | Hosted (API key) |
81
+ | --- | --- | --- |
82
+ | Runs | Locally, offline | Frisk API |
83
+ | Signals | Public, structural checks only | Reputation graph, trained models, live threat feed |
84
+ | Confidence | Always `low` | Rises with coverage |
85
+ | Cost | Free | Usage-based |
86
+
87
+ Lite mode catches obvious problems — malformed counterparties, `payTo` swaps,
88
+ insecure endpoints, policy violations, and a small seed blocklist. For
89
+ reputation history and continuously updated threat intelligence, pass an API
90
+ key:
91
+
92
+ ```python
93
+ client = Client(api_key="...")
94
+ ```
95
+
96
+ ## Design
97
+
98
+ - **Zero runtime dependencies.** The SDK uses only the standard library.
99
+ - **Advisory, not in-path.** Frisk never holds your funds or blocks a
100
+ payment itself; it returns a verdict and your code decides.
101
+ - **Typed.** Ships with type hints (`py.typed`).
102
+
103
+ ## Development
104
+
105
+ This project uses [uv](https://docs.astral.sh/uv/).
106
+
107
+ ```bash
108
+ uv sync
109
+ uv run pytest
110
+ ```
111
+
112
+ ## License
113
+
114
+ MIT — see [LICENSE](LICENSE). Security disclosures: see [SECURITY.md](SECURITY.md).
@@ -0,0 +1,73 @@
1
+ # Frisk
2
+
3
+ Pre-transaction risk screening for autonomous AI agents.
4
+
5
+ Before your agent pays an x402 seller or calls an unfamiliar tool, ask Frisk
6
+ whether the counterparty is trustworthy and whether the transaction fits your
7
+ policy. Frisk returns a verdict — `allow`, `review`, or `block` — with a
8
+ trust score and human-readable reasons. It is advisory: your agent stays in
9
+ control of the decision.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install frisk-screen
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```python
20
+ from frisk import Client, Policy
21
+
22
+ client = Client() # lite mode, no key required
23
+
24
+ result = client.screen(
25
+ "0x9a3f1b2c3d4e5f60718293a4b5c6d7e8f9a0bc12",
26
+ endpoint="https://api.seller.x402/quote",
27
+ amount=2.50,
28
+ asset="USDC",
29
+ policy=Policy(max_per_call=5.00),
30
+ )
31
+
32
+ if not result.allowed:
33
+ print(result.verdict, result.trust_score, result.reasons)
34
+ # hold the payment for review
35
+ ```
36
+
37
+ ## Lite mode vs. hosted
38
+
39
+ | | Lite (default) | Hosted (API key) |
40
+ | --- | --- | --- |
41
+ | Runs | Locally, offline | Frisk API |
42
+ | Signals | Public, structural checks only | Reputation graph, trained models, live threat feed |
43
+ | Confidence | Always `low` | Rises with coverage |
44
+ | Cost | Free | Usage-based |
45
+
46
+ Lite mode catches obvious problems — malformed counterparties, `payTo` swaps,
47
+ insecure endpoints, policy violations, and a small seed blocklist. For
48
+ reputation history and continuously updated threat intelligence, pass an API
49
+ key:
50
+
51
+ ```python
52
+ client = Client(api_key="...")
53
+ ```
54
+
55
+ ## Design
56
+
57
+ - **Zero runtime dependencies.** The SDK uses only the standard library.
58
+ - **Advisory, not in-path.** Frisk never holds your funds or blocks a
59
+ payment itself; it returns a verdict and your code decides.
60
+ - **Typed.** Ships with type hints (`py.typed`).
61
+
62
+ ## Development
63
+
64
+ This project uses [uv](https://docs.astral.sh/uv/).
65
+
66
+ ```bash
67
+ uv sync
68
+ uv run pytest
69
+ ```
70
+
71
+ ## License
72
+
73
+ MIT — see [LICENSE](LICENSE). Security disclosures: see [SECURITY.md](SECURITY.md).
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "frisk-screen"
7
+ version = "0.0.1"
8
+ description = "Pre-transaction risk screening for autonomous AI agents."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { file = "LICENSE" }
12
+ authors = [{ name = "Frisk contributors" }]
13
+ keywords = ["ai-agents", "x402", "risk", "trust", "mcp", "agent-payments"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Topic :: Security",
23
+ ]
24
+ dependencies = []
25
+
26
+ [dependency-groups]
27
+ dev = [
28
+ "pytest>=8",
29
+ "ruff>=0.15.17",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/Jiangw2718i/frisk"
34
+ Source = "https://github.com/Jiangw2718i/frisk"
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["src/frisk"]
38
+
39
+ [tool.hatch.build.targets.sdist]
40
+ include = ["src", "README.md", "LICENSE", "pyproject.toml"]
41
+
42
+ [tool.pytest.ini_options]
43
+ pythonpath = ["src"]
44
+ testpaths = ["tests"]
@@ -0,0 +1,22 @@
1
+ """Pre-transaction risk screening for autonomous AI agents."""
2
+
3
+ from .client import Client, ScreenError
4
+ from .types import (
5
+ Confidence,
6
+ Policy,
7
+ ScreenRequest,
8
+ ScreenResult,
9
+ Verdict,
10
+ )
11
+
12
+ __all__ = [
13
+ "Client",
14
+ "ScreenError",
15
+ "Confidence",
16
+ "Policy",
17
+ "ScreenRequest",
18
+ "ScreenResult",
19
+ "Verdict",
20
+ ]
21
+
22
+ __version__ = "0.0.1"
@@ -0,0 +1,134 @@
1
+ """Client entry point for screening agent transactions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import urllib.error
7
+ import urllib.request
8
+ from typing import Optional
9
+ from urllib.parse import urlparse
10
+
11
+ from .lite import evaluate_lite
12
+ from .types import Confidence, Policy, ScreenRequest, ScreenResult, Verdict
13
+
14
+ DEFAULT_BASE_URL = "https://api.tryfrisk.dev"
15
+
16
+
17
+ class ScreenError(RuntimeError):
18
+ """Raised when a hosted screening request cannot be completed."""
19
+
20
+
21
+ def _normalize_base_url(raw: str) -> str:
22
+ """Validate and normalize a base URL.
23
+
24
+ HTTPS is required so the API key in the Authorization header is never sent
25
+ in plaintext; http is allowed only for localhost to support local
26
+ development and tests.
27
+ """
28
+ trimmed = raw.rstrip("/")
29
+ parsed = urlparse(trimmed)
30
+ is_localhost = parsed.hostname in ("localhost", "127.0.0.1", "::1")
31
+ if parsed.scheme == "https" or (parsed.scheme == "http" and is_localhost):
32
+ return trimmed
33
+ raise ValueError("base_url must use HTTPS (http is allowed only for localhost)")
34
+
35
+
36
+ class Client:
37
+ """Screen agent transactions before they execute.
38
+
39
+ Without an API key the client runs in offline ``lite`` mode. Supply an
40
+ API key to use the hosted service, which adds reputation-graph, model,
41
+ and threat-feed signals and returns higher-confidence verdicts.
42
+
43
+ Example::
44
+
45
+ client = Client() # lite mode
46
+ result = client.screen("0xabc...", amount=2.5, asset="USDC")
47
+ if not result.allowed:
48
+ ... # hold the payment for review
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ api_key: Optional[str] = None,
54
+ *,
55
+ base_url: str = DEFAULT_BASE_URL,
56
+ timeout: float = 5.0,
57
+ ) -> None:
58
+ self._api_key = api_key
59
+ self._base_url = _normalize_base_url(base_url)
60
+ self._timeout = timeout
61
+
62
+ @property
63
+ def mode(self) -> str:
64
+ return "hosted" if self._api_key else "lite"
65
+
66
+ def screen(
67
+ self,
68
+ counterparty: str,
69
+ *,
70
+ endpoint: Optional[str] = None,
71
+ amount: Optional[float] = None,
72
+ asset: Optional[str] = None,
73
+ policy: Optional[Policy] = None,
74
+ observed_pay_to: Optional[str] = None,
75
+ strictness: Optional[float] = None,
76
+ ) -> ScreenResult:
77
+ request = ScreenRequest(
78
+ counterparty=counterparty,
79
+ endpoint=endpoint,
80
+ amount=amount,
81
+ asset=asset,
82
+ policy=policy,
83
+ observed_pay_to=observed_pay_to,
84
+ strictness=strictness,
85
+ )
86
+ if self._api_key:
87
+ return self._screen_hosted(request)
88
+ return evaluate_lite(request)
89
+
90
+ def _screen_hosted(self, request: ScreenRequest) -> ScreenResult:
91
+ payload = {
92
+ "counterparty": request.counterparty,
93
+ "endpoint": request.endpoint,
94
+ "amount": request.amount,
95
+ "asset": request.asset,
96
+ "observed_pay_to": request.observed_pay_to,
97
+ "strictness": request.strictness,
98
+ }
99
+ if request.policy is not None:
100
+ payload["policy"] = {
101
+ "max_per_call": request.policy.max_per_call,
102
+ "allowed_assets": request.policy.allowed_assets,
103
+ }
104
+
105
+ http_request = urllib.request.Request(
106
+ f"{self._base_url}/v1/screen",
107
+ data=json.dumps(payload).encode("utf-8"),
108
+ headers={
109
+ "Content-Type": "application/json",
110
+ "Authorization": f"Bearer {self._api_key}",
111
+ },
112
+ method="POST",
113
+ )
114
+ try:
115
+ with urllib.request.urlopen(
116
+ http_request, timeout=self._timeout
117
+ ) as response:
118
+ data = json.loads(response.read().decode("utf-8"))
119
+ except urllib.error.HTTPError as exc:
120
+ raise ScreenError(f"hosted screening failed: HTTP {exc.code}") from exc
121
+ except urllib.error.URLError as exc:
122
+ raise ScreenError(f"hosted screening unreachable: {exc.reason}") from exc
123
+ return _result_from_payload(data)
124
+
125
+
126
+ def _result_from_payload(data: dict) -> ScreenResult:
127
+ return ScreenResult(
128
+ verdict=Verdict(data["verdict"]),
129
+ trust_score=int(data["trust_score"]),
130
+ confidence=Confidence(data.get("confidence", "low")),
131
+ reasons=list(data.get("reasons", [])),
132
+ policy_hits=list(data.get("policy_hits", [])),
133
+ source="hosted",
134
+ )
@@ -0,0 +1,9 @@
1
+ # Seed blocklist
2
+
3
+ `blocklist_seed.json` is a small, offline subset of known-bad counterparties
4
+ shipped with the package so that lite mode can flag the most obvious cases
5
+ without a network call.
6
+
7
+ It is **not** the authoritative list. The live, continuously updated
8
+ blocklist and reputation data are served by the hosted API. Entries here are
9
+ only added when independently verifiable from public sources.
@@ -0,0 +1,115 @@
1
+ """Offline ("lite") screening.
2
+
3
+ The lite engine runs entirely on the caller's machine using only public,
4
+ locally available signals. It is intentionally weaker than the hosted
5
+ service and always reports LOW confidence: it has no reputation history, no
6
+ threat feed, and no trained models. Supply an API key to :class:`Client` to
7
+ use the hosted service for graph-, model-, and threat-feed-backed verdicts.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import os
14
+ from typing import Iterable, List, Optional, Set
15
+
16
+ from .types import Confidence, ScreenRequest, ScreenResult, Verdict
17
+
18
+ _SEED_BLOCKLIST_PATH = os.path.join(
19
+ os.path.dirname(__file__), "data", "blocklist_seed.json"
20
+ )
21
+
22
+ _BASELINE_SCORE = 70 # lite cannot see reputation history, so it starts neutral
23
+
24
+
25
+ def _load_seed_blocklist() -> Set[str]:
26
+ try:
27
+ with open(_SEED_BLOCKLIST_PATH, "r", encoding="utf-8") as handle:
28
+ return {str(entry).lower() for entry in json.load(handle)}
29
+ except (OSError, ValueError):
30
+ return set()
31
+
32
+
33
+ _SEED_BLOCKLIST = _load_seed_blocklist()
34
+
35
+
36
+ def evaluate_lite(
37
+ request: ScreenRequest,
38
+ blocklist: Optional[Iterable[str]] = None,
39
+ ) -> ScreenResult:
40
+ """Score a request using offline signals only."""
41
+ block = (
42
+ _SEED_BLOCKLIST
43
+ if blocklist is None
44
+ else {str(entry).lower() for entry in blocklist}
45
+ )
46
+
47
+ score = _BASELINE_SCORE
48
+ reasons: List[str] = []
49
+ counterparty = (request.counterparty or "").lower()
50
+
51
+ # 1. Seed blocklist. The live, continuously updated list is hosted-only.
52
+ if counterparty in block:
53
+ score = 0
54
+ reasons.append("counterparty on local seed blocklist")
55
+
56
+ # 2. Address sanity.
57
+ if not _looks_like_address(counterparty):
58
+ score -= 15
59
+ reasons.append("counterparty is not a well-formed address")
60
+
61
+ # 3. Dynamic payTo swap (a documented x402 attack vector).
62
+ if request.observed_pay_to and request.observed_pay_to.lower() != counterparty:
63
+ score -= 30
64
+ reasons.append("payTo differs from the expected counterparty")
65
+
66
+ # 4. Insecure endpoint.
67
+ if request.endpoint and not request.endpoint.lower().startswith("https://"):
68
+ score -= 20
69
+ reasons.append("endpoint is not served over HTTPS")
70
+
71
+ # 5. Spending policy (deterministic, fully offline).
72
+ policy_hits = _check_policy(request)
73
+ if policy_hits:
74
+ score -= 25
75
+
76
+ score = max(0, min(100, score))
77
+ return ScreenResult(
78
+ verdict=_verdict_for(score, policy_hits),
79
+ trust_score=score,
80
+ confidence=Confidence.LOW,
81
+ reasons=reasons,
82
+ policy_hits=policy_hits,
83
+ source="lite",
84
+ )
85
+
86
+
87
+ def _check_policy(request: ScreenRequest) -> List[str]:
88
+ hits: List[str] = []
89
+ policy = request.policy
90
+ if policy is None:
91
+ return hits
92
+ if policy.max_per_call is not None and request.amount is not None:
93
+ if request.amount > policy.max_per_call:
94
+ hits.append(
95
+ f"amount {request.amount} exceeds max_per_call {policy.max_per_call}"
96
+ )
97
+ if policy.allowed_assets is not None and request.asset is not None:
98
+ if request.asset not in policy.allowed_assets:
99
+ hits.append(f"asset {request.asset} not in allowed_assets")
100
+ return hits
101
+
102
+
103
+ def _verdict_for(score: int, policy_hits: List[str]) -> Verdict:
104
+ if score < 20:
105
+ return Verdict.BLOCK
106
+ if policy_hits or score < 65:
107
+ return Verdict.REVIEW
108
+ return Verdict.ALLOW
109
+
110
+
111
+ def _looks_like_address(value: str) -> bool:
112
+ if value.startswith("0x") and len(value) == 42:
113
+ return all(character in "0123456789abcdef" for character in value[2:])
114
+ # Non-EVM identifiers (e.g. Solana base58) — length heuristic only.
115
+ return 32 <= len(value) <= 60
File without changes
@@ -0,0 +1,67 @@
1
+ """Core data types for the screening SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+ from typing import List, Optional
8
+
9
+
10
+ class Verdict(str, Enum):
11
+ """Recommended action for a screened transaction."""
12
+
13
+ ALLOW = "allow"
14
+ REVIEW = "review"
15
+ BLOCK = "block"
16
+
17
+
18
+ class Confidence(str, Enum):
19
+ """How much weight to place on the verdict.
20
+
21
+ Offline (lite) screening always reports ``LOW``; the hosted service
22
+ raises confidence as graph, model, and threat-feed coverage improve.
23
+ """
24
+
25
+ LOW = "low"
26
+ MEDIUM = "medium"
27
+ HIGH = "high"
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class Policy:
32
+ """Caller-supplied spending policy. Every field is optional."""
33
+
34
+ max_per_call: Optional[float] = None
35
+ allowed_assets: Optional[List[str]] = None
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class ScreenRequest:
40
+ """A transaction an agent is about to perform."""
41
+
42
+ counterparty: str
43
+ endpoint: Optional[str] = None
44
+ amount: Optional[float] = None
45
+ asset: Optional[str] = None
46
+ policy: Optional[Policy] = None
47
+ # The payTo address the endpoint actually returned this request, if
48
+ # observed. Used to detect dynamic-payTo swaps.
49
+ observed_pay_to: Optional[str] = None
50
+ # Screening "temperature" in [0, 1]: 0 = permissive, 1 = paranoid. Default 0.3.
51
+ strictness: Optional[float] = None
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class ScreenResult:
56
+ """The outcome of screening a transaction."""
57
+
58
+ verdict: Verdict
59
+ trust_score: int # 0-100
60
+ confidence: Confidence
61
+ reasons: List[str] = field(default_factory=list)
62
+ policy_hits: List[str] = field(default_factory=list)
63
+ source: str = "lite" # "lite" | "hosted"
64
+
65
+ @property
66
+ def allowed(self) -> bool:
67
+ return self.verdict is Verdict.ALLOW