weaver-kernel 0.3.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_kernel/__init__.py +139 -0
- agent_kernel/drivers/__init__.py +7 -0
- agent_kernel/drivers/base.py +42 -0
- agent_kernel/drivers/http.py +125 -0
- agent_kernel/drivers/memory.py +171 -0
- agent_kernel/enums.py +32 -0
- agent_kernel/errors.py +67 -0
- agent_kernel/firewall/__init__.py +8 -0
- agent_kernel/firewall/budgets.py +26 -0
- agent_kernel/firewall/redaction.py +143 -0
- agent_kernel/firewall/summarize.py +115 -0
- agent_kernel/firewall/transform.py +251 -0
- agent_kernel/handles.py +219 -0
- agent_kernel/kernel.py +350 -0
- agent_kernel/models.py +230 -0
- agent_kernel/policy.py +213 -0
- agent_kernel/py.typed +0 -0
- agent_kernel/registry.py +124 -0
- agent_kernel/router.py +76 -0
- agent_kernel/tokens.py +349 -0
- agent_kernel/trace.py +46 -0
- weaver_kernel-0.3.0.dist-info/METADATA +378 -0
- weaver_kernel-0.3.0.dist-info/RECORD +25 -0
- weaver_kernel-0.3.0.dist-info/WHEEL +4 -0
- weaver_kernel-0.3.0.dist-info/licenses/LICENSE +201 -0
agent_kernel/__init__.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""agent-kernel: capability-based security kernel for AI agents.
|
|
2
|
+
|
|
3
|
+
Public API
|
|
4
|
+
----------
|
|
5
|
+
|
|
6
|
+
Core classes::
|
|
7
|
+
|
|
8
|
+
from agent_kernel import Kernel, CapabilityRegistry
|
|
9
|
+
from agent_kernel import Capability, Principal
|
|
10
|
+
from agent_kernel import SafetyClass, SensitivityTag
|
|
11
|
+
|
|
12
|
+
Token management::
|
|
13
|
+
|
|
14
|
+
from agent_kernel import HMACTokenProvider, CapabilityToken
|
|
15
|
+
|
|
16
|
+
Policy::
|
|
17
|
+
|
|
18
|
+
from agent_kernel import DefaultPolicyEngine
|
|
19
|
+
|
|
20
|
+
Firewall::
|
|
21
|
+
|
|
22
|
+
from agent_kernel import Firewall, Budgets
|
|
23
|
+
|
|
24
|
+
Handles & traces::
|
|
25
|
+
|
|
26
|
+
from agent_kernel import HandleStore, TraceStore
|
|
27
|
+
|
|
28
|
+
Errors::
|
|
29
|
+
|
|
30
|
+
from agent_kernel import (
|
|
31
|
+
AgentKernelError,
|
|
32
|
+
TokenExpired, TokenInvalid, TokenScopeError,
|
|
33
|
+
PolicyDenied, DriverError, FirewallError,
|
|
34
|
+
CapabilityNotFound, HandleNotFound, HandleExpired,
|
|
35
|
+
)
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from .drivers.base import Driver, ExecutionContext
|
|
39
|
+
from .drivers.http import HTTPDriver
|
|
40
|
+
from .drivers.memory import InMemoryDriver, make_billing_driver
|
|
41
|
+
from .enums import SafetyClass, SensitivityTag
|
|
42
|
+
from .errors import (
|
|
43
|
+
AgentKernelError,
|
|
44
|
+
CapabilityAlreadyRegistered,
|
|
45
|
+
CapabilityNotFound,
|
|
46
|
+
DriverError,
|
|
47
|
+
FirewallError,
|
|
48
|
+
HandleExpired,
|
|
49
|
+
HandleNotFound,
|
|
50
|
+
PolicyDenied,
|
|
51
|
+
TokenExpired,
|
|
52
|
+
TokenInvalid,
|
|
53
|
+
TokenRevoked,
|
|
54
|
+
TokenScopeError,
|
|
55
|
+
)
|
|
56
|
+
from .firewall.budgets import Budgets
|
|
57
|
+
from .firewall.transform import Firewall
|
|
58
|
+
from .handles import HandleStore
|
|
59
|
+
from .kernel import Kernel
|
|
60
|
+
from .models import (
|
|
61
|
+
ActionTrace,
|
|
62
|
+
Capability,
|
|
63
|
+
CapabilityGrant,
|
|
64
|
+
CapabilityRequest,
|
|
65
|
+
Frame,
|
|
66
|
+
Handle,
|
|
67
|
+
ImplementationRef,
|
|
68
|
+
PolicyDecision,
|
|
69
|
+
Principal,
|
|
70
|
+
Provenance,
|
|
71
|
+
RawResult,
|
|
72
|
+
ResponseMode,
|
|
73
|
+
RoutePlan,
|
|
74
|
+
)
|
|
75
|
+
from .policy import DefaultPolicyEngine
|
|
76
|
+
from .registry import CapabilityRegistry
|
|
77
|
+
from .router import StaticRouter
|
|
78
|
+
from .tokens import CapabilityToken, HMACTokenProvider
|
|
79
|
+
from .trace import TraceStore
|
|
80
|
+
|
|
81
|
+
__version__ = "0.1.0"
|
|
82
|
+
|
|
83
|
+
__all__ = [
|
|
84
|
+
# version
|
|
85
|
+
"__version__",
|
|
86
|
+
# kernel
|
|
87
|
+
"Kernel",
|
|
88
|
+
# registry
|
|
89
|
+
"CapabilityRegistry",
|
|
90
|
+
# models
|
|
91
|
+
"Capability",
|
|
92
|
+
"CapabilityGrant",
|
|
93
|
+
"CapabilityRequest",
|
|
94
|
+
"CapabilityToken",
|
|
95
|
+
"Frame",
|
|
96
|
+
"Handle",
|
|
97
|
+
"ImplementationRef",
|
|
98
|
+
"PolicyDecision",
|
|
99
|
+
"Principal",
|
|
100
|
+
"Provenance",
|
|
101
|
+
"RawResult",
|
|
102
|
+
"ResponseMode",
|
|
103
|
+
"RoutePlan",
|
|
104
|
+
"ActionTrace",
|
|
105
|
+
# enums
|
|
106
|
+
"SafetyClass",
|
|
107
|
+
"SensitivityTag",
|
|
108
|
+
# errors
|
|
109
|
+
"AgentKernelError",
|
|
110
|
+
"CapabilityAlreadyRegistered",
|
|
111
|
+
"CapabilityNotFound",
|
|
112
|
+
"DriverError",
|
|
113
|
+
"FirewallError",
|
|
114
|
+
"HandleExpired",
|
|
115
|
+
"HandleNotFound",
|
|
116
|
+
"PolicyDenied",
|
|
117
|
+
"TokenExpired",
|
|
118
|
+
"TokenInvalid",
|
|
119
|
+
"TokenRevoked",
|
|
120
|
+
"TokenScopeError",
|
|
121
|
+
# policy
|
|
122
|
+
"DefaultPolicyEngine",
|
|
123
|
+
# tokens
|
|
124
|
+
"HMACTokenProvider",
|
|
125
|
+
# router
|
|
126
|
+
"StaticRouter",
|
|
127
|
+
# drivers
|
|
128
|
+
"Driver",
|
|
129
|
+
"ExecutionContext",
|
|
130
|
+
"InMemoryDriver",
|
|
131
|
+
"HTTPDriver",
|
|
132
|
+
"make_billing_driver",
|
|
133
|
+
# firewall
|
|
134
|
+
"Firewall",
|
|
135
|
+
"Budgets",
|
|
136
|
+
# stores
|
|
137
|
+
"HandleStore",
|
|
138
|
+
"TraceStore",
|
|
139
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Base driver protocol and execution context."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Protocol
|
|
7
|
+
|
|
8
|
+
from ..models import RawResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(slots=True)
|
|
12
|
+
class ExecutionContext:
|
|
13
|
+
"""Runtime context passed to a driver when executing a capability."""
|
|
14
|
+
|
|
15
|
+
capability_id: str
|
|
16
|
+
principal_id: str
|
|
17
|
+
args: dict[str, Any] = field(default_factory=dict)
|
|
18
|
+
constraints: dict[str, Any] = field(default_factory=dict)
|
|
19
|
+
action_id: str = ""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Driver(Protocol):
|
|
23
|
+
"""Interface for capability execution drivers."""
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def driver_id(self) -> str:
|
|
27
|
+
"""Unique identifier for this driver instance."""
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
async def execute(self, ctx: ExecutionContext) -> RawResult:
|
|
31
|
+
"""Execute a capability and return a raw result.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
ctx: Execution context including capability ID, args, and constraints.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
The unfiltered :class:`RawResult` from the underlying system.
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
DriverError: If execution fails.
|
|
41
|
+
"""
|
|
42
|
+
...
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""HTTPDriver: execute capabilities against HTTP APIs using httpx."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from ..errors import DriverError
|
|
11
|
+
from ..models import RawResult
|
|
12
|
+
from .base import ExecutionContext
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class HTTPEndpoint:
|
|
17
|
+
"""Describes an HTTP endpoint for a capability operation."""
|
|
18
|
+
|
|
19
|
+
url: str
|
|
20
|
+
method: str = "GET"
|
|
21
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
22
|
+
timeout: float | None = None
|
|
23
|
+
"""Per-endpoint timeout in seconds. Falls back to the driver's ``default_timeout``."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class HTTPDriver:
|
|
27
|
+
"""A driver that invokes capabilities via HTTP using :mod:`httpx`.
|
|
28
|
+
|
|
29
|
+
Each operation must be registered with an :class:`HTTPEndpoint`.
|
|
30
|
+
The driver performs *synchronous* execution inside an async method by
|
|
31
|
+
using ``httpx.AsyncClient`` for proper async support.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
driver_id: str = "http",
|
|
37
|
+
*,
|
|
38
|
+
base_headers: dict[str, str] | None = None,
|
|
39
|
+
default_timeout: float = 30.0,
|
|
40
|
+
) -> None:
|
|
41
|
+
self._driver_id = driver_id
|
|
42
|
+
self._endpoints: dict[str, HTTPEndpoint] = {}
|
|
43
|
+
self._base_headers = base_headers or {}
|
|
44
|
+
self._default_timeout = default_timeout
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def driver_id(self) -> str:
|
|
48
|
+
"""Unique identifier for this driver."""
|
|
49
|
+
return self._driver_id
|
|
50
|
+
|
|
51
|
+
def register_endpoint(self, operation: str, endpoint: HTTPEndpoint) -> None:
|
|
52
|
+
"""Register an HTTP endpoint for an operation.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
operation: The operation name to handle.
|
|
56
|
+
endpoint: The :class:`HTTPEndpoint` configuration.
|
|
57
|
+
"""
|
|
58
|
+
self._endpoints[operation] = endpoint
|
|
59
|
+
|
|
60
|
+
async def execute(self, ctx: ExecutionContext) -> RawResult:
|
|
61
|
+
"""Execute an HTTP request for the given context.
|
|
62
|
+
|
|
63
|
+
The operation is resolved from ``ctx.args.get("operation")`` first,
|
|
64
|
+
then falls back to ``ctx.capability_id``.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
ctx: The execution context.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
:class:`RawResult` containing the parsed JSON response.
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
DriverError: If the endpoint is not registered or the request fails.
|
|
74
|
+
"""
|
|
75
|
+
operation = str(ctx.args.get("operation", ctx.capability_id))
|
|
76
|
+
endpoint = self._endpoints.get(operation)
|
|
77
|
+
if endpoint is None:
|
|
78
|
+
raise DriverError(
|
|
79
|
+
f"HTTPDriver '{self._driver_id}' has no endpoint for operation='{operation}'."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
headers = {**self._base_headers, **endpoint.headers}
|
|
83
|
+
params: dict[str, Any] = {}
|
|
84
|
+
json_body: dict[str, Any] | None = None
|
|
85
|
+
|
|
86
|
+
if endpoint.method.upper() == "GET":
|
|
87
|
+
params = {k: v for k, v in ctx.args.items() if k != "operation"}
|
|
88
|
+
else:
|
|
89
|
+
json_body = {k: v for k, v in ctx.args.items() if k != "operation"}
|
|
90
|
+
|
|
91
|
+
effective_timeout = (
|
|
92
|
+
endpoint.timeout if endpoint.timeout is not None else self._default_timeout
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
async with httpx.AsyncClient(headers=headers, timeout=effective_timeout) as client:
|
|
97
|
+
if endpoint.method.upper() == "GET":
|
|
98
|
+
response = await client.get(endpoint.url, params=params)
|
|
99
|
+
elif endpoint.method.upper() == "POST":
|
|
100
|
+
response = await client.post(endpoint.url, json=json_body)
|
|
101
|
+
elif endpoint.method.upper() == "PUT":
|
|
102
|
+
response = await client.put(endpoint.url, json=json_body)
|
|
103
|
+
elif endpoint.method.upper() == "DELETE":
|
|
104
|
+
response = await client.delete(endpoint.url, params=params)
|
|
105
|
+
else:
|
|
106
|
+
response = await client.request(
|
|
107
|
+
endpoint.method.upper(), endpoint.url, json=json_body
|
|
108
|
+
)
|
|
109
|
+
response.raise_for_status()
|
|
110
|
+
data: Any = response.json()
|
|
111
|
+
except httpx.HTTPStatusError as exc:
|
|
112
|
+
raise DriverError(
|
|
113
|
+
f"HTTPDriver '{self._driver_id}': HTTP {exc.response.status_code} "
|
|
114
|
+
f"from {endpoint.url}: {exc.response.text[:200]}"
|
|
115
|
+
) from exc
|
|
116
|
+
except httpx.RequestError as exc:
|
|
117
|
+
raise DriverError(
|
|
118
|
+
f"HTTPDriver '{self._driver_id}': Request to {endpoint.url} failed: {exc}"
|
|
119
|
+
) from exc
|
|
120
|
+
|
|
121
|
+
return RawResult(
|
|
122
|
+
capability_id=ctx.capability_id,
|
|
123
|
+
data=data,
|
|
124
|
+
metadata={"status_code": response.status_code, "url": endpoint.url},
|
|
125
|
+
)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""In-memory driver for testing and local demos."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import random
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ..errors import DriverError
|
|
10
|
+
from ..models import RawResult
|
|
11
|
+
from .base import ExecutionContext
|
|
12
|
+
|
|
13
|
+
Handler = Callable[[ExecutionContext], Any]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InMemoryDriver:
|
|
17
|
+
"""A driver that executes capabilities using registered Python callables.
|
|
18
|
+
|
|
19
|
+
This driver is primarily intended for unit tests, demos, and
|
|
20
|
+
local development where no external API is available.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, driver_id: str = "memory") -> None:
|
|
24
|
+
self._driver_id = driver_id
|
|
25
|
+
self._handlers: dict[str, Handler] = {}
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def driver_id(self) -> str:
|
|
29
|
+
"""Unique identifier for this driver."""
|
|
30
|
+
return self._driver_id
|
|
31
|
+
|
|
32
|
+
def register_handler(self, operation: str, handler: Handler) -> None:
|
|
33
|
+
"""Register a Python callable as the handler for an operation.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
operation: The operation name (must match ``ImplementationRef.operation``).
|
|
37
|
+
handler: A callable ``(ExecutionContext) -> Any`` that performs the operation.
|
|
38
|
+
"""
|
|
39
|
+
self._handlers[operation] = handler
|
|
40
|
+
|
|
41
|
+
async def execute(self, ctx: ExecutionContext) -> RawResult:
|
|
42
|
+
"""Execute a capability via its registered handler.
|
|
43
|
+
|
|
44
|
+
The operation is looked up from ``ctx.args.get("operation")`` first,
|
|
45
|
+
then falls back to ``ctx.capability_id``.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
ctx: The execution context.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
:class:`RawResult` wrapping the handler's return value.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
DriverError: If no handler is registered or the handler raises.
|
|
55
|
+
"""
|
|
56
|
+
operation = str(ctx.args.get("operation", ctx.capability_id))
|
|
57
|
+
handler = self._handlers.get(operation)
|
|
58
|
+
if handler is None:
|
|
59
|
+
raise DriverError(
|
|
60
|
+
f"InMemoryDriver '{self._driver_id}' has no handler for "
|
|
61
|
+
f"operation='{operation}'. Register one with register_handler()."
|
|
62
|
+
)
|
|
63
|
+
try:
|
|
64
|
+
data = handler(ctx)
|
|
65
|
+
except Exception as exc:
|
|
66
|
+
raise DriverError(f"Handler for operation='{operation}' raised: {exc}") from exc
|
|
67
|
+
return RawResult(capability_id=ctx.capability_id, data=data)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ── Billing dataset factory ───────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _make_billing_dataset(n: int = 200) -> list[dict[str, Any]]:
|
|
74
|
+
"""Generate a deterministic synthetic billing dataset.
|
|
75
|
+
|
|
76
|
+
Uses :class:`random.Random` seeded with ``42`` so the output is always
|
|
77
|
+
the same regardless of global random state.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
n: Number of invoice records to generate.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
A list of invoice dicts.
|
|
84
|
+
"""
|
|
85
|
+
rng = random.Random(42)
|
|
86
|
+
statuses = ["paid", "unpaid", "overdue"]
|
|
87
|
+
currencies = ["USD", "EUR", "GBP"]
|
|
88
|
+
first_names = ["Alice", "Bob", "Carol", "Dave", "Eve", "Frank", "Grace", "Hiro"]
|
|
89
|
+
last_names = ["Smith", "Jones", "Lee", "Brown", "Taylor", "Wilson", "Davis"]
|
|
90
|
+
|
|
91
|
+
records: list[dict[str, Any]] = []
|
|
92
|
+
for i in range(1, n + 1):
|
|
93
|
+
fname = rng.choice(first_names)
|
|
94
|
+
lname = rng.choice(last_names)
|
|
95
|
+
name = f"{fname} {lname}"
|
|
96
|
+
email = f"{fname.lower()}.{lname.lower()}{i}@example.com"
|
|
97
|
+
phone = f"+1-555-{rng.randint(1000, 9999)}"
|
|
98
|
+
amount = round(rng.uniform(10.0, 5000.0), 2)
|
|
99
|
+
currency = rng.choice(currencies)
|
|
100
|
+
status = rng.choice(statuses)
|
|
101
|
+
year = rng.randint(2023, 2024)
|
|
102
|
+
month = rng.randint(1, 12)
|
|
103
|
+
day = rng.randint(1, 28)
|
|
104
|
+
date_str = f"{year}-{month:02d}-{day:02d}"
|
|
105
|
+
line_items = [
|
|
106
|
+
{
|
|
107
|
+
"description": f"Item {j}",
|
|
108
|
+
"qty": rng.randint(1, 5),
|
|
109
|
+
"unit_price": round(rng.uniform(5.0, 500.0), 2),
|
|
110
|
+
}
|
|
111
|
+
for j in range(1, rng.randint(1, 4) + 1)
|
|
112
|
+
]
|
|
113
|
+
records.append(
|
|
114
|
+
{
|
|
115
|
+
"id": f"INV-{i:04d}",
|
|
116
|
+
"customer_name": name,
|
|
117
|
+
"email": email,
|
|
118
|
+
"phone": phone,
|
|
119
|
+
"amount": amount,
|
|
120
|
+
"currency": currency,
|
|
121
|
+
"status": status,
|
|
122
|
+
"date": date_str,
|
|
123
|
+
"line_items": line_items,
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
return records
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
BILLING_DATASET: list[dict[str, Any]] = _make_billing_dataset()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def make_billing_driver() -> InMemoryDriver:
|
|
133
|
+
"""Return an :class:`InMemoryDriver` pre-loaded with billing operations.
|
|
134
|
+
|
|
135
|
+
Operations:
|
|
136
|
+
- ``list_invoices`` — returns all invoices (filtered by ``status`` if provided).
|
|
137
|
+
- ``get_invoice`` — returns a single invoice by ``id``.
|
|
138
|
+
- ``summarize_spend`` — returns total spend per currency/status.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
A fully configured :class:`InMemoryDriver`.
|
|
142
|
+
"""
|
|
143
|
+
driver = InMemoryDriver(driver_id="billing")
|
|
144
|
+
|
|
145
|
+
def list_invoices(ctx: ExecutionContext) -> list[dict[str, Any]]:
|
|
146
|
+
status_filter = ctx.args.get("status")
|
|
147
|
+
data = BILLING_DATASET
|
|
148
|
+
if status_filter:
|
|
149
|
+
data = [r for r in data if r["status"] == status_filter]
|
|
150
|
+
return data
|
|
151
|
+
|
|
152
|
+
def get_invoice(ctx: ExecutionContext) -> dict[str, Any] | None:
|
|
153
|
+
invoice_id = ctx.args.get("id")
|
|
154
|
+
for record in BILLING_DATASET:
|
|
155
|
+
if record["id"] == invoice_id:
|
|
156
|
+
return record
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
def summarize_spend(ctx: ExecutionContext) -> dict[str, Any]:
|
|
160
|
+
totals: dict[str, dict[str, float]] = {}
|
|
161
|
+
for record in BILLING_DATASET:
|
|
162
|
+
cur = record["currency"]
|
|
163
|
+
sta = record["status"]
|
|
164
|
+
totals.setdefault(cur, {}).setdefault(sta, 0.0)
|
|
165
|
+
totals[cur][sta] = round(totals[cur][sta] + record["amount"], 2)
|
|
166
|
+
return {"totals": totals, "invoice_count": len(BILLING_DATASET)}
|
|
167
|
+
|
|
168
|
+
driver.register_handler("list_invoices", list_invoices)
|
|
169
|
+
driver.register_handler("get_invoice", get_invoice)
|
|
170
|
+
driver.register_handler("summarize_spend", summarize_spend)
|
|
171
|
+
return driver
|
agent_kernel/enums.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Enumerations for SafetyClass and SensitivityTag."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SafetyClass(str, Enum):
|
|
7
|
+
"""Classifies the danger level of a capability's side-effects."""
|
|
8
|
+
|
|
9
|
+
READ = "READ"
|
|
10
|
+
"""No side-effects; safe to retry."""
|
|
11
|
+
|
|
12
|
+
WRITE = "WRITE"
|
|
13
|
+
"""Mutates state; requires justification and writer/admin role."""
|
|
14
|
+
|
|
15
|
+
DESTRUCTIVE = "DESTRUCTIVE"
|
|
16
|
+
"""Irreversible; requires admin role."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SensitivityTag(str, Enum):
|
|
20
|
+
"""Tags data sensitivity requirements on a capability."""
|
|
21
|
+
|
|
22
|
+
NONE = "NONE"
|
|
23
|
+
"""No special sensitivity."""
|
|
24
|
+
|
|
25
|
+
PII = "PII"
|
|
26
|
+
"""Personally identifiable information (name, email, phone, SSN)."""
|
|
27
|
+
|
|
28
|
+
PCI = "PCI"
|
|
29
|
+
"""Payment card industry data (card numbers, CVV)."""
|
|
30
|
+
|
|
31
|
+
SECRETS = "SECRETS"
|
|
32
|
+
"""Credentials, API keys, tokens."""
|
agent_kernel/errors.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Custom exception hierarchy for agent-kernel."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AgentKernelError(Exception):
|
|
5
|
+
"""Base class for all agent-kernel errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# ── Token errors ──────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TokenExpired(AgentKernelError):
|
|
12
|
+
"""Raised when a token's ``expires_at`` is in the past."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TokenInvalid(AgentKernelError):
|
|
16
|
+
"""Raised when a token's HMAC signature does not verify."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TokenScopeError(AgentKernelError):
|
|
20
|
+
"""Raised when a token is used by the wrong principal or for the wrong capability."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TokenRevoked(AgentKernelError):
|
|
24
|
+
"""Raised when a revoked token is presented for verification."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ── Policy errors ─────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PolicyDenied(AgentKernelError):
|
|
31
|
+
"""Raised when the policy engine rejects a capability request."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ── Driver errors ─────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DriverError(AgentKernelError):
|
|
38
|
+
"""Raised when a driver fails to execute a capability."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ── Firewall errors ───────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class FirewallError(AgentKernelError):
|
|
45
|
+
"""Raised when the context firewall cannot transform a raw result."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ── Registry / lookup errors ──────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class CapabilityAlreadyRegistered(AgentKernelError):
|
|
52
|
+
"""Raised when a capability with the same ID is already registered."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class CapabilityNotFound(AgentKernelError):
|
|
56
|
+
"""Raised when a capability ID is not found in the registry."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ── Handle errors ─────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class HandleNotFound(AgentKernelError):
|
|
63
|
+
"""Raised when a handle ID is not found in the handle store."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class HandleExpired(AgentKernelError):
|
|
67
|
+
"""Raised when a handle's TTL has elapsed."""
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Budgets dataclass for the context firewall.
|
|
2
|
+
|
|
3
|
+
Canonical definition of :class:`Budgets`. Re-exported via
|
|
4
|
+
``agent_kernel.firewall`` and the top-level ``agent_kernel`` package.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(slots=True)
|
|
13
|
+
class Budgets:
|
|
14
|
+
"""Budget constraints enforced by the context firewall.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
max_rows: Maximum number of rows to include in a table preview.
|
|
18
|
+
max_fields: Maximum number of fields per row.
|
|
19
|
+
max_chars: Maximum total characters in the frame output.
|
|
20
|
+
max_depth: Maximum nesting depth when traversing dict/list values.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
max_rows: int = 50
|
|
24
|
+
max_fields: int = 20
|
|
25
|
+
max_chars: int = 4000
|
|
26
|
+
max_depth: int = 3
|