chp-core 0.1.0__py3-none-any.whl

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.
chp_core/__init__.py ADDED
@@ -0,0 +1,98 @@
1
+ """CHP v0.1 reference local host."""
2
+
3
+ from .adapters import (
4
+ CHP_ADAPTER_GROUP,
5
+ BaseAdapter,
6
+ CapabilityAdapter,
7
+ HostedCapability,
8
+ SimpleAdapter,
9
+ auto_register_adapters,
10
+ discover_adapters,
11
+ register_adapter,
12
+ register_capability_once,
13
+ register_hosted_capabilities,
14
+ )
15
+ from .capabilities import (
16
+ register_builtin_capabilities,
17
+ register_evaluate_counterfactual,
18
+ register_explain_execution,
19
+ register_trace_execution,
20
+ )
21
+ from .host import CapabilityExecutionContext, LocalCapabilityHost
22
+ from .http import CapabilityHostHTTPServer, create_http_server, serve_http
23
+ from .store import SQLiteEvidenceStore
24
+ from .decorators import capability
25
+ from .codex import (
26
+ CODEX_CAPABILITY_IDS,
27
+ record_codex_action,
28
+ register_codex_observation_capabilities,
29
+ )
30
+ from .otel import evidence_to_otel_span, replay_to_otel_spans
31
+ from .redaction import DEFAULT_SENSITIVE_KEYS, redact_payload
32
+ from .types import (
33
+ AssuranceMetadata,
34
+ CapabilityCategory,
35
+ CapabilityDescriptor,
36
+ CapabilityIdempotency,
37
+ CapabilityStatus,
38
+ CorrelationContext,
39
+ DenialReason,
40
+ ExecutionEvidence,
41
+ ExecutionOutcome,
42
+ HostDescriptor,
43
+ HostRequirements,
44
+ InvariantDescriptor,
45
+ InvocationEnvelope,
46
+ InvocationResult,
47
+ PolicyDescriptor,
48
+ ReplayQuery,
49
+ ReplayResult,
50
+ )
51
+
52
+ __all__ = [
53
+ "AssuranceMetadata",
54
+ "BaseAdapter",
55
+ "CHP_ADAPTER_GROUP",
56
+ "CODEX_CAPABILITY_IDS",
57
+ "CapabilityAdapter",
58
+ "CapabilityCategory",
59
+ "CapabilityIdempotency",
60
+ "CapabilityStatus",
61
+ "CapabilityDescriptor",
62
+ "CapabilityHostHTTPServer",
63
+ "CapabilityExecutionContext",
64
+ "capability",
65
+ "CorrelationContext",
66
+ "DEFAULT_SENSITIVE_KEYS",
67
+ "DenialReason",
68
+ "ExecutionEvidence",
69
+ "ExecutionOutcome",
70
+ "HostedCapability",
71
+ "HostDescriptor",
72
+ "HostRequirements",
73
+ "InvariantDescriptor",
74
+ "InvocationEnvelope",
75
+ "InvocationResult",
76
+ "LocalCapabilityHost",
77
+ "PolicyDescriptor",
78
+ "ReplayQuery",
79
+ "ReplayResult",
80
+ "SQLiteEvidenceStore",
81
+ "create_http_server",
82
+ "evidence_to_otel_span",
83
+ "redact_payload",
84
+ "SimpleAdapter",
85
+ "auto_register_adapters",
86
+ "discover_adapters",
87
+ "register_adapter",
88
+ "register_builtin_capabilities",
89
+ "register_capability_once",
90
+ "record_codex_action",
91
+ "register_codex_observation_capabilities",
92
+ "register_evaluate_counterfactual",
93
+ "register_explain_execution",
94
+ "register_hosted_capabilities",
95
+ "register_trace_execution",
96
+ "replay_to_otel_spans",
97
+ "serve_http",
98
+ ]
chp_core/adapters.py ADDED
@@ -0,0 +1,252 @@
1
+ """Adapter primitives for grouping and registering CHP capabilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from collections.abc import Iterable, Sequence
7
+ from dataclasses import dataclass
8
+ from typing import Any, Protocol
9
+
10
+ from .decorators import adapt_callable, get_capability_descriptor
11
+ from .host import CapabilityHandler, LocalCapabilityHost
12
+ from .types import CapabilityDescriptor
13
+
14
+
15
+ @dataclass(slots=True)
16
+ class HostedCapability:
17
+ """A capability descriptor and handler supplied by an adapter."""
18
+
19
+ descriptor: CapabilityDescriptor
20
+ handler: CapabilityHandler
21
+ enabled: bool = True
22
+
23
+
24
+ class CapabilityAdapter(Protocol):
25
+ """Structural protocol for CHP capability adapters.
26
+
27
+ Any object with ``adapter_id`` and ``capabilities()`` satisfies this
28
+ protocol and can be passed to ``register_adapter``.
29
+ """
30
+
31
+ adapter_id: str
32
+
33
+ def capabilities(self) -> Iterable[HostedCapability]:
34
+ """Return hosted capabilities declared by this adapter."""
35
+
36
+
37
+ class BaseAdapter:
38
+ """Base class for CHP capability adapters.
39
+
40
+ Subclass this, declare ``adapter_id``, and decorate methods with
41
+ ``@capability`` from ``chp_core``. All decorated methods are discovered
42
+ automatically by ``capabilities()``.
43
+
44
+ Class attributes for adapter metadata::
45
+
46
+ adapter_id # required — stable identity string
47
+ adapter_name # human-readable name (defaults to adapter_id)
48
+ adapter_description # optional description
49
+ adapter_version # semver string, default "1.0.0"
50
+ adapter_tags # list of string tags for discovery
51
+
52
+ Override ``on_register(host)`` for any setup that requires the host
53
+ (e.g. registering secondary capabilities, emitting startup evidence).
54
+
55
+ Example::
56
+
57
+ from chp_core import capability, BaseAdapter, LocalCapabilityHost, register_adapter
58
+
59
+ class MathAdapter(BaseAdapter):
60
+ adapter_id = "math"
61
+ adapter_name = "Math Capabilities"
62
+
63
+ @capability(id="math.add", version="1.0.0", description="Add two numbers.")
64
+ async def add(self, ctx, payload):
65
+ return {"sum": payload["a"] + payload["b"]}
66
+
67
+ @capability(id="math.mul", version="1.0.0", description="Multiply two numbers.")
68
+ async def multiply(self, ctx, payload):
69
+ return {"product": payload["a"] * payload["b"]}
70
+
71
+ host = LocalCapabilityHost()
72
+ register_adapter(host, MathAdapter())
73
+ """
74
+
75
+ adapter_id: str
76
+ adapter_name: str | None = None
77
+ adapter_description: str | None = None
78
+ adapter_version: str = "1.0.0"
79
+ adapter_tags: list[str] = []
80
+ adapter_category: str | None = None
81
+
82
+ def capabilities(self) -> Iterable[HostedCapability]:
83
+ """Yield capabilities from all ``@capability``-decorated methods."""
84
+ for _, method in inspect.getmembers(self, predicate=inspect.ismethod):
85
+ descriptor = get_capability_descriptor(method.__func__)
86
+ if descriptor is not None:
87
+ yield HostedCapability(descriptor=descriptor, handler=adapt_callable(method))
88
+
89
+ def on_register(self, host: LocalCapabilityHost) -> None:
90
+ """Called after all capabilities from this adapter are registered."""
91
+
92
+ def metadata(self) -> dict[str, Any]:
93
+ """Return adapter identity metadata."""
94
+ return {
95
+ "adapter_id": self.adapter_id,
96
+ "adapter_name": self.adapter_name or self.adapter_id,
97
+ "adapter_description": self.adapter_description,
98
+ "adapter_version": self.adapter_version,
99
+ "adapter_tags": list(self.adapter_tags),
100
+ "adapter_category": self.adapter_category,
101
+ }
102
+
103
+
104
+ class SimpleAdapter(BaseAdapter):
105
+ """Adapter wrapping a list of ``@capability``-decorated functions.
106
+
107
+ Use when you have standalone functions and don't need a class::
108
+
109
+ from chp_core import capability, SimpleAdapter, LocalCapabilityHost, register_adapter
110
+
111
+ @capability(id="math.add", version="1.0.0", description="Add two numbers.")
112
+ def add(a: int, b: int):
113
+ return {"sum": a + b}
114
+
115
+ host = LocalCapabilityHost()
116
+ register_adapter(host, SimpleAdapter("math", [add]))
117
+ """
118
+
119
+ def __init__(
120
+ self,
121
+ adapter_id: str,
122
+ functions: Sequence[Any],
123
+ *,
124
+ name: str | None = None,
125
+ description: str | None = None,
126
+ version: str = "1.0.0",
127
+ tags: list[str] | None = None,
128
+ ) -> None:
129
+ self.adapter_id = adapter_id
130
+ self.adapter_name = name
131
+ self.adapter_description = description
132
+ self.adapter_version = version
133
+ self.adapter_tags = tags or []
134
+ self._functions = list(functions)
135
+
136
+ def capabilities(self) -> Iterable[HostedCapability]:
137
+ for fn in self._functions:
138
+ descriptor = get_capability_descriptor(fn)
139
+ if descriptor is not None:
140
+ yield HostedCapability(descriptor=descriptor, handler=adapt_callable(fn))
141
+
142
+
143
+ def register_adapter(
144
+ host: LocalCapabilityHost,
145
+ adapter: CapabilityAdapter,
146
+ ) -> list[CapabilityDescriptor]:
147
+ """Register all capabilities from *adapter* with *host*, skipping duplicates.
148
+
149
+ Calls ``adapter.on_register(host)`` after registration if the method exists.
150
+ """
151
+ registered = register_hosted_capabilities(host, list(adapter.capabilities()))
152
+ on_register = getattr(adapter, "on_register", None)
153
+ if callable(on_register):
154
+ on_register(host)
155
+ return registered
156
+
157
+
158
+ CHP_ADAPTER_GROUP = "chp.adapters"
159
+ """Entry-point group name for installed CHP adapter packages.
160
+
161
+ Third-party adapter packages declare their adapter class under this group in
162
+ ``pyproject.toml``::
163
+
164
+ [project.entry-points."chp.adapters"]
165
+ linear = "chp_linear:LinearAdapter"
166
+
167
+ The adapter class must satisfy the ``CapabilityAdapter`` protocol (i.e. expose
168
+ ``adapter_id`` and ``capabilities()``). Using ``BaseAdapter`` as the base class
169
+ is the recommended pattern.
170
+ """
171
+
172
+
173
+ def discover_adapters(group: str = CHP_ADAPTER_GROUP) -> dict[str, type]:
174
+ """Return installed adapter classes keyed by entry-point name.
175
+
176
+ Loads all entry points under *group* (default ``chp.adapters``) from the
177
+ current Python environment. Returns an empty dict if none are installed.
178
+
179
+ Example::
180
+
181
+ adapters = discover_adapters()
182
+ # {"linear": <class 'chp_linear.LinearAdapter'>, ...}
183
+ """
184
+ from importlib.metadata import entry_points
185
+
186
+ return {ep.name: ep.load() for ep in entry_points(group=group)}
187
+
188
+
189
+ def auto_register_adapters(
190
+ host: LocalCapabilityHost,
191
+ group: str = CHP_ADAPTER_GROUP,
192
+ ) -> list[CapabilityDescriptor]:
193
+ """Instantiate and register all installed adapters in *group* with *host*.
194
+
195
+ Each adapter class is instantiated with no arguments, so adapters that
196
+ require configuration (API keys, etc.) must be registered manually via
197
+ ``register_adapter`` instead.
198
+
199
+ Registration failures per adapter are isolated — one broken adapter will
200
+ not prevent others from loading. Errors are surfaced as warnings.
201
+
202
+ Example::
203
+
204
+ host = LocalCapabilityHost()
205
+ auto_register_adapters(host)
206
+ # all pip-installed chp.adapters are now registered
207
+ """
208
+ import warnings
209
+
210
+ registered: list[CapabilityDescriptor] = []
211
+ for name, adapter_cls in discover_adapters(group).items():
212
+ try:
213
+ registered.extend(register_adapter(host, adapter_cls()))
214
+ except Exception as exc:
215
+ warnings.warn(
216
+ f"chp: failed to auto-register adapter {name!r}: {exc}",
217
+ stacklevel=2,
218
+ )
219
+ return registered
220
+
221
+
222
+ def register_hosted_capabilities(
223
+ host: LocalCapabilityHost,
224
+ capabilities: Sequence[HostedCapability],
225
+ ) -> list[CapabilityDescriptor]:
226
+ registered: list[CapabilityDescriptor] = []
227
+ for capability in capabilities:
228
+ descriptor = register_capability_once(
229
+ host,
230
+ capability.descriptor,
231
+ capability.handler,
232
+ enabled=capability.enabled,
233
+ )
234
+ if descriptor is not None:
235
+ registered.append(descriptor)
236
+ return registered
237
+
238
+
239
+ def register_capability_once(
240
+ host: LocalCapabilityHost,
241
+ descriptor: CapabilityDescriptor,
242
+ handler: CapabilityHandler,
243
+ *,
244
+ enabled: bool = True,
245
+ ) -> CapabilityDescriptor | None:
246
+ capability_ids = {
247
+ capability["id"]
248
+ for capability in host.discover().get("capabilities", [])
249
+ }
250
+ if descriptor.id in capability_ids:
251
+ return None
252
+ return host.register(descriptor, handler, enabled=enabled)
@@ -0,0 +1,229 @@
1
+ """Reference capabilities shipped with the local CHP host."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .host import (
6
+ CapabilityExecutionContext,
7
+ LocalCapabilityHost,
8
+ evaluate_invariant_against_event,
9
+ )
10
+ from .types import CapabilityDescriptor, InvariantDescriptor, JSON, utc_now
11
+
12
+
13
+ def trace_execution_descriptor() -> CapabilityDescriptor:
14
+ return CapabilityDescriptor(
15
+ id="trace_execution",
16
+ version="0.1.0",
17
+ description="Capture and correlate execution events from agents, tools, or systems.",
18
+ input_schema={
19
+ "type": "object",
20
+ "required": ["source_id", "event_type"],
21
+ "properties": {
22
+ "source_id": {"type": "string"},
23
+ "event_type": {"type": "string"},
24
+ "timestamp": {"type": "string", "format": "date-time"},
25
+ "correlation_hints": {"type": "object"},
26
+ "summary": {"type": "string"},
27
+ },
28
+ },
29
+ output_schema={"type": "object"},
30
+ tags=["observability", "trace"],
31
+ emits=["execution_started", "execution_observed", "execution_completed", "execution_failed"],
32
+ )
33
+
34
+
35
+ def explain_execution_descriptor() -> CapabilityDescriptor:
36
+ return CapabilityDescriptor(
37
+ id="explain_execution",
38
+ version="0.1.0",
39
+ description="Produce an evidence-backed explanation of a trace.",
40
+ input_schema={
41
+ "type": "object",
42
+ "properties": {
43
+ "correlation_id": {"type": "string"},
44
+ "include_inferences": {"type": "boolean"},
45
+ },
46
+ },
47
+ output_schema={"type": "object"},
48
+ tags=["observability", "explanation"],
49
+ emits=["execution_started", "execution_completed", "execution_failed"],
50
+ )
51
+
52
+
53
+ def evaluate_counterfactual_descriptor() -> CapabilityDescriptor:
54
+ return CapabilityDescriptor(
55
+ id="evaluate_counterfactual",
56
+ version="0.1.0",
57
+ description="Evaluate a trace against proposed constraints or invariants.",
58
+ input_schema={
59
+ "type": "object",
60
+ "required": ["correlation_id", "invariant"],
61
+ "properties": {
62
+ "correlation_id": {"type": "string"},
63
+ "invariant": {"type": "object"},
64
+ },
65
+ },
66
+ output_schema={"type": "object"},
67
+ tags=["observability", "counterfactual"],
68
+ emits=["execution_started", "execution_completed", "execution_failed"],
69
+ )
70
+
71
+
72
+ async def trace_execution(ctx: CapabilityExecutionContext, payload: JSON) -> JSON:
73
+ source_id = str(payload["source_id"])
74
+ external_event_type = str(payload["event_type"])
75
+ observed_at = payload.get("timestamp") or utc_now()
76
+ hints = dict(payload.get("correlation_hints") or {})
77
+
78
+ event = ctx.emit(
79
+ "execution_observed",
80
+ {
81
+ "source_id": source_id,
82
+ "external_event_type": external_event_type,
83
+ "observed_at": observed_at,
84
+ "summary": payload.get("summary"),
85
+ "correlation_hints": hints,
86
+ },
87
+ )
88
+
89
+ return {
90
+ "accepted": True,
91
+ "observed_event_id": event.event_id,
92
+ "correlation_id": ctx.correlation_id,
93
+ }
94
+
95
+
96
+ async def explain_execution(ctx: CapabilityExecutionContext, payload: JSON) -> JSON:
97
+ correlation_id = str(payload.get("correlation_id") or ctx.correlation_id)
98
+ include_inferences = bool(payload.get("include_inferences", True))
99
+ events = ctx.replay(correlation_id)
100
+
101
+ facts = [
102
+ {
103
+ "event_id": event["event_id"],
104
+ "event_type": event["event_type"],
105
+ "timestamp": event["timestamp"],
106
+ "capability_id": event["capability_id"],
107
+ "outcome": event.get("outcome"),
108
+ }
109
+ for event in events
110
+ ]
111
+
112
+ terminal = [event for event in events if event["event_type"] in {"execution_completed", "execution_failed", "execution_denied"}]
113
+ failures = [event for event in terminal if event["event_type"] == "execution_failed"]
114
+ denials = [event for event in terminal if event["event_type"] == "execution_denied"]
115
+ completed = [event for event in terminal if event["event_type"] == "execution_completed"]
116
+
117
+ inferences: list[JSON] = []
118
+ if include_inferences:
119
+ if denials:
120
+ inferences.append(
121
+ {
122
+ "statement": "At least one invocation was denied.",
123
+ "confidence": 1.0,
124
+ "evidence_ids": [event["event_id"] for event in denials],
125
+ }
126
+ )
127
+ elif failures:
128
+ inferences.append(
129
+ {
130
+ "statement": "The trace contains failed execution attempts.",
131
+ "confidence": 1.0,
132
+ "evidence_ids": [event["event_id"] for event in failures],
133
+ }
134
+ )
135
+ elif completed:
136
+ inferences.append(
137
+ {
138
+ "statement": "Observed invocations in this trace completed without recorded failure or denial.",
139
+ "confidence": 0.85,
140
+ "evidence_ids": [event["event_id"] for event in completed],
141
+ }
142
+ )
143
+
144
+ explanation_event = ctx.emit(
145
+ "explanation_generated",
146
+ {
147
+ "target_correlation_id": correlation_id,
148
+ "fact_count": len(facts),
149
+ "inference_count": len(inferences),
150
+ },
151
+ )
152
+
153
+ return {
154
+ "correlation_id": correlation_id,
155
+ "facts": facts,
156
+ "inferences": inferences,
157
+ "evidence_references": [event["event_id"] for event in events],
158
+ "explanation_event_id": explanation_event.event_id,
159
+ }
160
+
161
+
162
+ async def evaluate_counterfactual(ctx: CapabilityExecutionContext, payload: JSON) -> JSON:
163
+ correlation_id = str(payload["correlation_id"])
164
+ invariant_payload = dict(payload["invariant"])
165
+ invariant = InvariantDescriptor(
166
+ id=str(invariant_payload.get("id", "proposed")),
167
+ kind=str(invariant_payload["kind"]),
168
+ description=str(invariant_payload.get("description", "")),
169
+ enforcement=invariant_payload.get("enforcement", "host"),
170
+ failure_behavior=invariant_payload.get("failure_behavior", "deny"),
171
+ parameters=dict(invariant_payload.get("parameters") or {}),
172
+ )
173
+
174
+ events = ctx.replay(correlation_id)
175
+ violations = []
176
+ for event in events:
177
+ reason = evaluate_invariant_against_event(invariant, event)
178
+ if reason:
179
+ violations.append(
180
+ {
181
+ "event_id": event["event_id"],
182
+ "event_type": event["event_type"],
183
+ "capability_id": event["capability_id"],
184
+ "reason": reason,
185
+ }
186
+ )
187
+
188
+ counterfactual_event = ctx.emit(
189
+ "counterfactual_evaluated",
190
+ {
191
+ "target_correlation_id": correlation_id,
192
+ "invariant_id": invariant.id,
193
+ "violation_count": len(violations),
194
+ },
195
+ )
196
+
197
+ return {
198
+ "correlation_id": correlation_id,
199
+ "invariant": invariant.to_dict(),
200
+ "would_have_denied": bool(violations) and invariant.failure_behavior == "deny",
201
+ "would_have_warned": bool(violations) and invariant.failure_behavior == "warn",
202
+ "would_deny": bool(violations) and invariant.failure_behavior == "deny",
203
+ "violating_events": violations,
204
+ "facts": [
205
+ {
206
+ "statement": f"Evaluated {len(events)} evidence events against invariant {invariant.id}.",
207
+ "evidence_ids": [event["event_id"] for event in events],
208
+ }
209
+ ],
210
+ "counterfactual_event_id": counterfactual_event.event_id,
211
+ }
212
+
213
+
214
+ def register_trace_execution(host: LocalCapabilityHost) -> CapabilityDescriptor:
215
+ return host.register(trace_execution_descriptor(), trace_execution)
216
+
217
+
218
+ def register_explain_execution(host: LocalCapabilityHost) -> CapabilityDescriptor:
219
+ return host.register(explain_execution_descriptor(), explain_execution)
220
+
221
+
222
+ def register_evaluate_counterfactual(host: LocalCapabilityHost) -> CapabilityDescriptor:
223
+ return host.register(evaluate_counterfactual_descriptor(), evaluate_counterfactual)
224
+
225
+
226
+ def register_builtin_capabilities(host: LocalCapabilityHost) -> None:
227
+ register_trace_execution(host)
228
+ register_explain_execution(host)
229
+ register_evaluate_counterfactual(host)
chp_core/checks.py ADDED
@@ -0,0 +1,39 @@
1
+ """Shared helpers for local CHP development checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from pathlib import Path
8
+
9
+ from .types import JSON
10
+
11
+
12
+ def add_check(checks: list[JSON], name: str, passed: bool, details: JSON) -> None:
13
+ checks.append(
14
+ {
15
+ "name": name,
16
+ "passed": passed,
17
+ "details": details,
18
+ }
19
+ )
20
+
21
+
22
+ def read_text(path: Path) -> str:
23
+ return path.read_text(encoding="utf-8")
24
+
25
+
26
+ def read_json(path: Path) -> JSON:
27
+ return json.loads(path.read_text(encoding="utf-8"))
28
+
29
+
30
+ def safe_check_name(path: str) -> str:
31
+ return re.sub(r"[^a-zA-Z0-9]+", "_", path).strip("_")
32
+
33
+
34
+ def preview_text(value: str | bytes, limit: int = 1200) -> str:
35
+ if isinstance(value, bytes):
36
+ value = value.decode("utf-8", errors="replace")
37
+ if len(value) <= limit:
38
+ return value
39
+ return value[-limit:]