weaver-kernel 0.6.0__tar.gz → 0.8.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.
Files changed (80) hide show
  1. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/.github/workflows/ci.yml +1 -0
  2. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/CHANGELOG.md +113 -0
  3. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/Makefile +1 -0
  4. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/PKG-INFO +42 -1
  5. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/README.md +41 -0
  6. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/docs/architecture.md +52 -4
  7. weaver_kernel-0.8.0/docs/security.md +95 -0
  8. weaver_kernel-0.8.0/docs/tutorial.md +293 -0
  9. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/examples/basic_cli.py +1 -0
  10. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/examples/billing_demo.py +2 -0
  11. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/examples/http_driver_demo.py +1 -0
  12. weaver_kernel-0.8.0/examples/tutorial.py +235 -0
  13. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/pyproject.toml +1 -1
  14. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/__init__.py +11 -0
  15. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/enums.py +8 -0
  16. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/errors.py +37 -1
  17. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/handles.py +125 -8
  18. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/kernel.py +77 -4
  19. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/models.py +134 -2
  20. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/policy.py +209 -20
  21. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/policy_dsl.py +162 -4
  22. weaver_kernel-0.8.0/src/agent_kernel/policy_reasons.py +117 -0
  23. weaver_kernel-0.8.0/tests/test_firewall_boundary.py +274 -0
  24. weaver_kernel-0.8.0/tests/test_handles.py +320 -0
  25. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/tests/test_kernel.py +68 -1
  26. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/tests/test_logging.py +2 -2
  27. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/tests/test_models.py +89 -0
  28. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/tests/test_policy.py +574 -0
  29. weaver_kernel-0.6.0/docs/security.md +0 -42
  30. weaver_kernel-0.6.0/tests/test_handles.py +0 -147
  31. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/.claude/CLAUDE.md +0 -0
  32. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/.github/copilot-instructions.md +0 -0
  33. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/.github/workflows/publish.yml +0 -0
  34. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/.gitignore +0 -0
  35. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/AGENTS.md +0 -0
  36. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/CONTRIBUTING.md +0 -0
  37. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/LICENSE +0 -0
  38. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/RELEASE.md +0 -0
  39. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/docs/agent-context/architecture.md +0 -0
  40. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/docs/agent-context/invariants.md +0 -0
  41. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/docs/agent-context/lessons-learned.md +0 -0
  42. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/docs/agent-context/review-checklist.md +0 -0
  43. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/docs/agent-context/workflows.md +0 -0
  44. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/docs/capabilities.md +0 -0
  45. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/docs/context_firewall.md +0 -0
  46. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/docs/integrations.md +0 -0
  47. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/examples/policies/default.toml +0 -0
  48. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/examples/policies/default.yaml +0 -0
  49. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/adapters/__init__.py +0 -0
  50. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/adapters/_base.py +0 -0
  51. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/adapters/anthropic.py +0 -0
  52. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/adapters/openai.py +0 -0
  53. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/drivers/__init__.py +0 -0
  54. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/drivers/base.py +0 -0
  55. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/drivers/http.py +0 -0
  56. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/drivers/mcp.py +0 -0
  57. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/drivers/mcp_support.py +0 -0
  58. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/drivers/memory.py +0 -0
  59. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/firewall/__init__.py +0 -0
  60. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/firewall/budget_manager.py +0 -0
  61. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/firewall/budgets.py +0 -0
  62. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/firewall/redaction.py +0 -0
  63. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/firewall/summarize.py +0 -0
  64. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/firewall/token_counting.py +0 -0
  65. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/firewall/transform.py +0 -0
  66. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/py.typed +0 -0
  67. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/registry.py +0 -0
  68. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/router.py +0 -0
  69. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/tokens.py +0 -0
  70. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/src/agent_kernel/trace.py +0 -0
  71. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/tests/conftest.py +0 -0
  72. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/tests/test_adapters.py +0 -0
  73. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/tests/test_drivers.py +0 -0
  74. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/tests/test_firewall.py +0 -0
  75. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/tests/test_mcp_driver.py +0 -0
  76. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/tests/test_redaction.py +0 -0
  77. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/tests/test_registry.py +0 -0
  78. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/tests/test_router.py +0 -0
  79. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/tests/test_tokens.py +0 -0
  80. {weaver_kernel-0.6.0 → weaver_kernel-0.8.0}/tests/test_trace.py +0 -0
