weaver-kernel 0.6.0__tar.gz → 0.7.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.
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/CHANGELOG.md +45 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/PKG-INFO +1 -1
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/docs/architecture.md +45 -2
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/pyproject.toml +1 -1
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/__init__.py +9 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/errors.py +18 -1
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/kernel.py +22 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/models.py +116 -1
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/policy.py +132 -20
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/policy_dsl.py +162 -4
- weaver_kernel-0.7.0/src/agent_kernel/policy_reasons.py +92 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/tests/test_kernel.py +65 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/tests/test_models.py +89 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/tests/test_policy.py +463 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/.claude/CLAUDE.md +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/.github/copilot-instructions.md +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/.github/workflows/ci.yml +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/.github/workflows/publish.yml +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/.gitignore +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/AGENTS.md +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/CONTRIBUTING.md +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/LICENSE +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/Makefile +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/README.md +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/RELEASE.md +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/docs/agent-context/architecture.md +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/docs/agent-context/invariants.md +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/docs/agent-context/lessons-learned.md +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/docs/agent-context/review-checklist.md +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/docs/agent-context/workflows.md +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/docs/capabilities.md +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/docs/context_firewall.md +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/docs/integrations.md +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/docs/security.md +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/examples/basic_cli.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/examples/billing_demo.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/examples/http_driver_demo.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/examples/policies/default.toml +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/examples/policies/default.yaml +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/adapters/__init__.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/adapters/_base.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/adapters/anthropic.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/adapters/openai.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/drivers/__init__.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/drivers/base.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/drivers/http.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/drivers/mcp.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/drivers/mcp_support.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/drivers/memory.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/enums.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/firewall/__init__.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/firewall/budget_manager.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/firewall/budgets.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/firewall/redaction.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/firewall/summarize.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/firewall/token_counting.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/firewall/transform.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/handles.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/py.typed +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/registry.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/router.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/tokens.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/src/agent_kernel/trace.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/tests/conftest.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/tests/test_adapters.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/tests/test_drivers.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/tests/test_firewall.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/tests/test_handles.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/tests/test_logging.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/tests/test_mcp_driver.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/tests/test_redaction.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/tests/test_registry.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/tests/test_router.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/tests/test_tokens.py +0 -0
- {weaver_kernel-0.6.0 → weaver_kernel-0.7.0}/tests/test_trace.py +0 -0
|
@@ -7,6 +7,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.0] - 2026-05-20
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Structured intent and scope metadata on `CapabilityRequest`: new optional
|
|
14
|
+
`intent: str | None` and `scope: dict[str, Any]` fields let policy engines
|
|
15
|
+
authorize based on machine-readable intent and scope alongside the existing
|
|
16
|
+
free-text `goal`. `DeclarativePolicyEngine` rules can match on these via new
|
|
17
|
+
`intent: [...]` and `scope: {key: value}` clauses in YAML/TOML policy files.
|
|
18
|
+
Intent-aware allow rules fail closed for legacy callers that don't set an
|
|
19
|
+
intent. (#72)
|
|
20
|
+
- Structured policy decision trace (`PolicyDecisionTrace` + `PolicyTraceStep`):
|
|
21
|
+
both built-in policy engines now attach a step-by-step trace to every
|
|
22
|
+
`PolicyDecision` (allow and deny paths). Each step records the rule
|
|
23
|
+
considered, the outcome (`matched`/`skipped`/`denied`/`allowed`/
|
|
24
|
+
`constraint_applied`), a human-readable detail, and — for terminal
|
|
25
|
+
steps — the stable reason code. Traces echo `intent` and `scope_keys`
|
|
26
|
+
(scope dimension names only — values redacted) from the request and contain
|
|
27
|
+
no raw argument values. `DryRunResult.policy_decision`
|
|
28
|
+
also carries a synthesized single-step trace. (#73)
|
|
29
|
+
- Stable machine-readable denial reason codes: new `DenialReason` and
|
|
30
|
+
`AllowReason` enums in `agent_kernel.policy_reasons` (also exported as
|
|
31
|
+
`from agent_kernel import DenialReason, AllowReason`). Every built-in
|
|
32
|
+
denial path on `DefaultPolicyEngine` and `DeclarativePolicyEngine` populates
|
|
33
|
+
`PolicyDecision.reason_code`, `DenialExplanation.reason_code`,
|
|
34
|
+
`FailedCondition.reason_code`, and `PolicyDenied.reason_code`. Tests should
|
|
35
|
+
assert on these codes instead of matching the human-readable `reason` /
|
|
36
|
+
`narrative` strings, which remain part of the API but may evolve for
|
|
37
|
+
clarity. Codes: `missing_role`, `missing_tenant_attribute`,
|
|
38
|
+
`missing_attribute`, `insufficient_justification`, `invalid_constraint`,
|
|
39
|
+
`rate_limited`, `no_matching_rule`, `explicit_deny_rule`,
|
|
40
|
+
`intent_not_allowed`, `scope_not_allowed`; allow-side: `default_policy_allow`,
|
|
41
|
+
`rule_allow`, `default_fallthrough_allow`. (#77)
|
|
42
|
+
- New public exports: `AllowReason`, `DenialReason`, `PolicyDecisionTrace`,
|
|
43
|
+
`PolicyTraceStep`.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
- `PolicyDecision` gained optional `reason_code: str | None` and
|
|
47
|
+
`trace: PolicyDecisionTrace | None` fields (both default `None` so
|
|
48
|
+
third-party engines that don't populate them keep working).
|
|
49
|
+
- `DenialExplanation` and `FailedCondition` gained optional `reason_code`
|
|
50
|
+
fields populated by both built-in engines on every denial path.
|
|
51
|
+
- `PolicyDenied(reason_code=...)` keyword argument: the exception now carries
|
|
52
|
+
a `reason_code` attribute so callers can branch on a stable code without
|
|
53
|
+
matching the human-readable message.
|
|
54
|
+
|
|
10
55
|
## [0.6.0] - 2026-05-19
|
|
11
56
|
|
|
12
57
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: weaver-kernel
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Capability-based security kernel for AI agents operating in large tool ecosystems
|
|
5
5
|
Project-URL: Homepage, https://github.com/dgenio/agent-kernel
|
|
6
6
|
Project-URL: Repository, https://github.com/dgenio/agent-kernel
|
|
@@ -54,11 +54,54 @@ Both built-in engines satisfy `ExplainingPolicyEngine`:
|
|
|
54
54
|
5. **SECRETS** — requires role `admin|secrets_reader` + `justification ≥ 15 chars`
|
|
55
55
|
6. **max_rows** — 50 (user), 500 (service)
|
|
56
56
|
7. **Rate limiting** — sliding-window per `(principal_id, capability_id)` (60 READ / 10 WRITE / 2 DESTRUCTIVE per 60s; service role gets 10×)
|
|
57
|
-
- **`DeclarativePolicyEngine`** — loads rules from a YAML or TOML file (or a plain dict). Supports `safety_class`, `sensitivity`, `roles`, `attributes`, and `
|
|
57
|
+
- **`DeclarativePolicyEngine`** — loads rules from a YAML or TOML file (or a plain dict). Supports `safety_class`, `sensitivity`, `roles`, `attributes`, `min_justification`, `intent`, and `scope` match conditions; `allow`/`deny` actions; per-rule `constraints` merged into the resulting `PolicyDecision`; configurable `default` action. Rules are evaluated top-down with first-match-wins. `pyyaml` and `tomli` are optional dependencies — `import agent_kernel` works without them; calling `from_yaml`/`from_toml` without the parser raises `PolicyConfigError` with an install hint.
|
|
58
|
+
|
|
59
|
+
#### Intent and scope on requests
|
|
60
|
+
|
|
61
|
+
`CapabilityRequest` carries optional structured metadata alongside its free-text `goal`:
|
|
62
|
+
|
|
63
|
+
- `intent: str | None` — a machine-readable label (e.g. `"customer_support_lookup"`).
|
|
64
|
+
- `scope: dict[str, Any]` — a small structured map (e.g. `{"region": "eu-west", "customer_id": "C-42"}`).
|
|
65
|
+
|
|
66
|
+
`DeclarativePolicyEngine` rules can match on these via top-level keys in `match`:
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
- name: support_eu_lookup
|
|
70
|
+
match:
|
|
71
|
+
safety_class: [READ]
|
|
72
|
+
intent: [customer_support_lookup]
|
|
73
|
+
scope: { region: "eu-west" }
|
|
74
|
+
action: allow
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Intent-aware rules fail closed: a request with `intent=None` never matches a rule that requires a specific intent. `scope: { key: "*" }` means "the key must be present with any value".
|
|
58
78
|
|
|
59
79
|
#### Denial explanations
|
|
60
80
|
|
|
61
|
-
`PolicyEngine.explain()` (when available) returns a structured `DenialExplanation` with `denied`, `rule_name`, a `failed_conditions: list[FailedCondition]` describing each missing condition with `required`/`actual`/`suggestion`, a `remediation` list,
|
|
81
|
+
`PolicyEngine.explain()` (when available) returns a structured `DenialExplanation` with `denied`, `rule_name`, a `failed_conditions: list[FailedCondition]` describing each missing condition with `required`/`actual`/`suggestion`/`reason_code`, a `remediation` list, a human-readable `narrative`, and a top-level `reason_code` (the code of the first failed condition). Engines collect all failing conditions (no short-circuit) so callers get the full picture. For `DeclarativePolicyEngine`, an explicit deny rule that fully matches is reported as the cause; partial-match deny rules are skipped during explanation so the surfaced advice is actionable rather than self-defeating.
|
|
82
|
+
|
|
83
|
+
#### Reason codes
|
|
84
|
+
|
|
85
|
+
Every `PolicyDecision`, `DenialExplanation`, `FailedCondition`, and `PolicyDenied` from the built-in engines carries a stable `reason_code`. Assert on these codes — not on the human-readable `reason` / `narrative` strings:
|
|
86
|
+
|
|
87
|
+
| Code (`DenialReason.*`) | When |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `missing_role` | Principal lacks a required role |
|
|
90
|
+
| `missing_tenant_attribute` | PII/PCI capability needs `tenant` attribute |
|
|
91
|
+
| `missing_attribute` | Declarative rule's required attribute absent or mismatched |
|
|
92
|
+
| `insufficient_justification` | Justification shorter than the minimum |
|
|
93
|
+
| `invalid_constraint` | Constraint value (e.g. `max_rows`) not parseable |
|
|
94
|
+
| `rate_limited` | Sliding-window rate limit exceeded |
|
|
95
|
+
| `no_matching_rule` | DSL: no rule matched + default `deny` |
|
|
96
|
+
| `explicit_deny_rule` | DSL: a `deny` rule matched fully |
|
|
97
|
+
| `intent_not_allowed` | DSL: `match.intent` rejected the request's intent |
|
|
98
|
+
| `scope_not_allowed` | DSL: `match.scope` rejected the request's scope |
|
|
99
|
+
|
|
100
|
+
Allow-side codes (`AllowReason.*`): `default_policy_allow`, `rule_allow`, `default_fallthrough_allow`, `token_verified`.
|
|
101
|
+
|
|
102
|
+
#### Decision trace
|
|
103
|
+
|
|
104
|
+
Every `PolicyDecision` from a built-in engine carries a `PolicyDecisionTrace` describing how the decision was reached: the engine name, the capability and principal IDs, the request's `intent` (echoed) and `scope_keys` (scope dimension names only — values are redacted), and an ordered list of `PolicyTraceStep` entries. Each step records the rule name, the outcome (`matched`/`skipped`/`denied`/`allowed`/`constraint_applied`), a human-readable detail, and — for terminal steps — the same stable `reason_code` carried on the decision. Traces are safe to log and serialize: they contain rule names, condition names, and codes only — never raw argument values.
|
|
62
105
|
|
|
63
106
|
#### Dry-run mode
|
|
64
107
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "weaver-kernel"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.0"
|
|
8
8
|
description = "Capability-based security kernel for AI agents operating in large tool ecosystems"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -16,6 +16,8 @@ Token management::
|
|
|
16
16
|
Policy::
|
|
17
17
|
|
|
18
18
|
from agent_kernel import DefaultPolicyEngine, DeclarativePolicyEngine
|
|
19
|
+
from agent_kernel import PolicyDecisionTrace, PolicyTraceStep
|
|
20
|
+
from agent_kernel import DenialReason, AllowReason
|
|
19
21
|
|
|
20
22
|
Firewall::
|
|
21
23
|
|
|
@@ -82,6 +84,8 @@ from .models import (
|
|
|
82
84
|
Handle,
|
|
83
85
|
ImplementationRef,
|
|
84
86
|
PolicyDecision,
|
|
87
|
+
PolicyDecisionTrace,
|
|
88
|
+
PolicyTraceStep,
|
|
85
89
|
Principal,
|
|
86
90
|
Provenance,
|
|
87
91
|
RawResult,
|
|
@@ -91,6 +95,7 @@ from .models import (
|
|
|
91
95
|
)
|
|
92
96
|
from .policy import DefaultPolicyEngine, ExplainingPolicyEngine, PolicyEngine
|
|
93
97
|
from .policy_dsl import DeclarativePolicyEngine, PolicyMatch, PolicyRule
|
|
98
|
+
from .policy_reasons import AllowReason, DenialReason
|
|
94
99
|
from .registry import CapabilityRegistry
|
|
95
100
|
from .router import StaticRouter
|
|
96
101
|
from .tokens import CapabilityToken, HMACTokenProvider
|
|
@@ -117,6 +122,8 @@ __all__ = [
|
|
|
117
122
|
"Handle",
|
|
118
123
|
"ImplementationRef",
|
|
119
124
|
"PolicyDecision",
|
|
125
|
+
"PolicyDecisionTrace",
|
|
126
|
+
"PolicyTraceStep",
|
|
120
127
|
"Principal",
|
|
121
128
|
"Provenance",
|
|
122
129
|
"RawResult",
|
|
@@ -145,8 +152,10 @@ __all__ = [
|
|
|
145
152
|
"TokenRevoked",
|
|
146
153
|
"TokenScopeError",
|
|
147
154
|
# policy
|
|
155
|
+
"AllowReason",
|
|
148
156
|
"DefaultPolicyEngine",
|
|
149
157
|
"DeclarativePolicyEngine",
|
|
158
|
+
"DenialReason",
|
|
150
159
|
"ExplainingPolicyEngine",
|
|
151
160
|
"PolicyEngine",
|
|
152
161
|
"PolicyMatch",
|
|
@@ -28,7 +28,24 @@ class TokenRevoked(AgentKernelError):
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
class PolicyDenied(AgentKernelError):
|
|
31
|
-
"""Raised when the policy engine rejects a capability request.
|
|
31
|
+
"""Raised when the policy engine rejects a capability request.
|
|
32
|
+
|
|
33
|
+
Carries an optional ``reason_code`` attribute holding a stable
|
|
34
|
+
:class:`~agent_kernel.policy_reasons.DenialReason` value so callers can
|
|
35
|
+
branch on it without matching the human-readable message:
|
|
36
|
+
|
|
37
|
+
.. code-block:: python
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
kernel.grant_capability(request, principal, justification="...")
|
|
41
|
+
except PolicyDenied as exc:
|
|
42
|
+
if exc.reason_code == DenialReason.MISSING_ROLE:
|
|
43
|
+
...
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, message: str, *, reason_code: str | None = None) -> None:
|
|
47
|
+
super().__init__(message)
|
|
48
|
+
self.reason_code: str | None = reason_code
|
|
32
49
|
|
|
33
50
|
|
|
34
51
|
class PolicyConfigError(AgentKernelError):
|
|
@@ -23,11 +23,14 @@ from .models import (
|
|
|
23
23
|
Frame,
|
|
24
24
|
Handle,
|
|
25
25
|
PolicyDecision,
|
|
26
|
+
PolicyDecisionTrace,
|
|
27
|
+
PolicyTraceStep,
|
|
26
28
|
Principal,
|
|
27
29
|
ResponseMode,
|
|
28
30
|
RoutePlan,
|
|
29
31
|
)
|
|
30
32
|
from .policy import DefaultPolicyEngine, PolicyEngine
|
|
33
|
+
from .policy_reasons import AllowReason
|
|
31
34
|
from .registry import CapabilityRegistry
|
|
32
35
|
from .router import Router, StaticRouter
|
|
33
36
|
from .tokens import CapabilityToken, HMACTokenProvider, TokenProvider
|
|
@@ -303,6 +306,23 @@ class Kernel:
|
|
|
303
306
|
SafetyClass.WRITE: "medium",
|
|
304
307
|
SafetyClass.DESTRUCTIVE: "high",
|
|
305
308
|
}
|
|
309
|
+
dry_run_trace = PolicyDecisionTrace(
|
|
310
|
+
engine="Kernel.invoke[dry_run]",
|
|
311
|
+
capability_id=token.capability_id,
|
|
312
|
+
principal_id=principal.principal_id,
|
|
313
|
+
intent=None,
|
|
314
|
+
scope_keys=[],
|
|
315
|
+
steps=[
|
|
316
|
+
PolicyTraceStep(
|
|
317
|
+
name="token_verified",
|
|
318
|
+
outcome="allowed",
|
|
319
|
+
detail="Token verified; original policy decision was at grant time.",
|
|
320
|
+
reason_code=str(AllowReason.TOKEN_VERIFIED),
|
|
321
|
+
)
|
|
322
|
+
],
|
|
323
|
+
final_outcome="allowed",
|
|
324
|
+
final_reason_code=str(AllowReason.TOKEN_VERIFIED),
|
|
325
|
+
)
|
|
306
326
|
return DryRunResult(
|
|
307
327
|
capability_id=token.capability_id,
|
|
308
328
|
principal_id=principal.principal_id,
|
|
@@ -310,6 +330,8 @@ class Kernel:
|
|
|
310
330
|
allowed=True,
|
|
311
331
|
reason="Token verified. Policy was evaluated at grant time.",
|
|
312
332
|
constraints=dict(token.constraints),
|
|
333
|
+
reason_code=str(AllowReason.TOKEN_VERIFIED),
|
|
334
|
+
trace=dry_run_trace,
|
|
313
335
|
),
|
|
314
336
|
driver_id=driver_id,
|
|
315
337
|
operation=operation,
|
|
@@ -134,6 +134,22 @@ class CapabilityRequest:
|
|
|
134
134
|
constraints: dict[str, Any] = field(default_factory=dict)
|
|
135
135
|
"""Optional execution constraints (e.g. ``{"max_rows": 10}``)."""
|
|
136
136
|
|
|
137
|
+
intent: str | None = None
|
|
138
|
+
"""Structured intent label (e.g. ``"customer_support_lookup"``).
|
|
139
|
+
|
|
140
|
+
Free-text :attr:`goal` is still required for human-readable audit; ``intent``
|
|
141
|
+
is the machine-readable counterpart that declarative policies can match
|
|
142
|
+
on directly without parsing the goal. See :class:`PolicyMatch.intent`.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
scope: dict[str, Any] = field(default_factory=dict)
|
|
146
|
+
"""Structured scope metadata describing what the request narrows to.
|
|
147
|
+
|
|
148
|
+
Examples: ``{"region": "eu-west"}``, ``{"customer_id": "C-42"}``. Policies
|
|
149
|
+
can deny a capability invocation that is technically allowed but unsafe
|
|
150
|
+
for a particular scope. See :class:`PolicyMatch.scope`.
|
|
151
|
+
"""
|
|
152
|
+
|
|
137
153
|
|
|
138
154
|
@dataclass(slots=True)
|
|
139
155
|
class Principal:
|
|
@@ -149,6 +165,77 @@ class Principal:
|
|
|
149
165
|
"""Arbitrary attributes, e.g. ``{"tenant": "acme"}``."""
|
|
150
166
|
|
|
151
167
|
|
|
168
|
+
@dataclass(slots=True)
|
|
169
|
+
class PolicyTraceStep:
|
|
170
|
+
"""A single step recorded while a policy engine evaluated a request.
|
|
171
|
+
|
|
172
|
+
Steps describe what the engine considered, in order — which rule it
|
|
173
|
+
examined, whether it matched, what condition (if any) failed, and what
|
|
174
|
+
constraint (if any) was applied. Steps never contain raw argument values
|
|
175
|
+
from the caller; they reference fields and IDs only.
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
name: str
|
|
179
|
+
"""Short label for the step (e.g. ``"safety_class:WRITE"`` or rule name)."""
|
|
180
|
+
|
|
181
|
+
outcome: Literal["matched", "skipped", "denied", "allowed", "constraint_applied"]
|
|
182
|
+
"""What happened at this step.
|
|
183
|
+
|
|
184
|
+
- ``"matched"``: a rule's match clause matched and evaluation continues.
|
|
185
|
+
- ``"skipped"``: the step did not apply (e.g. wildcard, wrong safety class).
|
|
186
|
+
- ``"denied"``: this step produced the final denial.
|
|
187
|
+
- ``"allowed"``: this step produced the final allow.
|
|
188
|
+
- ``"constraint_applied"``: the step merged a constraint into the decision.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
detail: str = ""
|
|
192
|
+
"""Human-readable detail, e.g. ``"role 'writer' required, principal had ['reader']"``."""
|
|
193
|
+
|
|
194
|
+
reason_code: str | None = None
|
|
195
|
+
"""For ``"denied"`` steps, the :class:`~agent_kernel.policy_reasons.DenialReason`.
|
|
196
|
+
For ``"allowed"`` steps, the :class:`~agent_kernel.policy_reasons.AllowReason`.
|
|
197
|
+
``None`` for ``"matched"``, ``"skipped"``, and ``"constraint_applied"`` steps.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@dataclass(slots=True)
|
|
202
|
+
class PolicyDecisionTrace:
|
|
203
|
+
"""Structured trace of how a :class:`PolicyDecision` was reached.
|
|
204
|
+
|
|
205
|
+
The trace lists every step the policy engine took, in order, so callers
|
|
206
|
+
can audit which rule matched, which conditions failed, and which
|
|
207
|
+
constraints were applied. The trace must not contain raw argument
|
|
208
|
+
values — only field names, role names, attribute names, rule names, and
|
|
209
|
+
safe IDs — so it is safe to serialize and log.
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
engine: str
|
|
213
|
+
"""Engine identifier (e.g. ``"DefaultPolicyEngine"``)."""
|
|
214
|
+
|
|
215
|
+
capability_id: str
|
|
216
|
+
"""The capability that was being evaluated."""
|
|
217
|
+
|
|
218
|
+
principal_id: str
|
|
219
|
+
"""The principal the decision was made for."""
|
|
220
|
+
|
|
221
|
+
intent: str | None
|
|
222
|
+
"""Echoed :attr:`CapabilityRequest.intent` (may be ``None``)."""
|
|
223
|
+
|
|
224
|
+
scope_keys: list[str] = field(default_factory=list)
|
|
225
|
+
"""Scope dimension names present on the request (values redacted for safety)."""
|
|
226
|
+
|
|
227
|
+
steps: list[PolicyTraceStep] = field(default_factory=list)
|
|
228
|
+
"""Ordered list of evaluation steps."""
|
|
229
|
+
|
|
230
|
+
final_outcome: Literal["allowed", "denied"] = "denied"
|
|
231
|
+
"""The decision the engine reached."""
|
|
232
|
+
|
|
233
|
+
final_reason_code: str | None = None
|
|
234
|
+
"""The :class:`~agent_kernel.policy_reasons.AllowReason` or
|
|
235
|
+
:class:`~agent_kernel.policy_reasons.DenialReason` for the final outcome.
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
|
|
152
239
|
@dataclass(slots=True)
|
|
153
240
|
class PolicyDecision:
|
|
154
241
|
"""Result of a policy engine evaluation."""
|
|
@@ -157,11 +244,27 @@ class PolicyDecision:
|
|
|
157
244
|
"""``True`` if the request is permitted."""
|
|
158
245
|
|
|
159
246
|
reason: str
|
|
160
|
-
"""Human-readable explanation.
|
|
247
|
+
"""Human-readable explanation. Wording may evolve; assert on
|
|
248
|
+
:attr:`reason_code` for stable behavior."""
|
|
161
249
|
|
|
162
250
|
constraints: dict[str, Any] = field(default_factory=dict)
|
|
163
251
|
"""Any additional constraints imposed by the policy (e.g. ``max_rows``)."""
|
|
164
252
|
|
|
253
|
+
reason_code: str | None = None
|
|
254
|
+
"""Stable machine-readable code (typically a :class:`~agent_kernel.policy_reasons.AllowReason`
|
|
255
|
+
or :class:`~agent_kernel.policy_reasons.DenialReason` value).
|
|
256
|
+
|
|
257
|
+
Use this for assertions, metrics, and UI mapping. ``None`` only when an
|
|
258
|
+
out-of-tree policy engine has not populated it.
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
trace: PolicyDecisionTrace | None = None
|
|
262
|
+
"""Structured trace of how this decision was reached.
|
|
263
|
+
|
|
264
|
+
Populated by both built-in engines on allow and deny paths. ``None`` for
|
|
265
|
+
third-party engines that don't produce a trace.
|
|
266
|
+
"""
|
|
267
|
+
|
|
165
268
|
|
|
166
269
|
@dataclass(slots=True)
|
|
167
270
|
class CapabilityGrant:
|
|
@@ -306,6 +409,12 @@ class FailedCondition:
|
|
|
306
409
|
suggestion: str
|
|
307
410
|
"""Actionable remediation hint."""
|
|
308
411
|
|
|
412
|
+
reason_code: str | None = None
|
|
413
|
+
"""Stable machine-readable code (a :class:`~agent_kernel.policy_reasons.DenialReason` value).
|
|
414
|
+
Use this for assertions instead of matching the human-readable
|
|
415
|
+
:attr:`suggestion` string.
|
|
416
|
+
"""
|
|
417
|
+
|
|
309
418
|
|
|
310
419
|
@dataclass(slots=True)
|
|
311
420
|
class DenialExplanation:
|
|
@@ -326,6 +435,12 @@ class DenialExplanation:
|
|
|
326
435
|
narrative: str
|
|
327
436
|
"""Human-readable single-sentence summary."""
|
|
328
437
|
|
|
438
|
+
reason_code: str | None = None
|
|
439
|
+
"""Primary :class:`~agent_kernel.policy_reasons.DenialReason` for the denial
|
|
440
|
+
(typically the code of the first :class:`FailedCondition`). ``None`` on the
|
|
441
|
+
allow path (``denied=False``).
|
|
442
|
+
"""
|
|
443
|
+
|
|
329
444
|
|
|
330
445
|
# ── Dry-run ───────────────────────────────────────────────────────────────────
|
|
331
446
|
|