weaver-kernel 0.8.0__tar.gz → 0.9.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 (103) hide show
  1. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/.github/workflows/ci.yml +1 -0
  2. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/AGENTS.md +1 -1
  3. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/CHANGELOG.md +129 -0
  4. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/CONTRIBUTING.md +6 -5
  5. weaver_kernel-0.9.0/Makefile +25 -0
  6. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/PKG-INFO +14 -5
  7. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/README.md +13 -4
  8. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/docs/agent-context/architecture.md +1 -1
  9. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/docs/agent-context/invariants.md +2 -1
  10. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/docs/agent-context/review-checklist.md +1 -1
  11. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/docs/agent-context/workflows.md +12 -6
  12. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/docs/architecture.md +1 -1
  13. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/docs/capabilities.md +55 -0
  14. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/docs/context_firewall.md +64 -3
  15. weaver_kernel-0.9.0/docs/federation.md +218 -0
  16. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/docs/integrations.md +35 -0
  17. weaver_kernel-0.9.0/examples/readme_quickstart.py +79 -0
  18. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/pyproject.toml +5 -1
  19. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/src/agent_kernel/__init__.py +69 -5
  20. weaver_kernel-0.9.0/src/agent_kernel/drivers/base.py +92 -0
  21. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/src/agent_kernel/errors.py +46 -0
  22. weaver_kernel-0.9.0/src/agent_kernel/federation.py +272 -0
  23. weaver_kernel-0.9.0/src/agent_kernel/federation_discovery.py +306 -0
  24. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/src/agent_kernel/firewall/transform.py +63 -1
  25. weaver_kernel-0.9.0/src/agent_kernel/kernel/__init__.py +446 -0
  26. weaver_kernel-0.9.0/src/agent_kernel/kernel/_dry_run.py +99 -0
  27. weaver_kernel-0.9.0/src/agent_kernel/kernel/_federation.py +132 -0
  28. weaver_kernel-0.9.0/src/agent_kernel/kernel/_invoke.py +329 -0
  29. weaver_kernel-0.9.0/src/agent_kernel/kernel/_stream.py +215 -0
  30. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/src/agent_kernel/models.py +213 -0
  31. weaver_kernel-0.9.0/src/agent_kernel/otel.py +255 -0
  32. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/src/agent_kernel/policy.py +5 -62
  33. weaver_kernel-0.9.0/src/agent_kernel/policy_dsl.py +297 -0
  34. weaver_kernel-0.9.0/src/agent_kernel/policy_dsl_explain.py +214 -0
  35. weaver_kernel-0.9.0/src/agent_kernel/policy_dsl_parser.py +277 -0
  36. weaver_kernel-0.9.0/src/agent_kernel/rate_limit.py +78 -0
  37. weaver_kernel-0.9.0/src/agent_kernel/registry.py +296 -0
  38. weaver_kernel-0.9.0/src/agent_kernel/search_index.py +160 -0
  39. weaver_kernel-0.9.0/tests/test_federation.py +501 -0
  40. weaver_kernel-0.9.0/tests/test_federation_discovery.py +390 -0
  41. weaver_kernel-0.9.0/tests/test_firewall_stream.py +198 -0
  42. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/tests/test_kernel.py +110 -0
  43. weaver_kernel-0.9.0/tests/test_otel.py +192 -0
  44. weaver_kernel-0.9.0/tests/test_public_api.py +40 -0
  45. weaver_kernel-0.9.0/tests/test_readme_quickstart.py +53 -0
  46. weaver_kernel-0.9.0/tests/test_registry.py +415 -0
  47. weaver_kernel-0.8.0/Makefile +0 -21
  48. weaver_kernel-0.8.0/src/agent_kernel/drivers/base.py +0 -42
  49. weaver_kernel-0.8.0/src/agent_kernel/kernel.py +0 -632
  50. weaver_kernel-0.8.0/src/agent_kernel/policy_dsl.py +0 -661
  51. weaver_kernel-0.8.0/src/agent_kernel/registry.py +0 -124
  52. weaver_kernel-0.8.0/tests/test_registry.py +0 -112
  53. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/.claude/CLAUDE.md +0 -0
  54. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/.github/copilot-instructions.md +0 -0
  55. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/.github/workflows/publish.yml +0 -0
  56. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/.gitignore +0 -0
  57. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/LICENSE +0 -0
  58. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/RELEASE.md +0 -0
  59. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/docs/agent-context/lessons-learned.md +0 -0
  60. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/docs/security.md +0 -0
  61. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/docs/tutorial.md +0 -0
  62. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/examples/basic_cli.py +0 -0
  63. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/examples/billing_demo.py +0 -0
  64. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/examples/http_driver_demo.py +0 -0
  65. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/examples/policies/default.toml +0 -0
  66. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/examples/policies/default.yaml +0 -0
  67. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/examples/tutorial.py +0 -0
  68. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/src/agent_kernel/adapters/__init__.py +0 -0
  69. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/src/agent_kernel/adapters/_base.py +0 -0
  70. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/src/agent_kernel/adapters/anthropic.py +0 -0
  71. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/src/agent_kernel/adapters/openai.py +0 -0
  72. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/src/agent_kernel/drivers/__init__.py +0 -0
  73. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/src/agent_kernel/drivers/http.py +0 -0
  74. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/src/agent_kernel/drivers/mcp.py +0 -0
  75. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/src/agent_kernel/drivers/mcp_support.py +0 -0
  76. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/src/agent_kernel/drivers/memory.py +0 -0
  77. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/src/agent_kernel/enums.py +0 -0
  78. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/src/agent_kernel/firewall/__init__.py +0 -0
  79. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/src/agent_kernel/firewall/budget_manager.py +0 -0
  80. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/src/agent_kernel/firewall/budgets.py +0 -0
  81. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/src/agent_kernel/firewall/redaction.py +0 -0
  82. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/src/agent_kernel/firewall/summarize.py +0 -0
  83. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/src/agent_kernel/firewall/token_counting.py +0 -0
  84. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/src/agent_kernel/handles.py +0 -0
  85. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/src/agent_kernel/policy_reasons.py +0 -0
  86. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/src/agent_kernel/py.typed +0 -0
  87. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/src/agent_kernel/router.py +0 -0
  88. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/src/agent_kernel/tokens.py +0 -0
  89. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/src/agent_kernel/trace.py +0 -0
  90. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/tests/conftest.py +0 -0
  91. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/tests/test_adapters.py +0 -0
  92. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/tests/test_drivers.py +0 -0
  93. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/tests/test_firewall.py +0 -0
  94. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/tests/test_firewall_boundary.py +0 -0
  95. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/tests/test_handles.py +0 -0
  96. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/tests/test_logging.py +0 -0
  97. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/tests/test_mcp_driver.py +0 -0
  98. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/tests/test_models.py +0 -0
  99. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/tests/test_policy.py +0 -0
  100. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/tests/test_redaction.py +0 -0
  101. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/tests/test_router.py +0 -0
  102. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/tests/test_tokens.py +0 -0
  103. {weaver_kernel-0.8.0 → weaver_kernel-0.9.0}/tests/test_trace.py +0 -0