@@ -45,6 +45,7 @@ jobs:
45
45
  python examples/basic_cli.py
46
46
  python examples/billing_demo.py
47
47
  python examples/http_driver_demo.py
48
+ python examples/tutorial.py
48
49
 
49
50
  conformance_stub:
50
51
  name: "Weaver Spec Conformance Stub (v0.1.0)"
@@ -7,6 +7,119 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.0] - 2026-05-22
11
+
12
+ ### Added
13
+ - "Secure your first MCP tool in 5 minutes" tutorial: new
14
+ [`docs/tutorial.md`](docs/tutorial.md) walks a new reader from install to a
15
+ working invocation, covering registration, principals, grants, the three
16
+ LLM-safe response modes (`summary` / `table` / `handle_only`), handle
17
+ expansion, policy denial with stable `reason_code`, and `explain()`
18
+ audit. The admin-only `raw` mode is described but not exercised by the
19
+ walkthrough. Companion runnable example
20
+ [`examples/tutorial.py`](examples/tutorial.py) uses `InMemoryDriver`
21
+ (offline, zero external deps) and is exercised by `make example` and CI;
22
+ it now `assert`s that no PII field leaks into the LLM-safe Frame so a
23
+ firewall regression fails the build. (#46)
24
+ - README "How this relates to neighboring projects" section: a neutral
25
+ boundaries table covering `AgentFence` (external CLI/proxy gate),
26
+ `contextweaver` (context compilation library), `ChainWeaver`
27
+ (deterministic flow orchestrator), and `weaver-spec` (specification +
28
+ conformance suite), plus a "When *not* to use this" callout. (#71)
29
+ - Grant-constraint enforcement on handle expansion (#76). `Handle` now carries
30
+ the `principal_id` and `constraints` from the original grant, persisted at
31
+ handle creation time by `HandleStore.store`. `HandleStore.expand` rechecks
32
+ these against the requested expand query:
33
+ - A request `limit` larger than the grant's `max_rows` is rejected with
34
+ `HandleConstraintViolation` (`reason_code = handle_constraint_violation`).
35
+ - A request `fields` entry outside `allowed_fields` is rejected; an
36
+ unscoped expand applies `allowed_fields` as the default projection.
37
+ - A request filter that disagrees with the grant's `scope` is rejected;
38
+ the scope filter is otherwise AND-merged so the caller cannot bypass it.
39
+ - A `principal_id` parameter that does not match the handle's stored
40
+ principal raises `HandleConstraintViolation`
41
+ (`reason_code = handle_principal_mismatch`).
42
+ - `SensitivityTag.MEMORY` and memory-action policy rules in
43
+ `DefaultPolicyEngine` (#75). Project-scoped memory reads are allowed by
44
+ default; sensitive-scoped reads require the `memory_reader_sensitive` role
45
+ (or `admin`); writes always require `memory_writer` (or `admin`). The
46
+ `explain()` path lists the same conditions with stable `reason_code`s.
47
+ - New stable `DenialReason` codes: `HANDLE_CONSTRAINT_VIOLATION`,
48
+ `HANDLE_PRINCIPAL_MISMATCH`, `MEMORY_WRITE_REQUIRES_WRITER`,
49
+ `MEMORY_SENSITIVE_READ_DENIED`.
50
+ - `HandleConstraintViolation` error class (subclass of `AgentKernelError`,
51
+ exported from `agent_kernel`) — carries an optional `reason_code` matching
52
+ the `DenialReason` vocabulary so handle-side and grant-side denials share
53
+ one set of stable codes.
54
+ - `Kernel.expand` accepts an optional `principal: Principal` argument that
55
+ is forwarded to `HandleStore.expand` for principal-mismatch checks.
56
+ - Memory-action input redaction (#75): `ActionTrace.args` for any capability
57
+ whose ID starts with `memory.` has payload-like keys (`payload`, `content`,
58
+ `value`, `memory`, `text`, `body`) replaced with `"[REDACTED]"`. Keys are
59
+ preserved so audit can confirm the action took place without exposing the
60
+ durable content the agent wrote or read.
61
+ - New `tests/test_firewall_boundary.py` (#74) — focused regression suite that
62
+ pushes synthetic secret/PII values through the raw → `Frame` boundary
63
+ end-to-end and asserts those values never appear in summary/table/raw
64
+ frames, are stripped by `allowed_fields`, never reach `ActionTrace.args`
65
+ for memory capabilities, and stay quarantined when raw mode is downgraded
66
+ for non-admin principals.
67
+
68
+ ### Security
69
+ - Closes #76: handle expansion can no longer return data outside the original
70
+ grant's `max_rows` / `allowed_fields` / `scope`, and handle IDs are no
71
+ longer bearer credentials that work across principals.
72
+ - Closes #75: memory reads and writes are governed actions with stable
73
+ denial codes and trace-side redaction of durable payloads.
74
+ - Closes #74: redaction boundary is pinned by negative assertions against
75
+ fake-secret strings, catching future regressions that drop a redaction
76
+ step or route raw data through a new path.
77
+
78
+ ## [0.7.0] - 2026-05-20
79
+
80
+ ### Added
81
+ - Structured intent and scope metadata on `CapabilityRequest`: new optional
82
+ `intent: str | None` and `scope: dict[str, Any]` fields let policy engines
83
+ authorize based on machine-readable intent and scope alongside the existing
84
+ free-text `goal`. `DeclarativePolicyEngine` rules can match on these via new
85
+ `intent: [...]` and `scope: {key: value}` clauses in YAML/TOML policy files.
86
+ Intent-aware allow rules fail closed for legacy callers that don't set an
87
+ intent. (#72)
88
+ - Structured policy decision trace (`PolicyDecisionTrace` + `PolicyTraceStep`):
89
+ both built-in policy engines now attach a step-by-step trace to every
90
+ `PolicyDecision` (allow and deny paths). Each step records the rule
91
+ considered, the outcome (`matched`/`skipped`/`denied`/`allowed`/
92
+ `constraint_applied`), a human-readable detail, and — for terminal
93
+ steps — the stable reason code. Traces echo `intent` and `scope_keys`
94
+ (scope dimension names only — values redacted) from the request and contain
95
+ no raw argument values. `DryRunResult.policy_decision`
96
+ also carries a synthesized single-step trace. (#73)
97
+ - Stable machine-readable denial reason codes: new `DenialReason` and
98
+ `AllowReason` enums in `agent_kernel.policy_reasons` (also exported as
99
+ `from agent_kernel import DenialReason, AllowReason`). Every built-in
100
+ denial path on `DefaultPolicyEngine` and `DeclarativePolicyEngine` populates
101
+ `PolicyDecision.reason_code`, `DenialExplanation.reason_code`,
102
+ `FailedCondition.reason_code`, and `PolicyDenied.reason_code`. Tests should
103
+ assert on these codes instead of matching the human-readable `reason` /
104
+ `narrative` strings, which remain part of the API but may evolve for
105
+ clarity. Codes: `missing_role`, `missing_tenant_attribute`,
106
+ `missing_attribute`, `insufficient_justification`, `invalid_constraint`,
107
+ `rate_limited`, `no_matching_rule`, `explicit_deny_rule`,
108
+ `intent_not_allowed`, `scope_not_allowed`; allow-side: `default_policy_allow`,
109
+ `rule_allow`, `default_fallthrough_allow`. (#77)
110
+ - New public exports: `AllowReason`, `DenialReason`, `PolicyDecisionTrace`,
111
+ `PolicyTraceStep`.
112
+
113
+ ### Changed
114
+ - `PolicyDecision` gained optional `reason_code: str | None` and
115
+ `trace: PolicyDecisionTrace | None` fields (both default `None` so
116
+ third-party engines that don't populate them keep working).
117
+ - `DenialExplanation` and `FailedCondition` gained optional `reason_code`
118
+ fields populated by both built-in engines on every denial path.
119
+ - `PolicyDenied(reason_code=...)` keyword argument: the exception now carries
120
+ a `reason_code` attribute so callers can branch on a stable code without
121
+ matching the human-readable message.
122
+
10
123
  ## [0.6.0] - 2026-05-19
11
124
 
12
125
  ### Added
@@ -16,5 +16,6 @@ example:
16
16
  python examples/basic_cli.py
17
17
  python examples/billing_demo.py
18
18
  python examples/http_driver_demo.py
19
+ python examples/tutorial.py
19
20
 
20
21
  ci: fmt lint type test example
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: weaver-kernel
3
- Version: 0.6.0
3
+ Version: 0.8.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
@@ -289,6 +289,8 @@ pip install weaver-kernel
289
289
 
290
290
  > **Note:** The PyPI package is `weaver-kernel` (Weaver ecosystem), but the Python import remains `agent_kernel`.
291
291
 
292
+ > **New here?** [docs/tutorial.md](docs/tutorial.md) walks through register → grant → invoke → expand → explain in five minutes.
293
+
292
294
  ```python
293
295
  import asyncio, os
294
296
  os.environ["AGENT_KERNEL_SECRET"] = "my-secret"
@@ -356,6 +358,45 @@ asyncio.run(main())
356
358
 
357
359
  `agent-kernel` sits **above** `contextweaver` (context compilation) and **above** raw tool execution. It provides the authorization, execution, and audit layer.
358
360
 
361
+ ## How this relates to neighboring projects
362
+
363
+ `agent-kernel` is the embeddable runtime layer of the **Weaver ecosystem**. The
364
+ projects below solve adjacent problems and are designed to compose, not to
365
+ overlap.
366
+
367
+ | Project | Role | Where it runs | Use it when… |
368
+ |---|---|---|---|
369
+ | **agent-kernel** *(this repo)* | Embeddable library/runtime: capability registry, policy, HMAC tokens, context firewall, audit trace. | In-process inside your agent host. | You need authorization, redaction, and audit between an LLM loop and a large tool ecosystem. |
370
+ | [**AgentFence**](https://github.com/dgenio/AgentFence) | External CLI / local proxy that intercepts tool calls and applies a policy gate. | Out-of-process, alongside your agent. | You want a policy boundary without changing your agent code, or you need to gate a third-party agent host you can't modify. |
371
+ | [**contextweaver**](https://github.com/dgenio/contextweaver) | Library that selects and compiles the context an LLM receives. | In-process, before the LLM call. | You need to assemble relevant context for a prompt. It sits *under* the LLM loop; agent-kernel sits *between* the LLM and tools. |
372
+ | **ChainWeaver** | Orchestrator for deterministic tool chains. | In-process or as a separate service. | You need to run a multi-step deterministic flow rather than free-form LLM tool use. |
373
+ | [**weaver-spec**](https://github.com/dgenio/weaver-spec) | Specification: invariants, capability/token/frame contracts, conformance suite. | Not a runtime — it's docs + a contract test suite. | You're building another Weaver-compatible implementation, or you want to verify an existing one. |
374
+
375
+ A minimal architecture using `agent-kernel` as the central runtime:
376
+
377
+ ```
378
+ LLM / agent loop
379
+
380
+
381
+ contextweaver ─► agent-kernel ─► driver ─► MCP / HTTP / A2A / internal API
382
+
383
+
384
+ ActionTrace
385
+ ```
386
+
387
+ ### When *not* to use this
388
+
389
+ - You only need a process-level policy gate around an existing agent host —
390
+ reach for `AgentFence` instead.
391
+ - You only need to compile context for a prompt — use `contextweaver`.
392
+ - You want a deterministic, scripted workflow with no LLM in the inner loop —
393
+ use `ChainWeaver`.
394
+ - You're writing a static analyzer or one-shot CLI scanner with no
395
+ per-invocation runtime — `agent-kernel` would be overkill.
396
+
397
+ See [docs/tutorial.md](docs/tutorial.md) for an end-to-end "secure your first
398
+ MCP tool in 5 minutes" walkthrough.
399
+
359
400
  ## Weaver Spec Compatibility: v0.1.0
360
401
 
361
402
  agent-kernel is a compliant implementation of [weaver-spec v0.1.0](https://github.com/dgenio/weaver-spec).
@@ -43,6 +43,8 @@ pip install weaver-kernel
43
43
 
44
44
  > **Note:** The PyPI package is `weaver-kernel` (Weaver ecosystem), but the Python import remains `agent_kernel`.
45
45
 
46
+ > **New here?** [docs/tutorial.md](docs/tutorial.md) walks through register → grant → invoke → expand → explain in five minutes.
47
+
46
48
  ```python
47
49
  import asyncio, os
48
50
  os.environ["AGENT_KERNEL_SECRET"] = "my-secret"
@@ -110,6 +112,45 @@ asyncio.run(main())
110
112
 
111
113
  `agent-kernel` sits **above** `contextweaver` (context compilation) and **above** raw tool execution. It provides the authorization, execution, and audit layer.
112
114
 
115
+ ## How this relates to neighboring projects
116
+
117
+ `agent-kernel` is the embeddable runtime layer of the **Weaver ecosystem**. The
118
+ projects below solve adjacent problems and are designed to compose, not to
119
+ overlap.
120
+
121
+ | Project | Role | Where it runs | Use it when… |
122
+ |---|---|---|---|
123
+ | **agent-kernel** *(this repo)* | Embeddable library/runtime: capability registry, policy, HMAC tokens, context firewall, audit trace. | In-process inside your agent host. | You need authorization, redaction, and audit between an LLM loop and a large tool ecosystem. |
124
+ | [**AgentFence**](https://github.com/dgenio/AgentFence) | External CLI / local proxy that intercepts tool calls and applies a policy gate. | Out-of-process, alongside your agent. | You want a policy boundary without changing your agent code, or you need to gate a third-party agent host you can't modify. |
125
+ | [**contextweaver**](https://github.com/dgenio/contextweaver) | Library that selects and compiles the context an LLM receives. | In-process, before the LLM call. | You need to assemble relevant context for a prompt. It sits *under* the LLM loop; agent-kernel sits *between* the LLM and tools. |
126
+ | **ChainWeaver** | Orchestrator for deterministic tool chains. | In-process or as a separate service. | You need to run a multi-step deterministic flow rather than free-form LLM tool use. |
127
+ | [**weaver-spec**](https://github.com/dgenio/weaver-spec) | Specification: invariants, capability/token/frame contracts, conformance suite. | Not a runtime — it's docs + a contract test suite. | You're building another Weaver-compatible implementation, or you want to verify an existing one. |
128
+
129
+ A minimal architecture using `agent-kernel` as the central runtime:
130
+
131
+ ```
132
+ LLM / agent loop
133
+
134
+
135
+ contextweaver ─► agent-kernel ─► driver ─► MCP / HTTP / A2A / internal API
136
+
137
+
138
+ ActionTrace
139
+ ```
140
+
141
+ ### When *not* to use this
142
+
143
+ - You only need a process-level policy gate around an existing agent host —
144
+ reach for `AgentFence` instead.
145
+ - You only need to compile context for a prompt — use `contextweaver`.
146
+ - You want a deterministic, scripted workflow with no LLM in the inner loop —
147
+ use `ChainWeaver`.
148
+ - You're writing a static analyzer or one-shot CLI scanner with no
149
+ per-invocation runtime — `agent-kernel` would be overkill.
150
+
151
+ See [docs/tutorial.md](docs/tutorial.md) for an end-to-end "secure your first
152
+ MCP tool in 5 minutes" walkthrough.
153
+
113
154
  ## Weaver Spec Compatibility: v0.1.0
114
155
 
115
156
  agent-kernel is a compliant implementation of [weaver-spec v0.1.0](https://github.com/dgenio/weaver-spec).
@@ -52,13 +52,61 @@ Both built-in engines satisfy `ExplainingPolicyEngine`:
52
52
  3. **DESTRUCTIVE** — requires role `admin` + `justification ≥ 15 chars`
53
53
  4. **PII/PCI** — requires `tenant` attribute; enforces `allowed_fields` unless `pii_reader`
54
54
  5. **SECRETS** — requires role `admin|secrets_reader` + `justification ≥ 15 chars`
55
- 6. **max_rows** — 50 (user), 500 (service)
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 `min_justification` 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.
55
+ 6. **MEMORY** — `memory.read` with `scope.memory_scope == "sensitive"` requires role `memory_reader_sensitive|admin`; `memory.write` / DESTRUCTIVE memory requires role `memory_writer|admin`. Project-scoped memory reads are allowed by default. The kernel also redacts `payload`/`content`/`value`/`memory`/`text`/`body` keys from `ActionTrace.args` for any capability whose ID starts with `memory.`
56
+ 7. **max_rows** — 50 (user), 500 (service)
57
+ 8. **Rate limiting** sliding-window per `(principal_id, capability_id)` (60 READ / 10 WRITE / 2 DESTRUCTIVE per 60s; service role gets 10×)
58
+ - **`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.
59
+
60
+ #### Intent and scope on requests
61
+
62
+ `CapabilityRequest` carries optional structured metadata alongside its free-text `goal`:
63
+
64
+ - `intent: str | None` — a machine-readable label (e.g. `"customer_support_lookup"`).
65
+ - `scope: dict[str, Any]` — a small structured map (e.g. `{"region": "eu-west", "customer_id": "C-42"}`).
66
+
67
+ `DeclarativePolicyEngine` rules can match on these via top-level keys in `match`:
68
+
69
+ ```yaml
70
+ - name: support_eu_lookup
71
+ match:
72
+ safety_class: [READ]
73
+ intent: [customer_support_lookup]
74
+ scope: { region: "eu-west" }
75
+ action: allow
76
+ ```
77
+
78
+ 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
79
 
59
80
  #### Denial explanations
60
81
 
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, and a human-readable `narrative`. 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
+ `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.
83
+
84
+ #### Reason codes
85
+
86
+ 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:
87
+
88
+ | Code (`DenialReason.*`) | When |
89
+ |---|---|
90
+ | `missing_role` | Principal lacks a required role |
91
+ | `missing_tenant_attribute` | PII/PCI capability needs `tenant` attribute |
92
+ | `missing_attribute` | Declarative rule's required attribute absent or mismatched |
93
+ | `insufficient_justification` | Justification shorter than the minimum |
94
+ | `invalid_constraint` | Constraint value (e.g. `max_rows`) not parseable |
95
+ | `rate_limited` | Sliding-window rate limit exceeded |
96
+ | `no_matching_rule` | DSL: no rule matched + default `deny` |
97
+ | `explicit_deny_rule` | DSL: a `deny` rule matched fully |
98
+ | `intent_not_allowed` | DSL: `match.intent` rejected the request's intent |
99
+ | `scope_not_allowed` | DSL: `match.scope` rejected the request's scope |
100
+ | `handle_constraint_violation` | `HandleStore.expand` request exceeded grant's `max_rows`, `allowed_fields`, or `scope` (#76) |
101
+ | `handle_principal_mismatch` | Handle expansion attempted by a different principal than the one the original grant was issued to (#76) |
102
+ | `memory_write_requires_writer` | `SensitivityTag.MEMORY` WRITE/DESTRUCTIVE without `memory_writer` or `admin` role (#75) |
103
+ | `memory_sensitive_read_denied` | `SensitivityTag.MEMORY` read with `scope.memory_scope == "sensitive"` without `memory_reader_sensitive` or `admin` role (#75) |
104
+
105
+ Allow-side codes (`AllowReason.*`): `default_policy_allow`, `rule_allow`, `default_fallthrough_allow`, `token_verified`.
106
+
107
+ #### Decision trace
108
+
109
+ 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
110
 
63
111
  #### Dry-run mode
64
112
 
@@ -0,0 +1,95 @@
1
+ # Security Model
2
+
3
+ ## Threat model
4
+
5
+ | Threat | Mitigation |
6
+ |--------|-----------|
7
+ | Tool-space interference (agent calls wrong tool) | Capability registry + policy gate before any execution |
8
+ | Confused deputy attack | Tokens are bound to `principal_id` — cannot be reused by another principal |
9
+ | Token forgery / tampering | HMAC-SHA256 signature; any bit flip → `TokenInvalid` |
10
+ | Token replay after expiry | Expiry checked on every `verify()` call |
11
+ | Context injection via raw tool output | Firewall always transforms `RawResult → Frame`; raw data never reaches LLM by default |
12
+ | PII / PCI leakage | Redaction + `allowed_fields` enforcement in the firewall |
13
+ | Privilege escalation via WRITE/DESTRUCTIVE | Policy engine enforces role requirements |
14
+ | Audit evasion | Every `invoke()` creates an immutable `ActionTrace` |
15
+ | Handle scope escape (expand exceeds grant) | Handles persist grant constraints; `HandleStore.expand` rechecks `max_rows`, `allowed_fields`, `scope`, and principal binding (#76) |
16
+ | Memory exfiltration via tool output | `SensitivityTag.MEMORY` capabilities gate sensitive reads and durable writes; `ActionTrace.args` redacts payload-like fields for `memory.*` capabilities (#75) |
17
+ | Raw memory payload reaching audit log | Kernel strips `payload`/`content`/`value`/`memory`/`text`/`body` from `ActionTrace.args` for `memory.*` capabilities |
18
+
19
+ ## Token scopes
20
+
21
+ A `CapabilityToken` binds:
22
+ - `capability_id` — which capability is authorized
23
+ - `principal_id` — who the token was issued to
24
+ - `constraints` — max_rows, allowed_fields, etc. (signed into the token)
25
+ - `expires_at` — validity window
26
+
27
+ Any change to these fields invalidates the HMAC signature.
28
+
29
+ ## Confused deputy prevention
30
+
31
+ Consider an agent that obtains a token for `billing.list_invoices` then passes it to a different agent. The second agent cannot use it because `verify()` checks that `token.principal_id == expected_principal_id`.
32
+
33
+ The same principle extends to handles: every `Handle` carries the `principal_id`
34
+ the original grant was issued to. When `handle.principal_id` is non-empty,
35
+ `HandleStore.expand` rejects expansion unless the caller supplies a matching
36
+ `principal_id`. **An omitted or empty `principal_id` is treated as a
37
+ mismatch** (`HandleConstraintViolation`, `reason_code = HANDLE_PRINCIPAL_MISMATCH`),
38
+ so a handle ID alone is not a bearer credential — proof of the original
39
+ principal is always required. `Kernel.expand(..., principal=Principal(...))`
40
+ forwards the principal automatically.
41
+
42
+ ## Handle expansion boundary
43
+
44
+ Calling `kernel.expand(handle, query=...)` does not re-run the policy engine —
45
+ the original grant already authorised the dataset, and handles are short-lived.
46
+ But the grant's _constraints_ must still apply, otherwise an over-broad
47
+ `expand` query would silently return data the original grant never covered.
48
+
49
+ `HandleStore.expand` rechecks the constraints the kernel persists on the handle
50
+ at creation time (`token.constraints`):
51
+
52
+ | Constraint | Enforced behavior on expand |
53
+ |------------|-----------------------------|
54
+ | `max_rows` | A request `limit` larger than the cap raises `HandleConstraintViolation`. An unspecified or larger implicit limit is silently clamped. |
55
+ | `allowed_fields` | A request `fields` entry that is not in `allowed_fields` raises `HandleConstraintViolation`. An unscoped expand applies `allowed_fields` as the default projection, so disallowed fields never leak. |
56
+ | `scope` (e.g. `{"region": "eu"}`) | The scope filter is AND-merged into the request filter. A request filter that disagrees on a scoped dimension raises `HandleConstraintViolation`. |
57
+ | `principal_id` | A mismatched `principal_id` parameter raises `HandleConstraintViolation` (`HANDLE_PRINCIPAL_MISMATCH`). |
58
+
59
+ Errors carry stable `reason_code` values (`handle_constraint_violation`,
60
+ `handle_principal_mismatch`) — assert on those, not on the message text.
61
+
62
+ ## Memory actions
63
+
64
+ Capabilities tagged `SensitivityTag.MEMORY` represent durable agent memory
65
+ (project notes, session handoff, learned context). Reads of project-scoped
66
+ memory are allowed by default; reads of sensitive-scoped memory require an
67
+ explicit role. Writes always require the `memory_writer` role (or `admin`)
68
+ because they persist into future sessions.
69
+
70
+ | Action | Required role | Denial reason code |
71
+ |--------|---------------|--------------------|
72
+ | `memory.read` with `scope["memory_scope"] == "project"` | none | — |
73
+ | `memory.read` with `scope["memory_scope"] == "sensitive"` | `memory_reader_sensitive` or `admin` | `memory_sensitive_read_denied` |
74
+ | `memory.write` (any scope) | `memory_writer` or `admin` | `memory_write_requires_writer` |
75
+ | `memory.forget` (DESTRUCTIVE) | `admin` (then `memory_writer` or `admin`) | `missing_role`, then `memory_write_requires_writer` |
76
+
77
+ To prevent durable memory content from leaking into the audit log, the kernel
78
+ strips payload-like fields (`payload`, `content`, `value`, `memory`, `text`,
79
+ `body`) from `ActionTrace.args` for any capability whose ID begins with
80
+ `memory.`. Non-sensitive metadata keys (`key`, `id`, `scope`, ...) are
81
+ preserved so audit can still confirm an action took place.
82
+
83
+ ## Security disclaimers
84
+
85
+ > **v0.1 is not production-hardened for real authentication.**
86
+
87
+ - HMAC tokens are tamper-evident but **not encrypted**. Do not put sensitive data in token fields.
88
+ - The `AGENT_KERNEL_SECRET` must be kept secret. Rotate it if compromised.
89
+ - The default `InMemoryDriver` has no persistence — suitable for testing only.
90
+ - PII redaction is heuristic (regex-based). It is not a substitute for proper data governance.
91
+ - Rate limiting is enforced per `(principal_id, capability_id)` pair using a sliding window.
92
+ Default limits: 60 READ / 10 WRITE / 2 DESTRUCTIVE invocations per 60-second window.
93
+ Principals with the `"service"` role receive 10× the default limits. Limits are
94
+ configurable via `DefaultPolicyEngine(rate_limits=...)`. There is no distributed or
95
+ persistent rate-limit state — limits reset on process restart.