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.
@@ -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,7 @@
1
+ """Driver sub-package exports."""
2
+
3
+ from .base import Driver, ExecutionContext
4
+ from .http import HTTPDriver
5
+ from .memory import InMemoryDriver
6
+
7
+ __all__ = ["Driver", "ExecutionContext", "HTTPDriver", "InMemoryDriver"]
@@ -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,8 @@
1
+ """Firewall sub-package exports."""
2
+
3
+ from .budgets import Budgets
4
+ from .redaction import redact
5
+ from .summarize import summarize
6
+ from .transform import Firewall
7
+
8
+ __all__ = ["Budgets", "Firewall", "redact", "summarize"]
@@ -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