feat(registry,federation): namespaces, BM25 search, capability manifests#81
Open
dgenio wants to merge 1 commit into
Open
feat(registry,federation): namespaces, BM25 search, capability manifests#81dgenio wants to merge 1 commit into
dgenio wants to merge 1 commit into
Conversation
Implements the capability-discovery group from triage: #45 (namespaces & hierarchical discovery) + #52 (marketplace part 1 — manifest format & local registry). Both extend the same discovery seam and share models.py. #45 — CapabilityRegistry - Dot-notation `capability_id`s now expose `list_namespaces()` and `list_namespace(prefix)`. Flat IDs continue to work unchanged. - `register_namespace(prefix, loader=...)` enables deferred registration for large tool ecosystems; the loader runs at most once on first access (search/list/get). - `search()` gained an `offset` kwarg, strips a small stop-word set, and now scores with a BM25-flavoured ranker that weights matches on `capability_id` and `tags` above `description`. Determinism preserved via stable `capability_id` tie-breaking — required by AGENTS.md. #52 — Capability marketplace (local) - New `CapabilityDescriptor` and `CapabilityManifest` dataclasses with `to_dict`/`from_dict` for JSON portability. Internal driver IDs and operation names are stripped on serialisation. - New `agent_kernel.federation` module: `build_manifest()`, `import_manifest()`, `merge_sensitivity()`. Three trust policies honoured at import time (`most_restrictive` default, `local_only`, `remote_deferred`). - `Kernel.advertise(endpoint=...)` and `Kernel.import_remote(manifest, driver=..., trust_policy=...)` thin wrappers; `Kernel` gained a `kernel_id` kwarg used as the publisher identity (defaults to `"agent-kernel"`). - Imported capabilities flow through the full local pipeline (policy → token → firewall → trace). HMAC tokens remain kernel-scoped — a token from a kernel with a different secret fails signature check. New errors in `errors.py`: `NamespaceNotFound`, `FederationError`, `ManifestError`, `TrustPolicyError`. Tests: +32 net (450 → 482). New `tests/test_federation.py` covers manifest round-trip, descriptor stripping, all three trust policies, end-to-end imported-capability invocation, and kernel-scoped HMAC isolation. `tests/test_registry.py` extended with namespace ops, deferred-loader semantics, BM25 ranking, pagination, stop-word stripping, and a 500-capability scale sanity check. Docs: new `docs/federation.md`, namespace section added to `docs/capabilities.md`, CHANGELOG `[Unreleased]` entries. Out of scope (left for follow-ups, per the triage report): - Discovery over a network transport (#51, marketplace part 2). - Decomposition of `policy_dsl.py` / `kernel.py` / `models.py` beyond AGENTS.md's 300-line budget (#68). Closes #45 Closes #52 https://claude.ai/code/session_015PFMci84T2TBNhqKsV3Hyo
There was a problem hiding this comment.
Pull request overview
This PR extends agent-kernel’s capability discovery seam with (1) hierarchical namespaces + deferred namespace loaders + improved ranked search in CapabilityRegistry, and (2) a local “capability marketplace” foundation via serializable manifests that can be advertised/imported while preserving the local policy→token→firewall pipeline.
Changes:
- Added namespace operations to
CapabilityRegistry(list_namespaces,list_namespace,register_namespace) plus deferred loader support. - Replaced keyword-overlap scoring with a deterministic BM25-flavoured scorer, added stop-word stripping and
offsetpagination. - Introduced federation/marketplace primitives:
CapabilityDescriptor/CapabilityManifest,build_manifest/import_manifest, andKernel.advertise/Kernel.import_remote, with new federation-related errors and docs.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_registry.py | Adds coverage for namespaces, deferred loaders, BM25 ranking, pagination, stop words, and a 500-capability scale sanity check. |
| tests/test_federation.py | New tests for manifest round-trips, stripping internal fields, trust policies, imported invocation flow, and HMAC token isolation. |
| src/agent_kernel/registry.py | Implements namespaces, deferred namespace loading, BM25-style ranked search, stop-word stripping, and pagination. |
| src/agent_kernel/models.py | Adds NamespaceMetadata, CapabilityDescriptor, and CapabilityManifest models with to_dict/from_dict. |
| src/agent_kernel/kernel.py | Adds kernel_id, plus advertise() and import_remote() wrappers to publish/import manifests. |
| src/agent_kernel/federation.py | New local federation module implementing manifest build/import and sensitivity merge logic. |
| src/agent_kernel/errors.py | Adds NamespaceNotFound and federation-specific error types. |
| src/agent_kernel/init.py | Re-exports federation models/functions/errors from the top-level package. |
| docs/federation.md | Documents the marketplace part 1 workflow, trust policies, and non-goals. |
| docs/capabilities.md | Documents namespaces, deferred loading, and new search behavior. |
| CHANGELOG.md | Adds Unreleased entries describing the new registry and federation features. |
| NamespaceNotFound: If no declared namespace or registered capability | ||
| lives under *prefix*. | ||
| """ | ||
| self._maybe_load_namespace(prefix) |
Comment on lines
+364
to
+366
| head, _, _ = capability_id.partition(".") | ||
| candidates = [head, capability_id] | ||
| for prefix in candidates: |
Comment on lines
+370
to
+380
| def _maybe_load_namespace(self, prefix: str) -> None: | ||
| """Invoke the deferred loader for *prefix* if it has not run yet.""" | ||
| meta = self._namespaces.get(prefix) | ||
| if meta is None or meta.loaded or meta.loader is None: | ||
| return | ||
| loader = meta.loader | ||
| # Mark as loaded *before* calling so a recursive load doesn't re-enter. | ||
| meta.loaded = True | ||
| for cap in loader(): | ||
| self.register(cap) | ||
|
|
| scored.append((score, cap)) | ||
|
|
||
| scored.sort(key=lambda x: (-x[0], x[1].capability_id)) | ||
|
|
Comment on lines
+1
to
+8
| """Capability registry: register, lookup, namespaced discovery, and ranked search. | ||
|
|
||
| Supports dot-notation namespaces (``"billing.invoices.list"``), deferred | ||
| namespace loaders for large tool ecosystems, and a BM25-flavoured score | ||
| that weights matches on ``capability_id`` and ``tags`` higher than | ||
| ``description``. Flat (un-namespaced) capability IDs continue to work — they | ||
| are treated as living in a single-segment namespace. | ||
| """ |
| """Dot-notation namespace prefix (e.g. ``"billing"`` or ``"billing.invoices"``).""" | ||
|
|
||
| description: str = "" | ||
| """Optional human-readable description shown by ``list_namespaces``.""" |
Comment on lines
+527
to
+532
| return cls( | ||
| capability_id=data["capability_id"], | ||
| name=data["name"], | ||
| description=data["description"], | ||
| safety_class=SafetyClass(data["safety_class"]), | ||
| sensitivity=SensitivityTag(data.get("sensitivity", SensitivityTag.NONE.value)), |
Comment on lines
+592
to
+597
| return cls( | ||
| kernel_id=data["kernel_id"], | ||
| version=data["version"], | ||
| endpoint=data["endpoint"], | ||
| trust_level=data.get("trust_level", "unverified"), | ||
| capabilities=[CapabilityDescriptor.from_dict(c) for c in data["capabilities"]], |
Comment on lines
+687
to
+691
| # Route each imported capability to its driver so existing | ||
| # ``Kernel.invoke`` works unchanged. | ||
| router_add = getattr(self._router, "add_route", None) | ||
| if router_add is not None: | ||
| for cap in imported: |
Comment on lines
+299
to
+305
| start = time.perf_counter() | ||
| results = reg.search("billing thing", max_results=10) | ||
| elapsed = time.perf_counter() - start | ||
| assert len(results) == 10 | ||
| # Generous bound: BM25 over 500 docs with ~5 tokens each should be | ||
| # well under a second on any developer machine. | ||
| assert elapsed < 1.0, f"search took {elapsed:.3f}s for 500 capabilities" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the capability-discovery group from triage: #45 (namespaces &
hierarchical discovery) + #52 (marketplace part 1 — manifest format &
local registry). Both extend the same discovery seam and share models.py.
#45 — CapabilityRegistry
capability_ids now exposelist_namespaces()andlist_namespace(prefix). Flat IDs continue to work unchanged.register_namespace(prefix, loader=...)enables deferred registrationfor large tool ecosystems; the loader runs at most once on first
access (search/list/get).
search()gained anoffsetkwarg, strips a small stop-word set,and now scores with a BM25-flavoured ranker that weights matches on
capability_idandtagsabovedescription. Determinism preservedvia stable
capability_idtie-breaking — required by AGENTS.md.#52 — Capability marketplace (local)
CapabilityDescriptorandCapabilityManifestdataclasses withto_dict/from_dictfor JSON portability. Internal driver IDs andoperation names are stripped on serialisation.
agent_kernel.federationmodule:build_manifest(),import_manifest(),merge_sensitivity(). Three trust policieshonoured at import time (
most_restrictivedefault,local_only,remote_deferred).Kernel.advertise(endpoint=...)andKernel.import_remote(manifest, driver=..., trust_policy=...)thin wrappers;Kernelgained akernel_idkwarg used as the publisher identity (defaults to"agent-kernel").→ token → firewall → trace). HMAC tokens remain kernel-scoped — a
token from a kernel with a different secret fails signature check.
New errors in
errors.py:NamespaceNotFound,FederationError,ManifestError,TrustPolicyError.Tests: +32 net (450 → 482). New
tests/test_federation.pycoversmanifest round-trip, descriptor stripping, all three trust policies,
end-to-end imported-capability invocation, and kernel-scoped HMAC
isolation.
tests/test_registry.pyextended with namespace ops,deferred-loader semantics, BM25 ranking, pagination, stop-word
stripping, and a 500-capability scale sanity check.
Docs: new
docs/federation.md, namespace section added todocs/capabilities.md, CHANGELOG[Unreleased]entries.Out of scope (left for follow-ups, per the triage report):
policy_dsl.py/kernel.py/models.pybeyond AGENTS.md's 300-line budget ([policy/kernel] Tech debt: decompose policy_dsl.py and broaden dry-run driver test coverage #68).
Closes #45
Closes #52
https://claude.ai/code/session_015PFMci84T2TBNhqKsV3Hyo