aevum-core 0.2.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.
- aevum/core/__init__.py +30 -0
- aevum/core/audit/__init__.py +1 -0
- aevum/core/audit/event.py +77 -0
- aevum/core/audit/hlc.py +36 -0
- aevum/core/audit/ledger.py +71 -0
- aevum/core/audit/sigchain.py +166 -0
- aevum/core/barriers.py +105 -0
- aevum/core/complications/__init__.py +46 -0
- aevum/core/complications/circuit_breaker.py +83 -0
- aevum/core/complications/conflict.py +43 -0
- aevum/core/complications/manifest_validator.py +117 -0
- aevum/core/complications/registry.py +181 -0
- aevum/core/complications/webhook.py +168 -0
- aevum/core/consent/__init__.py +1 -0
- aevum/core/consent/ledger.py +64 -0
- aevum/core/consent/models.py +47 -0
- aevum/core/control_plane/__init__.py +1 -0
- aevum/core/control_plane/api.py +19 -0
- aevum/core/engine.py +340 -0
- aevum/core/envelope/__init__.py +1 -0
- aevum/core/envelope/models.py +217 -0
- aevum/core/exceptions.py +51 -0
- aevum/core/functions/__init__.py +1 -0
- aevum/core/functions/commit.py +64 -0
- aevum/core/functions/ingest.py +85 -0
- aevum/core/functions/query.py +167 -0
- aevum/core/functions/replay.py +64 -0
- aevum/core/functions/review.py +152 -0
- aevum/core/graph/__init__.py +1 -0
- aevum/core/graph/memory.py +42 -0
- aevum/core/policy/__init__.py +1 -0
- aevum/core/policy/bridge.py +198 -0
- aevum/core/protocols/__init__.py +13 -0
- aevum/core/protocols/audit_ledger.py +32 -0
- aevum/core/protocols/complication.py +12 -0
- aevum/core/protocols/consent_ledger.py +23 -0
- aevum/core/protocols/graph_store.py +12 -0
- aevum/core/py.typed +0 -0
- aevum/core/session.py +16 -0
- aevum_core-0.2.0.dist-info/METADATA +42 -0
- aevum_core-0.2.0.dist-info/RECORD +42 -0
- aevum_core-0.2.0.dist-info/WHEEL +4 -0
aevum/core/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
aevum.core — The Aevum context kernel.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from aevum.core import Engine
|
|
6
|
+
engine = Engine()
|
|
7
|
+
result = engine.commit(event_type="app.event", payload={}, actor="user-1")
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from aevum.core.engine import Engine
|
|
13
|
+
from aevum.core.envelope.models import OutputEnvelope
|
|
14
|
+
from aevum.core.exceptions import (
|
|
15
|
+
AevumError,
|
|
16
|
+
BarrierViolationError,
|
|
17
|
+
ConsentRequiredError,
|
|
18
|
+
ProvenanceRequiredError,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__version__ = "0.2.0"
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"Engine",
|
|
25
|
+
"OutputEnvelope",
|
|
26
|
+
"AevumError",
|
|
27
|
+
"BarrierViolationError",
|
|
28
|
+
"ConsentRequiredError",
|
|
29
|
+
"ProvenanceRequiredError",
|
|
30
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""aevum.core.audit — Episodic ledger, sigchain, HLC, AuditEvent."""
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AuditEvent — the 18-field episodic ledger entry. Spec Section 06.2.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import dataclasses
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclasses.dataclass(frozen=True)
|
|
14
|
+
class AuditEvent:
|
|
15
|
+
"""Immutable episodic ledger entry. All 18 fields required."""
|
|
16
|
+
|
|
17
|
+
event_id: str
|
|
18
|
+
episode_id: str
|
|
19
|
+
sequence: int
|
|
20
|
+
event_type: str
|
|
21
|
+
schema_version: str
|
|
22
|
+
valid_from: str
|
|
23
|
+
valid_to: str | None
|
|
24
|
+
system_time: int
|
|
25
|
+
causation_id: str | None
|
|
26
|
+
correlation_id: str | None
|
|
27
|
+
actor: str
|
|
28
|
+
trace_id: str | None
|
|
29
|
+
span_id: str | None
|
|
30
|
+
payload: dict[str, Any]
|
|
31
|
+
payload_hash: str
|
|
32
|
+
prior_hash: str
|
|
33
|
+
signature: str
|
|
34
|
+
signer_key_id: str
|
|
35
|
+
|
|
36
|
+
def __post_init__(self) -> None:
|
|
37
|
+
if not self.actor:
|
|
38
|
+
raise ValueError("actor MUST NOT be empty")
|
|
39
|
+
if self.sequence < 1:
|
|
40
|
+
raise ValueError(f"sequence must be >= 1, got {self.sequence}")
|
|
41
|
+
if not self.event_type:
|
|
42
|
+
raise ValueError("event_type MUST NOT be empty")
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def canonical_payload(payload: dict[str, Any]) -> bytes:
|
|
46
|
+
return json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def hash_payload(payload: dict[str, Any]) -> str:
|
|
50
|
+
return hashlib.sha3_256(AuditEvent.canonical_payload(payload)).hexdigest()
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def hash_event_for_chain(event: AuditEvent) -> str:
|
|
54
|
+
"""SHA3-256 over all fields (excluding signature) for prior_hash chaining."""
|
|
55
|
+
fields = {
|
|
56
|
+
"event_id": event.event_id,
|
|
57
|
+
"episode_id": event.episode_id,
|
|
58
|
+
"sequence": event.sequence,
|
|
59
|
+
"event_type": event.event_type,
|
|
60
|
+
"schema_version": event.schema_version,
|
|
61
|
+
"valid_from": event.valid_from,
|
|
62
|
+
"valid_to": event.valid_to,
|
|
63
|
+
"system_time": event.system_time,
|
|
64
|
+
"causation_id": event.causation_id,
|
|
65
|
+
"correlation_id": event.correlation_id,
|
|
66
|
+
"actor": event.actor,
|
|
67
|
+
"trace_id": event.trace_id,
|
|
68
|
+
"span_id": event.span_id,
|
|
69
|
+
"payload_hash": event.payload_hash,
|
|
70
|
+
"prior_hash": event.prior_hash,
|
|
71
|
+
"signer_key_id": event.signer_key_id,
|
|
72
|
+
}
|
|
73
|
+
canonical = json.dumps(fields, sort_keys=True, separators=(",", ":")).encode()
|
|
74
|
+
return hashlib.sha3_256(canonical).hexdigest()
|
|
75
|
+
|
|
76
|
+
def audit_id(self) -> str:
|
|
77
|
+
return f"urn:aevum:audit:{self.event_id}"
|
aevum/core/audit/hlc.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HybridLogicalClock — causal ordering. Spec Section 06.5.
|
|
3
|
+
Timestamp: bits 63-16 = ms since epoch, bits 15-0 = logical counter.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
_lock = threading.Lock()
|
|
12
|
+
_last_ms: int = 0
|
|
13
|
+
_counter: int = 0
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def now() -> int:
|
|
17
|
+
global _last_ms, _counter
|
|
18
|
+
with _lock:
|
|
19
|
+
ms = int(time.time() * 1000)
|
|
20
|
+
if ms > _last_ms:
|
|
21
|
+
_last_ms = ms
|
|
22
|
+
_counter = 0
|
|
23
|
+
else:
|
|
24
|
+
_counter += 1
|
|
25
|
+
if _counter > 0xFFFF:
|
|
26
|
+
_last_ms += 1
|
|
27
|
+
_counter = 0
|
|
28
|
+
return (_last_ms << 16) | _counter
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def to_millis(ts: int) -> int:
|
|
32
|
+
return ts >> 16
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def to_counter(ts: int) -> int:
|
|
36
|
+
return ts & 0xFFFF
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Episodic ledger — append-only. Barrier 4 enforced here. Spec Section 06.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import threading
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from aevum.core.audit.event import AuditEvent
|
|
11
|
+
from aevum.core.audit.sigchain import Sigchain
|
|
12
|
+
from aevum.core.exceptions import BarrierViolationError, ReplayNotFoundError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InMemoryLedger:
|
|
16
|
+
"""Thread-safe append-only in-memory episodic ledger. Suitable for development and testing."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, sigchain: Sigchain) -> None:
|
|
19
|
+
self._sigchain = sigchain
|
|
20
|
+
self._events: list[AuditEvent] = []
|
|
21
|
+
self._index: dict[str, AuditEvent] = {}
|
|
22
|
+
self._lock = threading.Lock()
|
|
23
|
+
|
|
24
|
+
def append(
|
|
25
|
+
self,
|
|
26
|
+
*,
|
|
27
|
+
event_type: str,
|
|
28
|
+
payload: dict[str, Any],
|
|
29
|
+
actor: str,
|
|
30
|
+
episode_id: str | None = None,
|
|
31
|
+
causation_id: str | None = None,
|
|
32
|
+
correlation_id: str | None = None,
|
|
33
|
+
) -> AuditEvent:
|
|
34
|
+
with self._lock:
|
|
35
|
+
event = self._sigchain.new_event(
|
|
36
|
+
event_type=event_type,
|
|
37
|
+
payload=payload,
|
|
38
|
+
actor=actor,
|
|
39
|
+
episode_id=episode_id,
|
|
40
|
+
causation_id=causation_id,
|
|
41
|
+
correlation_id=correlation_id,
|
|
42
|
+
)
|
|
43
|
+
self._events.append(event)
|
|
44
|
+
self._index[event.audit_id()] = event
|
|
45
|
+
return event
|
|
46
|
+
|
|
47
|
+
def get(self, audit_id: str) -> AuditEvent:
|
|
48
|
+
event = self._index.get(audit_id)
|
|
49
|
+
if event is None:
|
|
50
|
+
raise ReplayNotFoundError(f"No ledger entry for {audit_id!r}")
|
|
51
|
+
return event
|
|
52
|
+
|
|
53
|
+
def all_events(self) -> list[AuditEvent]:
|
|
54
|
+
with self._lock:
|
|
55
|
+
return list(self._events)
|
|
56
|
+
|
|
57
|
+
def count(self) -> int:
|
|
58
|
+
with self._lock:
|
|
59
|
+
return len(self._events)
|
|
60
|
+
|
|
61
|
+
def __delitem__(self, key: object) -> None:
|
|
62
|
+
"""Barrier 4: deletion forbidden."""
|
|
63
|
+
raise BarrierViolationError(
|
|
64
|
+
"Attempted to delete a ledger entry — Barrier 4 (Audit Immutability) violated."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def __setitem__(self, key: object, value: object) -> None:
|
|
68
|
+
"""Barrier 4: overwrite forbidden."""
|
|
69
|
+
raise BarrierViolationError(
|
|
70
|
+
"Attempted to overwrite a ledger entry — Barrier 4 (Audit Immutability) violated."
|
|
71
|
+
)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sigchain — Ed25519 signing and SHA3-256 chaining. Spec Section 06.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import base64
|
|
8
|
+
import datetime
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import time
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
|
|
16
|
+
Ed25519PrivateKey,
|
|
17
|
+
Ed25519PublicKey,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from aevum.core.audit.event import AuditEvent
|
|
21
|
+
from aevum.core.audit.hlc import now as hlc_now
|
|
22
|
+
|
|
23
|
+
GENESIS_HASH = hashlib.sha3_256(b"aevum:genesis").hexdigest()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _uuid7() -> str:
|
|
27
|
+
"""UUID version 7 (time-ordered). Inline — no external dep."""
|
|
28
|
+
ts_ms = int(time.time() * 1000) & 0xFFFFFFFFFFFF
|
|
29
|
+
rand = int.from_bytes(os.urandom(10), "big")
|
|
30
|
+
rand_a = (rand >> 62) & 0x0FFF
|
|
31
|
+
rand_b = rand & 0x3FFFFFFFFFFFFFFF
|
|
32
|
+
hi = (ts_ms << 16) | 0x7000 | rand_a
|
|
33
|
+
lo = 0x8000000000000000 | rand_b
|
|
34
|
+
h = f"{hi:016x}{lo:016x}"
|
|
35
|
+
return f"{h[:8]}-{h[8:12]}-{h[12:16]}-{h[16:20]}-{h[20:]}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Sigchain:
|
|
39
|
+
"""Per-node Ed25519 signing chain. Append-only by design."""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
private_key: Ed25519PrivateKey | None = None,
|
|
44
|
+
key_id: str | None = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
self._private_key = private_key or Ed25519PrivateKey.generate()
|
|
47
|
+
self._key_id = key_id or _uuid7()
|
|
48
|
+
self._sequence: int = 0
|
|
49
|
+
self._prior_hash: str = GENESIS_HASH
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def key_id(self) -> str:
|
|
53
|
+
return self._key_id
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def public_key(self) -> Ed25519PublicKey:
|
|
57
|
+
return self._private_key.public_key()
|
|
58
|
+
|
|
59
|
+
def _sign(self, fields: dict[str, Any]) -> str:
|
|
60
|
+
canonical = json.dumps(fields, sort_keys=True, separators=(",", ":")).encode()
|
|
61
|
+
sig_bytes = self._private_key.sign(canonical)
|
|
62
|
+
return base64.urlsafe_b64encode(sig_bytes).rstrip(b"=").decode()
|
|
63
|
+
|
|
64
|
+
def new_event(
|
|
65
|
+
self,
|
|
66
|
+
*,
|
|
67
|
+
event_type: str,
|
|
68
|
+
payload: dict[str, Any],
|
|
69
|
+
actor: str,
|
|
70
|
+
episode_id: str | None = None,
|
|
71
|
+
causation_id: str | None = None,
|
|
72
|
+
correlation_id: str | None = None,
|
|
73
|
+
trace_id: str | None = None,
|
|
74
|
+
span_id: str | None = None,
|
|
75
|
+
valid_from: str | None = None,
|
|
76
|
+
valid_to: str | None = None,
|
|
77
|
+
) -> AuditEvent:
|
|
78
|
+
"""Append a new signed event to the chain."""
|
|
79
|
+
self._sequence += 1
|
|
80
|
+
event_id = _uuid7()
|
|
81
|
+
ep_id = episode_id or _uuid7()
|
|
82
|
+
vf = valid_from or datetime.datetime.now(datetime.UTC).isoformat()
|
|
83
|
+
ts = hlc_now()
|
|
84
|
+
payload_hash = AuditEvent.hash_payload(payload)
|
|
85
|
+
prior = self._prior_hash
|
|
86
|
+
|
|
87
|
+
signing_fields: dict[str, Any] = {
|
|
88
|
+
"event_id": event_id,
|
|
89
|
+
"episode_id": ep_id,
|
|
90
|
+
"sequence": self._sequence,
|
|
91
|
+
"event_type": event_type,
|
|
92
|
+
"schema_version": "1.0",
|
|
93
|
+
"valid_from": vf,
|
|
94
|
+
"valid_to": valid_to,
|
|
95
|
+
"system_time": ts,
|
|
96
|
+
"causation_id": causation_id,
|
|
97
|
+
"correlation_id": correlation_id,
|
|
98
|
+
"actor": actor,
|
|
99
|
+
"trace_id": trace_id,
|
|
100
|
+
"span_id": span_id,
|
|
101
|
+
"payload_hash": payload_hash,
|
|
102
|
+
"prior_hash": prior,
|
|
103
|
+
"signer_key_id": self._key_id,
|
|
104
|
+
}
|
|
105
|
+
signature = self._sign(signing_fields)
|
|
106
|
+
|
|
107
|
+
event = AuditEvent(
|
|
108
|
+
event_id=event_id,
|
|
109
|
+
episode_id=ep_id,
|
|
110
|
+
sequence=self._sequence,
|
|
111
|
+
event_type=event_type,
|
|
112
|
+
schema_version="1.0",
|
|
113
|
+
valid_from=vf,
|
|
114
|
+
valid_to=valid_to,
|
|
115
|
+
system_time=ts,
|
|
116
|
+
causation_id=causation_id,
|
|
117
|
+
correlation_id=correlation_id,
|
|
118
|
+
actor=actor,
|
|
119
|
+
trace_id=trace_id,
|
|
120
|
+
span_id=span_id,
|
|
121
|
+
payload=payload,
|
|
122
|
+
payload_hash=payload_hash,
|
|
123
|
+
prior_hash=prior,
|
|
124
|
+
signature=signature,
|
|
125
|
+
signer_key_id=self._key_id,
|
|
126
|
+
)
|
|
127
|
+
self._prior_hash = AuditEvent.hash_event_for_chain(event)
|
|
128
|
+
return event
|
|
129
|
+
|
|
130
|
+
def verify_chain(self, events: list[AuditEvent]) -> bool:
|
|
131
|
+
"""Verify entire chain from genesis. Returns True if intact."""
|
|
132
|
+
public_key = self.public_key
|
|
133
|
+
expected_prior = GENESIS_HASH
|
|
134
|
+
for event in events:
|
|
135
|
+
if event.prior_hash != expected_prior:
|
|
136
|
+
return False
|
|
137
|
+
if AuditEvent.hash_payload(event.payload) != event.payload_hash:
|
|
138
|
+
return False
|
|
139
|
+
signing_fields: dict[str, Any] = {
|
|
140
|
+
"event_id": event.event_id,
|
|
141
|
+
"episode_id": event.episode_id,
|
|
142
|
+
"sequence": event.sequence,
|
|
143
|
+
"event_type": event.event_type,
|
|
144
|
+
"schema_version": event.schema_version,
|
|
145
|
+
"valid_from": event.valid_from,
|
|
146
|
+
"valid_to": event.valid_to,
|
|
147
|
+
"system_time": event.system_time,
|
|
148
|
+
"causation_id": event.causation_id,
|
|
149
|
+
"correlation_id": event.correlation_id,
|
|
150
|
+
"actor": event.actor,
|
|
151
|
+
"trace_id": event.trace_id,
|
|
152
|
+
"span_id": event.span_id,
|
|
153
|
+
"payload_hash": event.payload_hash,
|
|
154
|
+
"prior_hash": event.prior_hash,
|
|
155
|
+
"signer_key_id": event.signer_key_id,
|
|
156
|
+
}
|
|
157
|
+
canonical = json.dumps(
|
|
158
|
+
signing_fields, sort_keys=True, separators=(",", ":")
|
|
159
|
+
).encode()
|
|
160
|
+
try:
|
|
161
|
+
sig_bytes = base64.urlsafe_b64decode(event.signature + "==")
|
|
162
|
+
public_key.verify(sig_bytes, canonical)
|
|
163
|
+
except Exception:
|
|
164
|
+
return False
|
|
165
|
+
expected_prior = AuditEvent.hash_event_for_chain(event)
|
|
166
|
+
return True
|
aevum/core/barriers.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Absolute Barriers — hardcoded, unconditional, non-configurable.
|
|
3
|
+
Spec Section 09.3. Canary tests in tests/test_canary.py.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from aevum.core.envelope.models import OutputEnvelope, ProvenanceRecord
|
|
11
|
+
|
|
12
|
+
_CRISIS_KEYWORDS: frozenset[str] = frozenset({
|
|
13
|
+
"kill myself", "end my life", "want to die", "commit suicide",
|
|
14
|
+
"hurt someone", "going to hurt", "harm myself",
|
|
15
|
+
"heart attack", "can't breathe", "i can't breathe",
|
|
16
|
+
"overdose", "going to shoot", "going to stab",
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
_CRISIS_SAFE_MESSAGE = (
|
|
20
|
+
"It sounds like you or someone you know may be going through something "
|
|
21
|
+
"very difficult. Please reach out to a crisis service — you don't have to face this alone."
|
|
22
|
+
)
|
|
23
|
+
_CRISIS_RESOURCES = [
|
|
24
|
+
"988 Suicide & Crisis Lifeline: call or text 988 (US)",
|
|
25
|
+
"Crisis Text Line: text HOME to 741741 (US)",
|
|
26
|
+
"International Association for Suicide Prevention: https://www.iasp.info/resources/Crisis_Centres/",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _text_from_data(data: dict[str, Any]) -> str:
|
|
31
|
+
parts = []
|
|
32
|
+
for v in data.values():
|
|
33
|
+
parts.append(str(v) if not isinstance(v, str) else v)
|
|
34
|
+
return " ".join(parts).lower()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _kernel_provenance(audit_id: str) -> ProvenanceRecord:
|
|
38
|
+
return ProvenanceRecord(
|
|
39
|
+
source_id="aevum-core", ingest_audit_id=audit_id,
|
|
40
|
+
chain_of_custody=["aevum-core"], classification=0,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def check_crisis(data: dict[str, Any], audit_id: str) -> OutputEnvelope | None:
|
|
45
|
+
"""Barrier 1 — CRISIS. Returns crisis envelope or None."""
|
|
46
|
+
if any(kw in _text_from_data(data) for kw in _CRISIS_KEYWORDS):
|
|
47
|
+
return OutputEnvelope.crisis(
|
|
48
|
+
audit_id=audit_id,
|
|
49
|
+
safe_message=_CRISIS_SAFE_MESSAGE,
|
|
50
|
+
resources=_CRISIS_RESOURCES,
|
|
51
|
+
provenance=_kernel_provenance(audit_id),
|
|
52
|
+
)
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def apply_classification_ceiling(
|
|
57
|
+
results: dict[str, Any],
|
|
58
|
+
classifications: dict[str, int],
|
|
59
|
+
actor_clearance: int,
|
|
60
|
+
) -> tuple[dict[str, Any], list[str]]:
|
|
61
|
+
"""Barrier 2 — CLASSIFICATION CEILING. Redacts above-clearance items."""
|
|
62
|
+
filtered: dict[str, Any] = {}
|
|
63
|
+
redacted: list[str] = []
|
|
64
|
+
for entity_id, entity_data in results.items():
|
|
65
|
+
if classifications.get(entity_id, 0) <= actor_clearance:
|
|
66
|
+
filtered[entity_id] = entity_data
|
|
67
|
+
else:
|
|
68
|
+
redacted.append(entity_id)
|
|
69
|
+
return filtered, redacted
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def check_consent(
|
|
73
|
+
*,
|
|
74
|
+
subject_id: str,
|
|
75
|
+
operation: str,
|
|
76
|
+
grantee_id: str,
|
|
77
|
+
consent_ledger: Any,
|
|
78
|
+
audit_id: str,
|
|
79
|
+
) -> OutputEnvelope | None:
|
|
80
|
+
"""Barrier 3 — CONSENT. Returns error envelope or None."""
|
|
81
|
+
if not consent_ledger.has_consent(
|
|
82
|
+
subject_id=subject_id, operation=operation, grantee_id=grantee_id
|
|
83
|
+
):
|
|
84
|
+
return OutputEnvelope.error(
|
|
85
|
+
audit_id=audit_id,
|
|
86
|
+
error_code="consent_required",
|
|
87
|
+
error_detail=f"No active consent grant for operation '{operation}' on subject '{subject_id}' by '{grantee_id}'",
|
|
88
|
+
provenance=_kernel_provenance(audit_id),
|
|
89
|
+
)
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# Barrier 4 — AUDIT IMMUTABILITY enforced by InMemoryLedger.__delitem__/__setitem__
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def check_provenance(provenance: dict[str, Any], audit_id: str) -> OutputEnvelope | None:
|
|
97
|
+
"""Barrier 5 — PROVENANCE. Returns error envelope or None."""
|
|
98
|
+
if not provenance or not provenance.get("source_id"):
|
|
99
|
+
return OutputEnvelope.error(
|
|
100
|
+
audit_id=audit_id,
|
|
101
|
+
error_code="provenance_required",
|
|
102
|
+
error_detail="Provenance record is missing or has no source_id",
|
|
103
|
+
provenance=_kernel_provenance(audit_id),
|
|
104
|
+
)
|
|
105
|
+
return None
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
aevum.core.complications — Complication governance lifecycle.
|
|
3
|
+
|
|
4
|
+
ComplicationRegistry — 7-state machine: install/approve/suspend/decommission
|
|
5
|
+
CircuitBreaker — threshold-based, monotonic clock
|
|
6
|
+
ManifestValidator — schema + Ed25519 (optional)
|
|
7
|
+
ConflictDetector — capability overlap, fail-closed
|
|
8
|
+
WebhookRegistry — register/dispatch review events
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import concurrent.futures
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from aevum.core.complications.circuit_breaker import CircuitBreaker
|
|
18
|
+
from aevum.core.complications.conflict import ConflictDetector
|
|
19
|
+
from aevum.core.complications.manifest_validator import ManifestValidator
|
|
20
|
+
from aevum.core.complications.registry import ComplicationRegistry, ComplicationState
|
|
21
|
+
from aevum.core.complications.webhook import WebhookRegistry
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _run_coro(coro: Any) -> Any:
|
|
25
|
+
"""
|
|
26
|
+
Run a coroutine from sync context.
|
|
27
|
+
Handles the case where we are already inside a running event loop
|
|
28
|
+
(e.g. FastAPI handlers) by delegating to a thread pool.
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
asyncio.get_running_loop()
|
|
32
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
|
33
|
+
return pool.submit(asyncio.run, coro).result()
|
|
34
|
+
except RuntimeError:
|
|
35
|
+
return asyncio.run(coro)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"ComplicationRegistry",
|
|
40
|
+
"ComplicationState",
|
|
41
|
+
"CircuitBreaker",
|
|
42
|
+
"ManifestValidator",
|
|
43
|
+
"ConflictDetector",
|
|
44
|
+
"WebhookRegistry",
|
|
45
|
+
"_run_coro",
|
|
46
|
+
]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CircuitBreaker — per-complication threshold-based circuit breaker.
|
|
3
|
+
|
|
4
|
+
States: CLOSED (normal) → OPEN (tripped) → HALF_OPEN (testing recovery).
|
|
5
|
+
Uses monotonic clock — no clock drift issues.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
from enum import Enum, auto
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CBState(Enum):
|
|
16
|
+
CLOSED = auto() # Normal — calls pass through
|
|
17
|
+
OPEN = auto() # Tripped — calls fail immediately
|
|
18
|
+
HALF_OPEN = auto() # Recovery probe — one call allowed
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CircuitBreaker:
|
|
22
|
+
"""
|
|
23
|
+
Thread-safe circuit breaker for a single complication.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
failure_threshold: Consecutive failures before opening (default 5)
|
|
27
|
+
recovery_seconds: Seconds before attempting half-open probe (default 30)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
failure_threshold: int = 5,
|
|
33
|
+
recovery_seconds: float = 30.0,
|
|
34
|
+
) -> None:
|
|
35
|
+
self._threshold = failure_threshold
|
|
36
|
+
self._recovery = recovery_seconds
|
|
37
|
+
self._state = CBState.CLOSED
|
|
38
|
+
self._failures = 0
|
|
39
|
+
self._opened_at: float | None = None
|
|
40
|
+
self._lock = threading.Lock()
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def state(self) -> CBState:
|
|
44
|
+
with self._lock:
|
|
45
|
+
self._check_recovery()
|
|
46
|
+
return self._state
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def is_open(self) -> bool:
|
|
50
|
+
return self.state == CBState.OPEN
|
|
51
|
+
|
|
52
|
+
def record_success(self) -> None:
|
|
53
|
+
"""Call after a successful complication invocation."""
|
|
54
|
+
with self._lock:
|
|
55
|
+
self._failures = 0
|
|
56
|
+
self._state = CBState.CLOSED
|
|
57
|
+
self._opened_at = None
|
|
58
|
+
|
|
59
|
+
def record_failure(self) -> None:
|
|
60
|
+
"""Call after a failed complication invocation."""
|
|
61
|
+
with self._lock:
|
|
62
|
+
self._failures += 1
|
|
63
|
+
if self._failures >= self._threshold:
|
|
64
|
+
self._state = CBState.OPEN
|
|
65
|
+
self._opened_at = time.monotonic()
|
|
66
|
+
|
|
67
|
+
def allow_request(self) -> bool:
|
|
68
|
+
"""Return True if a request should be allowed through."""
|
|
69
|
+
with self._lock:
|
|
70
|
+
self._check_recovery()
|
|
71
|
+
return self._state in (CBState.CLOSED, CBState.HALF_OPEN)
|
|
72
|
+
|
|
73
|
+
def _check_recovery(self) -> None:
|
|
74
|
+
"""Transition OPEN → HALF_OPEN if recovery period has elapsed."""
|
|
75
|
+
if self._state == CBState.OPEN and self._opened_at is not None and time.monotonic() - self._opened_at >= self._recovery:
|
|
76
|
+
self._state = CBState.HALF_OPEN
|
|
77
|
+
|
|
78
|
+
def reset(self) -> None:
|
|
79
|
+
"""Manual reset — for admin use."""
|
|
80
|
+
with self._lock:
|
|
81
|
+
self._state = CBState.CLOSED
|
|
82
|
+
self._failures = 0
|
|
83
|
+
self._opened_at = None
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ConflictDetector — detect capability conflicts at install time.
|
|
3
|
+
Fail-closed: if a conflict exists, installation is refused.
|
|
4
|
+
Spec Section 11.2 (reapproval triggers).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ConflictDetector:
|
|
13
|
+
"""
|
|
14
|
+
Checks for capability conflicts before a new complication is installed.
|
|
15
|
+
|
|
16
|
+
A conflict occurs when two ACTIVE complications claim the same capability.
|
|
17
|
+
Fail-closed: the new complication is rejected, not the existing one.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def check(
|
|
21
|
+
self,
|
|
22
|
+
new_manifest: dict[str, Any],
|
|
23
|
+
active_manifests: list[dict[str, Any]],
|
|
24
|
+
) -> list[str]:
|
|
25
|
+
"""
|
|
26
|
+
Check new_manifest against all currently active manifests.
|
|
27
|
+
|
|
28
|
+
Returns a list of conflict descriptions. Empty list = no conflicts.
|
|
29
|
+
"""
|
|
30
|
+
new_capabilities = set(new_manifest.get("capabilities", []))
|
|
31
|
+
conflicts: list[str] = []
|
|
32
|
+
|
|
33
|
+
for existing in active_manifests:
|
|
34
|
+
existing_name = existing.get("name", "unknown")
|
|
35
|
+
existing_capabilities = set(existing.get("capabilities", []))
|
|
36
|
+
overlap = new_capabilities & existing_capabilities
|
|
37
|
+
if overlap:
|
|
38
|
+
conflicts.append(
|
|
39
|
+
f"Capability conflict with '{existing_name}': "
|
|
40
|
+
f"both claim {sorted(overlap)}"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return conflicts
|