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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ )