veefore-py 1.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.
- veefore_py-1.1.0/PKG-INFO +57 -0
- veefore_py-1.1.0/README.md +35 -0
- veefore_py-1.1.0/pyproject.toml +45 -0
- veefore_py-1.1.0/setup.cfg +4 -0
- veefore_py-1.1.0/src/veefore/__init__.py +73 -0
- veefore_py-1.1.0/src/veefore/assertion.py +119 -0
- veefore_py-1.1.0/src/veefore/client.py +639 -0
- veefore_py-1.1.0/src/veefore/errors.py +146 -0
- veefore_py-1.1.0/src/veefore/guard.py +232 -0
- veefore_py-1.1.0/src/veefore/models.py +131 -0
- veefore_py-1.1.0/src/veefore/protect.py +151 -0
- veefore_py-1.1.0/src/veefore/py.typed +0 -0
- veefore_py-1.1.0/src/veefore_py.egg-info/PKG-INFO +57 -0
- veefore_py-1.1.0/src/veefore_py.egg-info/SOURCES.txt +19 -0
- veefore_py-1.1.0/src/veefore_py.egg-info/dependency_links.txt +1 -0
- veefore_py-1.1.0/src/veefore_py.egg-info/requires.txt +5 -0
- veefore_py-1.1.0/src/veefore_py.egg-info/top_level.txt +1 -0
- veefore_py-1.1.0/tests/test_assertion.py +187 -0
- veefore_py-1.1.0/tests/test_client.py +604 -0
- veefore_py-1.1.0/tests/test_guard.py +266 -0
- veefore_py-1.1.0/tests/test_protect.py +176 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: veefore-py
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: VEEFORE SDK for Python agent runtimes.
|
|
5
|
+
Author: VEEFORE
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://veefore.com
|
|
8
|
+
Project-URL: Documentation, https://veefore.com/docs/build/python-sdk
|
|
9
|
+
Project-URL: Repository, https://github.com/veefore/veefore
|
|
10
|
+
Keywords: agent,identity,policy,authorization,enforcement
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Security
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: cryptography>=41.0.0
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
22
|
+
|
|
23
|
+
# veefore-py
|
|
24
|
+
|
|
25
|
+
Identity, policy and enforcement for AI agents. Every protected tool call is
|
|
26
|
+
decided by the Veefore gateway before your code runs it.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install veefore-py
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The package is `veefore-py`; the module is `veefore`. The quickest way to
|
|
33
|
+
integrate is the CLI, which installs this package with pip, uv or poetry:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install -g veefore
|
|
37
|
+
veefore # sign in, scan your project, set it up, wrap your tools
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or by hand:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from veefore import guard
|
|
44
|
+
|
|
45
|
+
@tool
|
|
46
|
+
@guard("refund_customer")
|
|
47
|
+
def refund_customer(amount: int) -> str:
|
|
48
|
+
...
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`@guard` asks Veefore first and runs the function only if the call is
|
|
52
|
+
permitted. A refusal raises `VeeforeBlockedError` (or
|
|
53
|
+
`VeeforeApprovalRequiredError` when a human must decide). Put it directly above
|
|
54
|
+
`def`, below your framework's decorator.
|
|
55
|
+
|
|
56
|
+
Configuration is read from the environment variables `veefore init` writes to
|
|
57
|
+
`.env.local`. Requires Python 3.9 or newer.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# veefore-py
|
|
2
|
+
|
|
3
|
+
Identity, policy and enforcement for AI agents. Every protected tool call is
|
|
4
|
+
decided by the Veefore gateway before your code runs it.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install veefore-py
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
The package is `veefore-py`; the module is `veefore`. The quickest way to
|
|
11
|
+
integrate is the CLI, which installs this package with pip, uv or poetry:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g veefore
|
|
15
|
+
veefore # sign in, scan your project, set it up, wrap your tools
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or by hand:
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from veefore import guard
|
|
22
|
+
|
|
23
|
+
@tool
|
|
24
|
+
@guard("refund_customer")
|
|
25
|
+
def refund_customer(amount: int) -> str:
|
|
26
|
+
...
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`@guard` asks Veefore first and runs the function only if the call is
|
|
30
|
+
permitted. A refusal raises `VeeforeBlockedError` (or
|
|
31
|
+
`VeeforeApprovalRequiredError` when a human must decide). Put it directly above
|
|
32
|
+
`def`, below your framework's decorator.
|
|
33
|
+
|
|
34
|
+
Configuration is read from the environment variables `veefore init` writes to
|
|
35
|
+
`.env.local`. Requires Python 3.9 or newer.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "veefore-py"
|
|
7
|
+
version = "1.1.0"
|
|
8
|
+
description = "VEEFORE SDK for Python agent runtimes."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "Apache-2.0" }
|
|
12
|
+
authors = [{ name = "VEEFORE" }]
|
|
13
|
+
keywords = ["agent", "identity", "policy", "authorization", "enforcement"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 5 - Production/Stable",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: Apache Software License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Security",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
# Only the standard library plus `cryptography` for Ed25519. No HTTP client
|
|
23
|
+
# dependency: urllib is used so the SDK cannot conflict with the host app.
|
|
24
|
+
dependencies = ["cryptography>=41.0.0"]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
dev = ["pytest>=7.4", "pytest-asyncio>=0.23"]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://veefore.com"
|
|
31
|
+
Documentation = "https://veefore.com/docs/build/python-sdk"
|
|
32
|
+
Repository = "https://github.com/veefore/veefore"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
where = ["src"]
|
|
36
|
+
|
|
37
|
+
# PEP 561: ships the marker that tells mypy and pyright this package's
|
|
38
|
+
# annotations are real, so `guard` keeps its types in a user's project instead
|
|
39
|
+
# of degrading to Any.
|
|
40
|
+
[tool.setuptools.package-data]
|
|
41
|
+
veefore = ["py.typed"]
|
|
42
|
+
|
|
43
|
+
[tool.pytest.ini_options]
|
|
44
|
+
testpaths = ["tests"]
|
|
45
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Veefore SDK for Python.
|
|
2
|
+
|
|
3
|
+
The SDK is a convenience layer, never a security boundary. Every protected
|
|
4
|
+
action is decided server-side by the gateway on every call. Nothing in this
|
|
5
|
+
package can grant an agent authority it does not already have, and nothing here
|
|
6
|
+
caches a permission.
|
|
7
|
+
|
|
8
|
+
from veefore import protect
|
|
9
|
+
|
|
10
|
+
agent = protect(my_agent)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from .assertion import (
|
|
16
|
+
ASSERTION_TTL_SECONDS,
|
|
17
|
+
canonical_json,
|
|
18
|
+
create_assertion,
|
|
19
|
+
generate_nonce,
|
|
20
|
+
)
|
|
21
|
+
from .client import SDK_NAME, SDK_VERSION, VeeforeClient
|
|
22
|
+
from .errors import (
|
|
23
|
+
VeeforeApprovalDeniedError,
|
|
24
|
+
VeeforeApprovalRequiredError,
|
|
25
|
+
VeeforeAuthenticationError,
|
|
26
|
+
VeeforeBlockedError,
|
|
27
|
+
VeeforeConfigurationError,
|
|
28
|
+
VeeforeConnectionError,
|
|
29
|
+
VeeforeError,
|
|
30
|
+
VeeforeRateLimitError,
|
|
31
|
+
)
|
|
32
|
+
from .models import (
|
|
33
|
+
ActionDecision,
|
|
34
|
+
ActionRequest,
|
|
35
|
+
ApprovalStatus,
|
|
36
|
+
HandshakeResult,
|
|
37
|
+
StageResult,
|
|
38
|
+
)
|
|
39
|
+
from .guard import guard, guard_client, reset_guard_client
|
|
40
|
+
from .protect import ProtectedAgent, config_from_env, create_client, protect
|
|
41
|
+
|
|
42
|
+
__version__ = SDK_VERSION
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"protect",
|
|
46
|
+
"guard",
|
|
47
|
+
"guard_client",
|
|
48
|
+
"reset_guard_client",
|
|
49
|
+
"create_client",
|
|
50
|
+
"config_from_env",
|
|
51
|
+
"ProtectedAgent",
|
|
52
|
+
"VeeforeClient",
|
|
53
|
+
"ActionRequest",
|
|
54
|
+
"ActionDecision",
|
|
55
|
+
"ApprovalStatus",
|
|
56
|
+
"HandshakeResult",
|
|
57
|
+
"StageResult",
|
|
58
|
+
"VeeforeError",
|
|
59
|
+
"VeeforeAuthenticationError",
|
|
60
|
+
"VeeforeBlockedError",
|
|
61
|
+
"VeeforeApprovalRequiredError",
|
|
62
|
+
"VeeforeApprovalDeniedError",
|
|
63
|
+
"VeeforeConnectionError",
|
|
64
|
+
"VeeforeConfigurationError",
|
|
65
|
+
"VeeforeRateLimitError",
|
|
66
|
+
"create_assertion",
|
|
67
|
+
"generate_nonce",
|
|
68
|
+
"canonical_json",
|
|
69
|
+
"ASSERTION_TTL_SECONDS",
|
|
70
|
+
"SDK_NAME",
|
|
71
|
+
"SDK_VERSION",
|
|
72
|
+
"__version__",
|
|
73
|
+
]
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Agent identity assertions.
|
|
2
|
+
|
|
3
|
+
A compact JWS (EdDSA / Ed25519) proving possession of the agent's private
|
|
4
|
+
identity key, bound to the agent id, the gateway audience, a single-use nonce
|
|
5
|
+
and a short expiry.
|
|
6
|
+
|
|
7
|
+
The private key never leaves this module: it is not logged, not placed in a
|
|
8
|
+
request body, and not written to disk.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import base64
|
|
14
|
+
import json
|
|
15
|
+
import secrets
|
|
16
|
+
from datetime import datetime, timedelta, timezone
|
|
17
|
+
from typing import Any, Optional, Tuple
|
|
18
|
+
|
|
19
|
+
from cryptography.hazmat.primitives import serialization
|
|
20
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
21
|
+
|
|
22
|
+
from .errors import VeeforeConfigurationError
|
|
23
|
+
|
|
24
|
+
__all__ = ["ASSERTION_TTL_SECONDS", "canonical_json", "create_assertion", "generate_nonce"]
|
|
25
|
+
|
|
26
|
+
ASSERTION_TTL_SECONDS = 120
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _b64url(data: bytes) -> str:
|
|
30
|
+
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def canonical_json(value: Any) -> str:
|
|
34
|
+
"""RFC 8785-style canonical JSON: sorted keys, no insignificant whitespace.
|
|
35
|
+
|
|
36
|
+
Must match the server's canonicalisation byte for byte or signatures fail.
|
|
37
|
+
"""
|
|
38
|
+
if value is None:
|
|
39
|
+
return "null"
|
|
40
|
+
if isinstance(value, bool):
|
|
41
|
+
return "true" if value else "false"
|
|
42
|
+
if isinstance(value, (int, float)):
|
|
43
|
+
return json.dumps(value)
|
|
44
|
+
if isinstance(value, str):
|
|
45
|
+
return json.dumps(value, ensure_ascii=False)
|
|
46
|
+
if isinstance(value, (list, tuple)):
|
|
47
|
+
return "[" + ",".join(canonical_json(item) for item in value) + "]"
|
|
48
|
+
if isinstance(value, dict):
|
|
49
|
+
items = sorted((k, v) for k, v in value.items() if v is not None)
|
|
50
|
+
return (
|
|
51
|
+
"{"
|
|
52
|
+
+ ",".join(
|
|
53
|
+
f"{json.dumps(k, ensure_ascii=False)}:{canonical_json(v)}" for k, v in items
|
|
54
|
+
)
|
|
55
|
+
+ "}"
|
|
56
|
+
)
|
|
57
|
+
return "null"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def generate_nonce() -> str:
|
|
61
|
+
return secrets.token_urlsafe(18)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _load_private_key(base64_pkcs8: str) -> Ed25519PrivateKey:
|
|
65
|
+
try:
|
|
66
|
+
der = base64.b64decode(base64_pkcs8, validate=False)
|
|
67
|
+
key = serialization.load_der_private_key(der, password=None)
|
|
68
|
+
except Exception as error: # noqa: BLE001 - surfaced as a config error
|
|
69
|
+
raise VeeforeConfigurationError(
|
|
70
|
+
"The agent private key could not be parsed. "
|
|
71
|
+
"Expected base64-encoded PKCS#8 Ed25519 material."
|
|
72
|
+
) from error
|
|
73
|
+
if not isinstance(key, Ed25519PrivateKey):
|
|
74
|
+
raise VeeforeConfigurationError(
|
|
75
|
+
"The agent private key is not an Ed25519 key. Veefore agent identities use Ed25519."
|
|
76
|
+
)
|
|
77
|
+
return key
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def create_assertion(
|
|
81
|
+
*,
|
|
82
|
+
agent_id: str,
|
|
83
|
+
audience: str,
|
|
84
|
+
private_key: str,
|
|
85
|
+
key_id: str,
|
|
86
|
+
purpose: str = "agent_authentication",
|
|
87
|
+
nonce: Optional[str] = None,
|
|
88
|
+
ttl_seconds: int = ASSERTION_TTL_SECONDS,
|
|
89
|
+
now: Optional[datetime] = None,
|
|
90
|
+
) -> Tuple[str, str]:
|
|
91
|
+
"""Returns ``(assertion, nonce)``."""
|
|
92
|
+
issued_at = now or datetime.now(timezone.utc)
|
|
93
|
+
actual_nonce = nonce or generate_nonce()
|
|
94
|
+
|
|
95
|
+
header = {"typ": "veefore-assertion+jwt", "alg": "EdDSA", "kid": key_id}
|
|
96
|
+
payload = {
|
|
97
|
+
"sub": agent_id,
|
|
98
|
+
"aud": audience,
|
|
99
|
+
"purpose": purpose,
|
|
100
|
+
"nonce": actual_nonce,
|
|
101
|
+
"iat": _iso(issued_at),
|
|
102
|
+
"exp": _iso(issued_at + timedelta(seconds=ttl_seconds)),
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
signing_input = (
|
|
106
|
+
_b64url(json.dumps(header, separators=(",", ":")).encode("utf-8"))
|
|
107
|
+
+ "."
|
|
108
|
+
+ _b64url(canonical_json(payload).encode("utf-8"))
|
|
109
|
+
)
|
|
110
|
+
key = _load_private_key(private_key)
|
|
111
|
+
signature = key.sign(signing_input.encode("ascii"))
|
|
112
|
+
return f"{signing_input}.{_b64url(signature)}", actual_nonce
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _iso(value: datetime) -> str:
|
|
116
|
+
"""ISO-8601 with a trailing ``Z``, matching the server's format."""
|
|
117
|
+
return value.astimezone(timezone.utc).isoformat(timespec="milliseconds").replace(
|
|
118
|
+
"+00:00", "Z"
|
|
119
|
+
)
|