Coverage for node / src / stigmem_node / plugins / context.py: 80%
59 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-25 01:49 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-25 01:49 +0000
1"""Capability-gated plugin context."""
3from __future__ import annotations
5from dataclasses import dataclass
6from typing import Any
8from .errors import CapabilityError
11@dataclass(frozen=True, slots=True)
12class CoreApis:
13 """Handles core chooses to expose to plugins.
15 PR 4-INF.1 keeps this deliberately small and optional; later PRs can replace
16 these generic handles with narrower typed facades without changing the
17 registry contract.
18 """
20 facts_reader: Any = None
21 facts_writer: Any = None
22 recall_reader: Any = None
23 recall_writer: Any = None
24 audit_emitter: Any = None
25 audit_reader: Any = None
26 federation_reader: Any = None
27 federation_writer: Any = None
28 identity_reader: Any = None
29 tenant_reader: Any = None
30 tenant_writer: Any = None
31 config_reader: Any = None
32 network_outbound: Any = None
35class PluginContext:
36 """Capability-restricted context passed to plugin handlers."""
38 __slots__ = ("_capabilities", "_core_apis", "plugin_name", "plugin_version")
40 def __init__(
41 self,
42 *,
43 plugin_name: str,
44 capabilities: frozenset[str],
45 plugin_version: str = "0.0.0",
46 core_apis: CoreApis | None = None,
47 ) -> None:
48 self.plugin_name = plugin_name
49 self.plugin_version = plugin_version
50 self._capabilities = capabilities
51 self._core_apis = core_apis or CoreApis()
53 @property
54 def capabilities(self) -> frozenset[str]:
55 return self._capabilities
57 def _require(self, capability: str, accessor: str, value: Any) -> Any:
58 if capability not in self._capabilities:
59 raise CapabilityError(
60 f"plugin {self.plugin_name!r} cannot call {accessor}: "
61 f"capability {capability!r} not declared"
62 )
63 return value
65 def get_facts_reader(self) -> Any:
66 return self._require("facts.read", "get_facts_reader", self._core_apis.facts_reader)
68 def get_facts_writer(self) -> Any:
69 return self._require("facts.write", "get_facts_writer", self._core_apis.facts_writer)
71 def get_recall_reader(self) -> Any:
72 return self._require("recall.read", "get_recall_reader", self._core_apis.recall_reader)
74 def get_recall_writer(self) -> Any:
75 return self._require("recall.write", "get_recall_writer", self._core_apis.recall_writer)
77 def get_audit_emitter(self) -> Any:
78 return self._require("audit.emit", "get_audit_emitter", self._core_apis.audit_emitter)
80 def get_audit_reader(self) -> Any:
81 return self._require("audit.read", "get_audit_reader", self._core_apis.audit_reader)
83 def get_federation_reader(self) -> Any:
84 return self._require(
85 "federation.read", "get_federation_reader", self._core_apis.federation_reader
86 )
88 def get_federation_writer(self) -> Any:
89 return self._require(
90 "federation.write", "get_federation_writer", self._core_apis.federation_writer
91 )
93 def get_identity_reader(self) -> Any:
94 return self._require(
95 "identity.read", "get_identity_reader", self._core_apis.identity_reader
96 )
98 def get_tenant_reader(self) -> Any:
99 return self._require("tenant.read", "get_tenant_reader", self._core_apis.tenant_reader)
101 def get_tenant_writer(self) -> Any:
102 return self._require("tenant.write", "get_tenant_writer", self._core_apis.tenant_writer)
104 def get_config_reader(self) -> Any:
105 return self._require("config.read", "get_config_reader", self._core_apis.config_reader)
107 def get_network_outbound(self) -> Any:
108 return self._require(
109 "network.outbound", "get_network_outbound", self._core_apis.network_outbound
110 )