@@ -46,6 +46,7 @@ jobs:
46
46
  python examples/billing_demo.py
47
47
  python examples/http_driver_demo.py
48
48
  python examples/tutorial.py
49
+ python examples/readme_quickstart.py
49
50
 
50
51
  conformance_stub:
51
52
  name: "Weaver Spec Conformance Stub (v0.1.0)"
@@ -46,7 +46,7 @@ Use these terms consistently. Never substitute synonyms:
46
46
 
47
47
  ## Quality bar
48
48
 
49
- - `make ci` must pass before every push. It runs: `fmt → lint → type → test → example`.
49
+ - `make ci` must pass before every push. It runs: `fmt-check → lint → type → test → example`. Use `make fmt` to auto-fix formatting before re-running `make ci`.
50
50
  - All public interfaces need type hints and docstrings.
51
51
  - Never raise bare `ValueError` or `KeyError` to callers. Use custom exceptions from `errors.py`. Catching stdlib exceptions internally to remap them is fine.
52
52
  - Error messages are part of the contract — tests must assert both exception type and message.
@@ -7,6 +7,135 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.0] - 2026-05-29
11
+
12
+ ### Added
13
+ - Capability namespaces and hierarchical discovery in `CapabilityRegistry`:
14
+ dot-notation `capability_id`s now expose `list_namespaces()` /
15
+ `list_namespace(prefix)` operations; `register_namespace(prefix, loader=...)`
16
+ enables deferred registration for large tool ecosystems (the loader runs
17
+ at most once on first access). `search()` gained an `offset` kwarg for
18
+ pagination, strips a small stop-word set, and now scores with a
19
+ BM25-flavoured ranker that weights `capability_id`/`tags` matches above
20
+ `description`. Flat (un-namespaced) capability IDs continue to work
21
+ unchanged. (#45)
22
+ - Capability marketplace, part 1 — manifest format & local registry: new
23
+ `CapabilityDescriptor` and `CapabilityManifest` dataclasses (both
24
+ JSON-round-trippable via `to_dict`/`from_dict`), new
25
+ `agent_kernel.federation` module with `build_manifest()`,
26
+ `import_manifest()`, and `merge_sensitivity()`, and new `Kernel.advertise()`
27
+ / `Kernel.import_remote()` methods. `Kernel` gained a `kernel_id`
28
+ argument used as the manifest publisher identity. Three trust policies
29
+ are honoured at import time (`most_restrictive` (default), `local_only`,
30
+ `remote_deferred`); imported capabilities are routed through a
31
+ caller-supplied driver and flow through the full local policy → token →
32
+ firewall pipeline. HMAC tokens remain kernel-scoped — a token issued by
33
+ one kernel cannot be verified by another with a different secret. New
34
+ errors `NamespaceNotFound`, `FederationError`, `ManifestError`,
35
+ `TrustPolicyError`. (#52)
36
+ - New docs: [`docs/federation.md`](docs/federation.md) for the marketplace
37
+ protocol and a namespace section in
38
+ [`docs/capabilities.md`](docs/capabilities.md).
39
+ - Capability marketplace, part 2 — federated discovery: new
40
+ `agent_kernel.federation_discovery` module with `discover_peers()`,
41
+ `sign_manifest()`, `verify_manifest()`, `serve_manifest_payload()`, and
42
+ `DiscoveryRateLimiter`. `Kernel.discover_peers()` fetches one or more
43
+ manifests over HTTP from peer URLs or a registry URL. Signed envelopes
44
+ (HMAC-SHA256) detect tampering and let importers refuse unsigned
45
+ manifests when a verification secret is in play (and vice versa). New
46
+ errors `ManifestSignatureError` and `DiscoveryError`. (#51, closes #49)
47
+ - OpenTelemetry integration: new `agent_kernel.otel` module with
48
+ `instrument_kernel(kernel)` that wraps `Kernel.invoke` and
49
+ `Kernel.grant_capability` with OTel spans + metrics (invocation count,
50
+ latency histogram, denial counter). No-op when the optional `[otel]`
51
+ extra is not installed (`OTEL_AVAILABLE` reports the runtime status).
52
+ Idempotent — repeat calls on the same kernel are no-ops. (#38)
53
+ - Streaming firewall: new `Firewall.apply_stream()` async-iterator method
54
+ that processes driver chunks one-at-a-time, applying PII redaction
55
+ per-chunk. New `StreamingDriver` Protocol in `drivers/base.py` extends
56
+ `Driver` with an optional `execute_stream()`. New `Kernel.invoke_stream()`
57
+ yields `Frame` chunks; the last chunk carries `is_final=True`. Drivers
58
+ without `execute_stream` automatically fall back to a single-chunk stream
59
+ via `execute()`. `Frame` gained an `is_final: bool` field. (#47)
60
+ - `examples/readme_quickstart.py` — a runnable mirror of the README quickstart,
61
+ wired into `make example` and the CI "Examples" step. Together with
62
+ `tests/test_readme_quickstart.py` (which extracts and runs the inline README
63
+ snippet itself), CI fails if the documented quickstart stops producing the
64
+ expected output. (#83)
65
+
66
+ ### Changed
67
+ - Tech debt: `policy_dsl.py` decomposed (was 661 lines). Parsing and
68
+ schema dataclasses now live in `policy_dsl_parser.py`
69
+ (`PolicyMatch`, `PolicyRule`, `parse_engine_data`, `parse_rule`,
70
+ YAML/TOML loaders), and the denial-explanation traversal in
71
+ `policy_dsl_explain.py`. The public import surface
72
+ (`DeclarativePolicyEngine`, `PolicyMatch`, `PolicyRule`) is unchanged.
73
+ `RateLimiter` and rate-limit constants extracted from `policy.py` into
74
+ a new `rate_limit.py` module; `policy.py` continues to re-export them
75
+ under their original names. (#68)
76
+ - Tech debt: `kernel.py` split into the `agent_kernel.kernel` sub-package
77
+ to honour AGENTS.md's ≤ 300-line module bar. The `Kernel` class lives
78
+ in `kernel/__init__.py`; heavy methods (invoke pipeline, dry-run,
79
+ federation, streaming) delegate to sibling modules. Existing
80
+ `from agent_kernel import Kernel` / `from agent_kernel.kernel import Kernel`
81
+ imports are unchanged. (#68)
82
+
83
+ ### Documentation
84
+ - Fixed handle-expansion examples that omitted the now-required `principal`
85
+ argument and therefore raised `HandleConstraintViolation` when copy-pasted:
86
+ the README quickstart (#83) and `docs/context_firewall.md` +
87
+ `docs/architecture.md` (#84) now pass `principal=` and link to
88
+ `docs/security.md#handle-expansion-boundary`. The README quickstart also
89
+ drops two unused imports (`ExecutionContext`, `HMACTokenProvider`).
90
+ - `docs/capabilities.md` "Sensitivity tags" now lists `SensitivityTag.MEMORY`
91
+ and links to `docs/security.md#memory-actions`. (#89)
92
+ - Corrected the base-dependency list in `docs/agent-context/invariants.md` and
93
+ `docs/agent-context/architecture.md` from "`httpx` only" to
94
+ "`httpx` + `pydantic`", pointing to `AGENTS.md` as the canonical dependency
95
+ policy. (#90)
96
+ - The `agent_kernel` module docstring's `Errors::` block now lists every
97
+ exported error class — added `TokenRevoked`, `AdapterParseError`,
98
+ `CapabilityAlreadyRegistered`, `HandleConstraintViolation`,
99
+ `ManifestSignatureError`, and `DiscoveryError`. (#91)
100
+
101
+ ### Tests
102
+ - Added explicit dry-run regression tests for `HTTPDriver` and `MCPDriver`,
103
+ pinning the kernel's driver-agnostic dry-run short-circuit. (#68)
104
+ - `tests/test_public_api.py` — asserts every error class exported via `__all__`
105
+ appears in the `agent_kernel` module docstring, preventing public-API
106
+ docstring drift. (#91)
107
+ - `tests/test_readme_quickstart.py` — extracts the README quickstart code block
108
+ and executes it, asserting the documented output so the inline snippet cannot
109
+ silently drift from the working API. (#83)
110
+
111
+ ### Fixed
112
+ - `merge_sensitivity()` (and `most_restrictive` imports) now ranks the `MEMORY`
113
+ sensitivity tag instead of silently treating it as `NONE`. (#52)
114
+ - `import_manifest()` is now atomic: a manifest whose capability ID is already
115
+ registered locally — or that lists the same ID more than once — raises
116
+ `ManifestError` and registers nothing, instead of leaving a partial,
117
+ unrouted import behind. (#52)
118
+ - `CapabilityRegistry.search()` now triggers every pending deferred namespace
119
+ loader before ranking, so matches are no longer missed when the query shares
120
+ no token with a namespace prefix. (#45)
121
+ - A deferred namespace loader that fails (by raising or returning an
122
+ out-of-namespace capability) no longer permanently disables the namespace —
123
+ the load is retried on a later access. (#45)
124
+ - `Makefile` now invokes every tool via `python -m <tool>` (matching the
125
+ existing `test` target) so `make ci` uses the active interpreter's
126
+ site-packages instead of whatever `ruff` / `mypy` resolves first on
127
+ `PATH`. Fixes spurious `import-not-found` / `no-redef` errors on machines
128
+ where `mypy` or `ruff` is provided by an isolated installer such as
129
+ `uv tool` or `pipx`. (#86)
130
+ - `make ci` now runs the non-mutating `fmt-check` target (`ruff format
131
+ --check`) instead of the file-mutating `fmt` target. Local `make ci`
132
+ now fails on unformatted code exactly like `.github/workflows/ci.yml`
133
+ does, instead of silently auto-fixing the working tree and letting an
134
+ unfixed commit be pushed. `make fmt` remains available for manual
135
+ auto-formatting. `AGENTS.md`, `docs/agent-context/workflows.md`,
136
+ `docs/agent-context/review-checklist.md`, `CONTRIBUTING.md`, and the
137
+ `README.md` development section are updated to describe the new chain. (#88)
138
+
10
139
  ## [0.8.0] - 2026-05-22
11
140
 
12
141
  ### Added
@@ -15,11 +15,12 @@ pip install -e ".[dev]"
15
15
  ## Running checks
16
16
 
17
17
  ```bash
18
- make fmt # auto-format with ruff
19
- make lint # lint with ruff
20
- make type # type-check with mypy
21
- make test # run pytest with coverage
22
- make ci # all of the above + examples
18
+ make fmt # auto-format with ruff (not run by `make ci`)
19
+ make fmt-check # verify formatting with `ruff format --check` (no mutation)
20
+ make lint # lint with ruff
21
+ make type # type-check with mypy
22
+ make test # run pytest with coverage
23
+ make ci # fmt-check + lint + type + test + example
23
24
  ```
24
25
 
25
26
  ## Pull request guidelines
@@ -0,0 +1,25 @@
1
+ .PHONY: fmt fmt-check lint type test example ci
2
+
3
+ fmt:
4
+ python -m ruff format src/ tests/ examples/
5
+
6
+ fmt-check:
7
+ python -m ruff format --check src/ tests/ examples/
8
+
9
+ lint:
10
+ python -m ruff check src/ tests/ examples/
11
+
12
+ type:
13
+ python -m mypy src/
14
+
15
+ test:
16
+ python -m pytest -q --cov=agent_kernel
17
+
18
+ example:
19
+ python examples/basic_cli.py
20
+ python examples/billing_demo.py
21
+ python examples/http_driver_demo.py
22
+ python examples/tutorial.py
23
+ python examples/readme_quickstart.py
24
+
25
+ ci: fmt-check lint type test example
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: weaver-kernel
3
- Version: 0.8.0
3
+ Version: 0.9.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
@@ -296,10 +296,9 @@ import asyncio, os
296
296
  os.environ["AGENT_KERNEL_SECRET"] = "my-secret"
297
297
 
298
298
  from agent_kernel import (
299
- Capability, CapabilityRegistry, HMACTokenProvider,
299
+ Capability, CapabilityRegistry,
300
300
  InMemoryDriver, Kernel, Principal, SafetyClass, StaticRouter,
301
301
  )
302
- from agent_kernel.drivers.base import ExecutionContext
303
302
  from agent_kernel.models import CapabilityRequest
304
303
 
305
304
  # 1. Register a capability
@@ -332,7 +331,11 @@ async def main():
332
331
  print(frame.facts) # ['Total rows: 1', 'Top keys: id, title', ...]
333
332
  print(frame.handle) # Handle(handle_id='...', ...)
334
333
 
335
- expanded = kernel.expand(frame.handle, query={"limit": 1, "fields": ["title"]})
334
+ # `principal` is required: the handle is bound to the granting principal,
335
+ # so an omitted principal raises HandleConstraintViolation.
336
+ expanded = kernel.expand(
337
+ frame.handle, query={"limit": 1, "fields": ["title"]}, principal=principal
338
+ )
336
339
  print(expanded.table_preview) # [{'title': 'Buy milk'}]
337
340
 
338
341
  trace = kernel.explain(frame.action_id)
@@ -341,6 +344,12 @@ async def main():
341
344
  asyncio.run(main())
342
345
  ```
343
346
 
347
+ > This snippet is extracted and executed by CI (`tests/test_readme_quickstart.py`), and
348
+ > a standalone runnable mirror lives at
349
+ > [`examples/readme_quickstart.py`](examples/readme_quickstart.py) (run by `make example`).
350
+ > CI fails if either stops producing the documented output, so this quickstart cannot
351
+ > silently drift from the working API.
352
+
344
353
  ## Where it fits
345
354
 
346
355
  ```
@@ -433,7 +442,7 @@ See [docs/agent-context/invariants.md](docs/agent-context/invariants.md) for the
433
442
  git clone https://github.com/dgenio/agent-kernel
434
443
  cd agent-kernel
435
444
  pip install -e ".[dev]"
436
- make ci # fmt + lint + type + test + examples
445
+ make ci # fmt-check + lint + type + test + examples
437
446
  ```
438
447
 
439
448
  ## License
@@ -50,10 +50,9 @@ import asyncio, os
50
50
  os.environ["AGENT_KERNEL_SECRET"] = "my-secret"
51
51
 
52
52
  from agent_kernel import (
53
- Capability, CapabilityRegistry, HMACTokenProvider,
53
+ Capability, CapabilityRegistry,
54
54
  InMemoryDriver, Kernel, Principal, SafetyClass, StaticRouter,
55
55
  )
56
- from agent_kernel.drivers.base import ExecutionContext
57
56
  from agent_kernel.models import CapabilityRequest
58
57
 
59
58
  # 1. Register a capability
@@ -86,7 +85,11 @@ async def main():
86
85
  print(frame.facts) # ['Total rows: 1', 'Top keys: id, title', ...]
87
86
  print(frame.handle) # Handle(handle_id='...', ...)
88
87
 
89
- expanded = kernel.expand(frame.handle, query={"limit": 1, "fields": ["title"]})
88
+ # `principal` is required: the handle is bound to the granting principal,
89
+ # so an omitted principal raises HandleConstraintViolation.
90
+ expanded = kernel.expand(
91
+ frame.handle, query={"limit": 1, "fields": ["title"]}, principal=principal
92
+ )
90
93
  print(expanded.table_preview) # [{'title': 'Buy milk'}]
91
94
 
92
95
  trace = kernel.explain(frame.action_id)
@@ -95,6 +98,12 @@ async def main():
95
98
  asyncio.run(main())
96
99
  ```
97
100
 
101
+ > This snippet is extracted and executed by CI (`tests/test_readme_quickstart.py`), and
102
+ > a standalone runnable mirror lives at
103
+ > [`examples/readme_quickstart.py`](examples/readme_quickstart.py) (run by `make example`).
104
+ > CI fails if either stops producing the documented output, so this quickstart cannot
105
+ > silently drift from the working API.
106
+
98
107
  ## Where it fits
99
108
 
100
109
  ```
@@ -187,7 +196,7 @@ See [docs/agent-context/invariants.md](docs/agent-context/invariants.md) for the
187
196
  git clone https://github.com/dgenio/agent-kernel
188
197
  cd agent-kernel
189
198
  pip install -e ".[dev]"
190
- make ci # fmt + lint + type + test + examples
199
+ make ci # fmt-check + lint + type + test + examples
191
200
  ```
192
201
 
193
202
  ## License
@@ -39,7 +39,7 @@ The architecture optimizes for:
39
39
  | Tokens signed, not encrypted | Simplicity; avoids key management complexity | Payloads are readable — never store secrets in them |
40
40
  | Keyword-based capability search | Deterministic; no external service dependency | Less flexible than semantic search; relies on good tagging |
41
41
  | Firewall is mandatory | Prevents accidental context blowup and data leakage | All output is bounded; debugging requires admin `raw` mode |
42
- | Single dep (`httpx` only) | Minimal attack surface for a security kernel | Adding a dependency requires justification |
42
+ | Minimal deps (`httpx` + `pydantic`) | Small attack surface for a security kernel | Adding a dependency requires justification (see `AGENTS.md`) |
43
43
 
44
44
  ## Things not to simplify
45
45
 
@@ -40,7 +40,8 @@ These constraints are non-negotiable. Violating any one silently degrades securi
40
40
  derived keys must stay out of logs, error messages, and traces.
41
41
 
42
42
  6. **Never add dependencies without justification.** The dependency list is intentionally
43
- minimal (`httpx` only). Every new dependency expands the attack surface.
43
+ minimal (`httpx` + `pydantic` see [`AGENTS.md`](../../AGENTS.md) for the canonical
44
+ dependency policy). Every new dependency expands the attack surface.
44
45
 
45
46
  7. **Never register duplicate capability IDs.** The registry raises
46
47
  `CapabilityAlreadyRegistered`. Duplicates cause ambiguous routing and policy
@@ -10,7 +10,7 @@
10
10
  Run before every PR submission.
11
11
 
12
12
  ### CI gate
13
- - [ ] `make ci` passes (fmt → lint → type → test → example).
13
+ - [ ] `make ci` passes (fmt-check → lint → type → test → example).
14
14
 
15
15
  ### Correctness
16
16
  - [ ] Every changed docstring matches the actual implementation.
@@ -7,18 +7,24 @@
7
7
 
8
8
  | Command | Purpose | When to run |
9
9
  |---------|---------|-------------|
10
- | `make ci` | Full pre-push gate: fmt → lint → type → test → example | Before every push |
11
- | `make fmt` | Auto-format with ruff | During development |
10
+ | `make ci` | Full pre-push gate: fmt-check → lint → type → test → example | Before every push |
11
+ | `make fmt` | Auto-format with ruff (mutates files) | During development |
12
+ | `make fmt-check` | Verify formatting with `ruff format --check` (no mutation) | Used by `make ci`; matches what CI runs |
12
13
  | `make lint` | Lint check with ruff | Isolated lint verification |
13
14
  | `make type` | mypy type check | After changing type annotations |
14
15
  | `make test` | pytest with coverage | After changing code |
15
16
  | `make example` | Run all example scripts | After changing examples or core APIs |
16
17
 
17
18
  `make ci` is the **single authoritative pre-push command**. It runs all five targets
18
- in sequence. If `make ci` passes, the PR is ready for review.
19
-
20
- **Note:** `make fmt` auto-formats locally, but CI runs `ruff format --check` and fails
21
- on unformatted code. Always run `make ci` to catch this asymmetry.
19
+ in sequence and mirrors the checks in the `test` job of `.github/workflows/ci.yml`: the
20
+ format step is the non-mutating `fmt-check` (equivalent to CI's `ruff format --check`),
21
+ and lint/type/test/example run the same tools CI does. (CI's separate `conformance_stub`
22
+ job is a no-op placeholder and is not part of the local gate.) The Makefile
23
+ additionally invokes every tool via `python -m <tool>` — a local hardening over CI
24
+ that uses the active interpreter's site-packages, preventing spurious failures when
25
+ `ruff` or `mypy` are provided by isolated installers such as `uv tool` or `pipx`. If
26
+ `make ci` passes locally, the same checks will pass in CI. Use `make fmt` (the
27
+ mutating target) when you want to auto-fix formatting before re-running `make ci`.
22
28
 
23
29
  ## PR conventions
24
30
 
@@ -31,7 +31,7 @@ The central orchestrator. Wires all components together and exposes:
31
31
  - `request_capabilities(goal)` — discover relevant capabilities
32
32
  - `grant_capability(request, principal, justification)` — policy check + token issuance
33
33
  - `invoke(token, principal, args, response_mode, dry_run=False)` — execute + firewall + trace, or short-circuit before driver dispatch when `dry_run=True`
34
- - `expand(handle, query)` — paginate/filter stored results
34
+ - `expand(handle, *, query, principal=None)` — paginate/filter stored results; `principal` is required for principal-bound handles (see [`docs/security.md`](security.md#handle-expansion-boundary))
35
35
  - `explain(action_id)` — retrieve audit trace
36
36
  - `explain_denial(request, principal, justification)` — return a structured `DenialExplanation` instead of raising `PolicyDenied`
37
37
 
@@ -3,9 +3,60 @@
3
3
  ## Naming conventions
4
4
 
5
5
  - Use `domain.verb_noun` format: `billing.list_invoices`, `users.get_profile`.
6
+ - Prefer fully namespaced IDs (`billing.invoices.list`) over flat ones —
7
+ the registry will infer namespace operations from the dot-segments and
8
+ large ecosystems benefit from being able to list/search per namespace.
6
9
  - Be specific: prefer `billing.cancel_invoice` over `billing.update`.
7
10
  - Avoid generic names like `billing.execute` or `api.call`.
8
11
 
12
+ ## Namespaces and discovery
13
+
14
+ `CapabilityRegistry` recognises dot-notation namespaces automatically. No
15
+ extra registration step is required — `register(Capability(capability_id=
16
+ "billing.invoices.list", ...))` is enough to populate the `billing` and
17
+ `billing.invoices` namespaces.
18
+
19
+ ```python
20
+ registry.list_namespaces()
21
+ # ['billing', 'crm']
22
+
23
+ registry.list_namespace("billing")
24
+ # [Capability('billing.invoices.list'), Capability('billing.payments.refund'), …]
25
+ ```
26
+
27
+ For large tool ecosystems where eagerly registering hundreds of
28
+ capabilities is wasteful, declare a deferred loader. The loader runs at
29
+ most once, the first time the namespace is searched, listed, or any
30
+ capability under it is fetched via `get()`:
31
+
32
+ ```python
33
+ def load_billing() -> list[Capability]:
34
+ return [
35
+ Capability(capability_id="billing.invoices.list", …),
36
+ Capability(capability_id="billing.invoices.create", …),
37
+ Capability(capability_id="billing.payments.refund", …),
38
+ ]
39
+
40
+ registry.register_namespace(
41
+ "billing",
42
+ description="Billing and invoicing tools",
43
+ loader=load_billing,
44
+ )
45
+ ```
46
+
47
+ Search ranks matches with a BM25-flavoured scorer that weights
48
+ `capability_id` and `tags` higher than `description`, strips a small
49
+ stop-word set (`a`, `the`, `please`, …), and offers `offset` for
50
+ pagination:
51
+
52
+ ```python
53
+ results = registry.search("list invoices", max_results=10, offset=0)
54
+ ```
55
+
56
+ Search is deterministic — equal-scoring capabilities are returned in
57
+ `capability_id` order — and trips any deferred namespace loader whose
58
+ prefix shares a token with the query.
59
+
9
60
  ## Granularity
10
61
 
11
62
  Each capability should map to a single, auditable action with clear side-effects.
@@ -32,6 +83,10 @@ Each capability should map to a single, auditable action with clear side-effects
32
83
  Use `SensitivityTag.PII` when results may contain: name, email, phone, SSN, address.
33
84
  Use `SensitivityTag.PCI` when results may contain: card numbers, CVV, bank details.
34
85
  Use `SensitivityTag.SECRETS` when results may contain: API keys, passwords, tokens.
86
+ Use `SensitivityTag.MEMORY` when results are durable agent memory (project notes,
87
+ session handoff, learned context). Pair it with the `memory.read` / `memory.write` /
88
+ `memory.forget` capability IDs to activate the policy rules and audit-trace redaction
89
+ described in [`docs/security.md#memory-actions`](security.md#memory-actions).
35
90
 
36
91
  Always pair sensitivity tags with `allowed_fields` to restrict which fields are returned
37
92
  to non-privileged callers.
@@ -32,18 +32,23 @@ Budgets(
32
32
 
33
33
  A `Handle` is an opaque reference to the full dataset stored server-side.
34
34
 
35
+ A handle is bound to the principal it was granted to, so `expand()` requires that
36
+ same `principal` — an omitted or mismatched principal raises
37
+ `HandleConstraintViolation` (handle IDs are not bearer credentials). See
38
+ [`docs/security.md#handle-expansion-boundary`](security.md#handle-expansion-boundary).
39
+
35
40
  ```python
36
41
  # Stored automatically on every invoke()
37
42
  handle = frame.handle
38
43
 
39
44
  # Expand with pagination
40
- expanded = kernel.expand(handle, query={"offset": 10, "limit": 5})
45
+ expanded = kernel.expand(handle, query={"offset": 10, "limit": 5}, principal=principal)
41
46
 
42
47
  # Field selection
43
- expanded = kernel.expand(handle, query={"fields": ["id", "name"]})
48
+ expanded = kernel.expand(handle, query={"fields": ["id", "name"]}, principal=principal)
44
49
 
45
50
  # Basic filtering
46
- expanded = kernel.expand(handle, query={"filter": {"status": "unpaid"}})
51
+ expanded = kernel.expand(handle, query={"filter": {"status": "unpaid"}}, principal=principal)
47
52
  ```
48
53
 
49
54
  ## Redaction
@@ -118,3 +123,59 @@ manager = BudgetManager(total_budget=128_000, token_counter=tiktoken_counter)
118
123
 
119
124
  The default counter (`default_token_counter`) is a character-based
120
125
  `len(json.dumps(value)) // 4` approximation with no extra dependencies.
126
+
127
+ ## Streaming
128
+
129
+ For large results that arrive incrementally (e.g. SSE-style HTTP responses,
130
+ chunked database cursors, line-by-line tool output), `Firewall.apply_stream()`
131
+ lets you process chunks one at a time. PII redaction and per-chunk budget
132
+ caps apply on every yielded Frame — secrets cannot leak just because they
133
+ arrived in chunk N rather than the final aggregate.
134
+
135
+ ```python
136
+ from agent_kernel.drivers.base import ExecutionContext, StreamingDriver
137
+
138
+ class MyStreamingDriver:
139
+ driver_id = "stream"
140
+
141
+ async def execute(self, ctx: ExecutionContext):
142
+ # one-shot fallback, called when StreamingDriver isn't used.
143
+ ...
144
+
145
+ async def execute_stream(self, ctx: ExecutionContext):
146
+ async for row in some_async_cursor(ctx):
147
+ yield {"row": row}
148
+ yield {"__is_final__": True} # explicit sentinel (optional)
149
+
150
+
151
+ # isinstance(driver, StreamingDriver) is runtime-checkable.
152
+ assert isinstance(MyStreamingDriver(), StreamingDriver)
153
+
154
+ async for frame in kernel.invoke_stream(token, principal=p, args={}):
155
+ handle_chunk(frame)
156
+ if frame.is_final:
157
+ break
158
+ ```
159
+
160
+ When the resolved driver does **not** implement `StreamingDriver`,
161
+ `Kernel.invoke_stream` falls back to a single `Driver.execute()` call and
162
+ yields exactly one `Frame` with `is_final=True`. Each invocation produces
163
+ one `ActionTrace` covering the whole stream.
164
+
165
+ ## Observability
166
+
167
+ `agent_kernel.instrument_kernel(kernel)` installs OpenTelemetry spans and
168
+ metric emission on `Kernel.invoke` and `Kernel.grant_capability`:
169
+
170
+ ```python
171
+ from agent_kernel import Kernel, instrument_kernel, OTEL_AVAILABLE
172
+
173
+ kernel = Kernel(registry=...)
174
+ if OTEL_AVAILABLE:
175
+ instrument_kernel(kernel) # no-op when [otel] extra not installed
176
+ ```
177
+
178
+ Spans: `agent_kernel.invoke`, `agent_kernel.grant`. Metrics:
179
+ `agent_kernel.invocations` (counter), `agent_kernel.invocation_duration`
180
+ (histogram, ms), `agent_kernel.policy_denials` (counter). The call is
181
+ idempotent — repeat invocations on the same kernel are no-ops.