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 +98 -0
- chp_core/adapters.py +252 -0
- chp_core/capabilities.py +229 -0
- chp_core/checks.py +39 -0
- chp_core/cli.py +685 -0
- chp_core/codex.py +80 -0
- chp_core/conformance_matrix.py +175 -0
- chp_core/decorators.py +115 -0
- chp_core/demo.py +80 -0
- chp_core/demo_validation.py +123 -0
- chp_core/evidence_quality.py +139 -0
- chp_core/host.py +508 -0
- chp_core/http.py +131 -0
- chp_core/otel.py +110 -0
- chp_core/protocol_checks.py +221 -0
- chp_core/redaction.py +53 -0
- chp_core/store.py +171 -0
- chp_core/types.py +347 -0
- chp_core/version_control.py +1093 -0
- chp_core/work.py +55 -0
- chp_core/work_api.py +211 -0
- chp_core/work_capabilities.py +275 -0
- chp_core/work_host.py +28 -0
- chp_core/work_inventory.py +328 -0
- chp_core-0.1.0.dist-info/METADATA +212 -0
- chp_core-0.1.0.dist-info/RECORD +28 -0
- chp_core-0.1.0.dist-info/WHEEL +4 -0
- chp_core-0.1.0.dist-info/entry_points.txt +5 -0
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)
|
chp_core/capabilities.py
ADDED
|
@@ -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:]
|