tibet-continuityd 0.3.3__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.
- tibet_continuityd/__init__.py +105 -0
- tibet_continuityd/__main__.py +7 -0
- tibet_continuityd/backpressure.py +185 -0
- tibet_continuityd/coalesce.py +143 -0
- tibet_continuityd/daemon.py +698 -0
- tibet_continuityd/police.py +281 -0
- tibet_continuityd/seal.py +227 -0
- tibet_continuityd/sniff.py +242 -0
- tibet_continuityd/trust_kernel.py +364 -0
- tibet_continuityd/verify_fork.py +306 -0
- tibet_continuityd/watch.py +203 -0
- tibet_continuityd-0.3.3.dist-info/METADATA +128 -0
- tibet_continuityd-0.3.3.dist-info/RECORD +16 -0
- tibet_continuityd-0.3.3.dist-info/WHEEL +5 -0
- tibet_continuityd-0.3.3.dist-info/entry_points.txt +2 -0
- tibet_continuityd-0.3.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tibet-continuityd — Continuous Integrity System Daemon
|
|
3
|
+
=======================================================
|
|
4
|
+
|
|
5
|
+
A residential trust guardian that runs in the background of
|
|
6
|
+
every machine where TIBET cryptographic discipline must be
|
|
7
|
+
continuously enforced.
|
|
8
|
+
|
|
9
|
+
Watch → inotify
|
|
10
|
+
Sniff → libmagic / TBZ magic-byte recognition
|
|
11
|
+
Verify → (v0.2) cryptographic verification
|
|
12
|
+
Fork → (v0.2) forward-causal materialize via phantom.icc
|
|
13
|
+
Triage → (v0.2) quarantine on mismatch
|
|
14
|
+
Reseal → (v0.3) periodic reseal of materialized state
|
|
15
|
+
|
|
16
|
+
Three operating modes:
|
|
17
|
+
Mode 1 Passive Guardian observe + log + advise
|
|
18
|
+
Mode 2 Sealing Guardian auto-reseal + active intake
|
|
19
|
+
Mode 3 Strict Continuity zero-trust, ICC/TBZ only
|
|
20
|
+
|
|
21
|
+
Spec: /srv/jtel-stack/hersenspinsels/tibet-continuityd-spec.md
|
|
22
|
+
Plus: /srv/jtel-stack/hersenspinsels/tibet-continuity-guardian.md
|
|
23
|
+
(Codex' parallel intake-discipline guide)
|
|
24
|
+
|
|
25
|
+
"Name is hint. Content is truth. Arrival is event."
|
|
26
|
+
— Codex, 9 mei 2026
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
__version__ = "0.3.3"
|
|
30
|
+
__author__ = "Jasper van de Meent, Root AI, Codex"
|
|
31
|
+
|
|
32
|
+
# Core stages — pure stdlib, always available
|
|
33
|
+
from tibet_continuityd.backpressure import (
|
|
34
|
+
BackpressureMonitor,
|
|
35
|
+
BackpressureSnapshot,
|
|
36
|
+
BackpressureState,
|
|
37
|
+
)
|
|
38
|
+
from tibet_continuityd.police import (
|
|
39
|
+
FindingSeverity,
|
|
40
|
+
PoliceAction,
|
|
41
|
+
PoliceFinding,
|
|
42
|
+
PoliceScanner,
|
|
43
|
+
apply_action,
|
|
44
|
+
)
|
|
45
|
+
from tibet_continuityd.sniff import IntakeClass, sniff_payload
|
|
46
|
+
from tibet_continuityd.trust_kernel import (
|
|
47
|
+
TrustQuery,
|
|
48
|
+
TrustVerdict,
|
|
49
|
+
apply_verdict_to_disposition,
|
|
50
|
+
load_policies,
|
|
51
|
+
query_trust_kernel,
|
|
52
|
+
)
|
|
53
|
+
from tibet_continuityd.watch import LaneWatcher, WatchEvent
|
|
54
|
+
|
|
55
|
+
# Verify + Fork + Seal — require tibet-drop (install via [verify] extra)
|
|
56
|
+
# Defensive imports: core daemon works without these.
|
|
57
|
+
try:
|
|
58
|
+
from tibet_continuityd.verify_fork import (
|
|
59
|
+
CausalIDs,
|
|
60
|
+
VerifyForkResult,
|
|
61
|
+
verify_and_fork,
|
|
62
|
+
)
|
|
63
|
+
from tibet_continuityd.seal import SealEngine, SealResult
|
|
64
|
+
from tibet_continuityd.daemon import ContinuityDaemon
|
|
65
|
+
_HAS_VERIFY = True
|
|
66
|
+
except ImportError:
|
|
67
|
+
# tibet-drop not on sys.path — verify/fork/seal/daemon unavailable.
|
|
68
|
+
# Core sniff/police/trust_kernel/watch/backpressure still work.
|
|
69
|
+
CausalIDs = None # type: ignore
|
|
70
|
+
VerifyForkResult = None # type: ignore
|
|
71
|
+
verify_and_fork = None # type: ignore
|
|
72
|
+
SealEngine = None # type: ignore
|
|
73
|
+
SealResult = None # type: ignore
|
|
74
|
+
ContinuityDaemon = None # type: ignore
|
|
75
|
+
_HAS_VERIFY = False
|
|
76
|
+
|
|
77
|
+
__all__ = [
|
|
78
|
+
# Core (always available)
|
|
79
|
+
"LaneWatcher",
|
|
80
|
+
"WatchEvent",
|
|
81
|
+
"IntakeClass",
|
|
82
|
+
"sniff_payload",
|
|
83
|
+
"TrustQuery",
|
|
84
|
+
"TrustVerdict",
|
|
85
|
+
"query_trust_kernel",
|
|
86
|
+
"apply_verdict_to_disposition",
|
|
87
|
+
"load_policies",
|
|
88
|
+
"PoliceFinding",
|
|
89
|
+
"PoliceScanner",
|
|
90
|
+
"PoliceAction",
|
|
91
|
+
"FindingSeverity",
|
|
92
|
+
"apply_action",
|
|
93
|
+
"BackpressureMonitor",
|
|
94
|
+
"BackpressureSnapshot",
|
|
95
|
+
"BackpressureState",
|
|
96
|
+
# Verify-stage (None when tibet-drop unavailable)
|
|
97
|
+
"ContinuityDaemon",
|
|
98
|
+
"CausalIDs",
|
|
99
|
+
"VerifyForkResult",
|
|
100
|
+
"verify_and_fork",
|
|
101
|
+
"SealEngine",
|
|
102
|
+
"SealResult",
|
|
103
|
+
# Capability flag
|
|
104
|
+
"_HAS_VERIFY",
|
|
105
|
+
]
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""
|
|
2
|
+
backpressure.py — Backpressure / circuit-breaker (v0.3.2, axe 3).
|
|
3
|
+
|
|
4
|
+
Per Jaspers prompt 9 mei 2026:
|
|
5
|
+
|
|
6
|
+
"MUX 500 files/sec, sniff/verify 100/sec. Inbox > 5000 onverwerkt
|
|
7
|
+
→ daemon signaleert MUX/network 'Halt' of TCP-windowing-stijl
|
|
8
|
+
backpressure. Geen self-inflicted DoS."
|
|
9
|
+
|
|
10
|
+
This module implements that protection: monitor the inbox depth
|
|
11
|
+
and emit state-transition audit events when pressure rises or
|
|
12
|
+
falls. The daemon itself does NOT throttle its own work-rate
|
|
13
|
+
(that would worsen things) — it signals UPSTREAM that producer
|
|
14
|
+
should slow down.
|
|
15
|
+
|
|
16
|
+
State machine:
|
|
17
|
+
|
|
18
|
+
NORMAL depth < low_water
|
|
19
|
+
│
|
|
20
|
+
│ depth ≥ low_water
|
|
21
|
+
▼
|
|
22
|
+
PRESSURE_RISING low_water ≤ depth < high_water
|
|
23
|
+
│
|
|
24
|
+
│ depth ≥ high_water
|
|
25
|
+
▼
|
|
26
|
+
OVERLOADED depth ≥ high_water
|
|
27
|
+
│
|
|
28
|
+
│ depth < low_water (hysteresis)
|
|
29
|
+
▼
|
|
30
|
+
RECOVERING transitional state
|
|
31
|
+
│
|
|
32
|
+
│ depth stable < low_water
|
|
33
|
+
▼
|
|
34
|
+
NORMAL
|
|
35
|
+
|
|
36
|
+
Hysteresis: NORMAL → OVERLOADED requires high_water,
|
|
37
|
+
OVERLOADED → NORMAL requires going back below
|
|
38
|
+
low_water (not just back below high_water).
|
|
39
|
+
This prevents oscillation at the boundary.
|
|
40
|
+
|
|
41
|
+
Signal mechanism (v0.3.2 minimal):
|
|
42
|
+
emit "backpressure" audit-record at every state-transition
|
|
43
|
+
(= upstream consumers can read audit-stream and react)
|
|
44
|
+
|
|
45
|
+
Future v0.3.x extensions:
|
|
46
|
+
- write signal-file at /var/lib/tibet/backpressure-state
|
|
47
|
+
- HTTP endpoint POST to mux upstream
|
|
48
|
+
- TIBET token emission for cross-host signaling
|
|
49
|
+
"""
|
|
50
|
+
from __future__ import annotations
|
|
51
|
+
|
|
52
|
+
import time
|
|
53
|
+
from dataclasses import dataclass, field
|
|
54
|
+
from enum import Enum
|
|
55
|
+
from pathlib import Path
|
|
56
|
+
from typing import Optional
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class BackpressureState(Enum):
|
|
60
|
+
"""Daemon's current capacity state."""
|
|
61
|
+
NORMAL = "normal" # depth < low_water
|
|
62
|
+
PRESSURE_RISING = "pressure-rising" # low ≤ depth < high
|
|
63
|
+
OVERLOADED = "overloaded" # depth ≥ high_water
|
|
64
|
+
RECOVERING = "recovering" # was overloaded, depth dropping
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class BackpressureSnapshot:
|
|
69
|
+
"""One observation of inbox depth + current state."""
|
|
70
|
+
state: BackpressureState
|
|
71
|
+
inbox_depth: int # number of unprocessed files
|
|
72
|
+
low_water: int # threshold for "rising"
|
|
73
|
+
high_water: int # threshold for "overloaded"
|
|
74
|
+
transitioned: bool # state changed since last check
|
|
75
|
+
prev_state: Optional[BackpressureState] = None
|
|
76
|
+
ts_unix: float = field(default_factory=time.time)
|
|
77
|
+
|
|
78
|
+
def to_dict(self) -> dict:
|
|
79
|
+
return {
|
|
80
|
+
"state": self.state.value,
|
|
81
|
+
"inbox_depth": self.inbox_depth,
|
|
82
|
+
"low_water": self.low_water,
|
|
83
|
+
"high_water": self.high_water,
|
|
84
|
+
"transitioned": self.transitioned,
|
|
85
|
+
"prev_state": self.prev_state.value
|
|
86
|
+
if self.prev_state else None,
|
|
87
|
+
"ts_unix": self.ts_unix,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class BackpressureMonitor:
|
|
93
|
+
"""Track inbox depth and compute state transitions.
|
|
94
|
+
|
|
95
|
+
Usage:
|
|
96
|
+
monitor = BackpressureMonitor(
|
|
97
|
+
lane=Path("/var/lib/tibet/inbox"),
|
|
98
|
+
low_water=2000,
|
|
99
|
+
high_water=5000,
|
|
100
|
+
)
|
|
101
|
+
snapshot = monitor.check()
|
|
102
|
+
if snapshot.transitioned:
|
|
103
|
+
# emit audit, optionally signal upstream
|
|
104
|
+
...
|
|
105
|
+
"""
|
|
106
|
+
lane: Path
|
|
107
|
+
low_water: int = 2000
|
|
108
|
+
high_water: int = 5000
|
|
109
|
+
_state: BackpressureState = BackpressureState.NORMAL
|
|
110
|
+
|
|
111
|
+
def __post_init__(self):
|
|
112
|
+
if self.low_water >= self.high_water:
|
|
113
|
+
raise ValueError(
|
|
114
|
+
f"low_water ({self.low_water}) must be < "
|
|
115
|
+
f"high_water ({self.high_water})"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def _measure_depth(self) -> int:
|
|
119
|
+
"""Count files in lane (excluding subdirectories)."""
|
|
120
|
+
if not self.lane.exists() or not self.lane.is_dir():
|
|
121
|
+
return 0
|
|
122
|
+
try:
|
|
123
|
+
return sum(1 for entry in self.lane.iterdir()
|
|
124
|
+
if entry.is_file())
|
|
125
|
+
except OSError:
|
|
126
|
+
return 0
|
|
127
|
+
|
|
128
|
+
def _classify_state(self, depth: int) -> BackpressureState:
|
|
129
|
+
"""Compute target state from depth WITHOUT hysteresis logic.
|
|
130
|
+
|
|
131
|
+
Hysteresis is applied in check() — this is the raw mapping.
|
|
132
|
+
"""
|
|
133
|
+
if depth >= self.high_water:
|
|
134
|
+
return BackpressureState.OVERLOADED
|
|
135
|
+
if depth >= self.low_water:
|
|
136
|
+
return BackpressureState.PRESSURE_RISING
|
|
137
|
+
return BackpressureState.NORMAL
|
|
138
|
+
|
|
139
|
+
def check(self) -> BackpressureSnapshot:
|
|
140
|
+
"""Measure depth and update state with hysteresis."""
|
|
141
|
+
depth = self._measure_depth()
|
|
142
|
+
prev = self._state
|
|
143
|
+
target = self._classify_state(depth)
|
|
144
|
+
|
|
145
|
+
# Hysteresis: OVERLOADED can ONLY drop to RECOVERING (not
|
|
146
|
+
# straight to NORMAL via PRESSURE_RISING). This prevents
|
|
147
|
+
# oscillation when depth hovers near low_water.
|
|
148
|
+
new_state = target
|
|
149
|
+
if prev == BackpressureState.OVERLOADED and \
|
|
150
|
+
target != BackpressureState.OVERLOADED:
|
|
151
|
+
# transition out of overloaded → recovering first
|
|
152
|
+
if target == BackpressureState.NORMAL:
|
|
153
|
+
new_state = BackpressureState.RECOVERING
|
|
154
|
+
# else: target is PRESSURE_RISING, accept it
|
|
155
|
+
elif prev == BackpressureState.RECOVERING:
|
|
156
|
+
# In RECOVERING, stay until depth is fully back under
|
|
157
|
+
# low_water (= NORMAL target).
|
|
158
|
+
if target == BackpressureState.NORMAL:
|
|
159
|
+
new_state = BackpressureState.NORMAL
|
|
160
|
+
elif target == BackpressureState.OVERLOADED:
|
|
161
|
+
new_state = BackpressureState.OVERLOADED
|
|
162
|
+
else:
|
|
163
|
+
new_state = BackpressureState.RECOVERING
|
|
164
|
+
|
|
165
|
+
transitioned = (new_state != prev)
|
|
166
|
+
self._state = new_state
|
|
167
|
+
|
|
168
|
+
return BackpressureSnapshot(
|
|
169
|
+
state=new_state,
|
|
170
|
+
inbox_depth=depth,
|
|
171
|
+
low_water=self.low_water,
|
|
172
|
+
high_water=self.high_water,
|
|
173
|
+
transitioned=transitioned,
|
|
174
|
+
prev_state=prev if transitioned else None,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ─── Public API ─────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
__all__ = [
|
|
182
|
+
"BackpressureMonitor",
|
|
183
|
+
"BackpressureSnapshot",
|
|
184
|
+
"BackpressureState",
|
|
185
|
+
]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
coalesce.py — object-level intake coalescing for continuityd v0.3.
|
|
3
|
+
|
|
4
|
+
The watcher stays syscall/event-level. This module collapses a burst of
|
|
5
|
+
related arrival events for the same path into one settled object so the
|
|
6
|
+
daemon reasons about files, not write-close noise.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from tibet_continuityd.watch import WatchEvent
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class FileSnapshot:
|
|
20
|
+
size_bytes: int
|
|
21
|
+
mtime_ns: int
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class PendingArrival:
|
|
26
|
+
latest_event: WatchEvent
|
|
27
|
+
first_seen_ts: float
|
|
28
|
+
last_seen_ts: float
|
|
29
|
+
event_count: int
|
|
30
|
+
last_snapshot: Optional[FileSnapshot]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class SettledArrival:
|
|
35
|
+
lane: Path
|
|
36
|
+
name: str
|
|
37
|
+
full_path: Path
|
|
38
|
+
flags: int
|
|
39
|
+
is_dir: bool
|
|
40
|
+
ts_unix: float
|
|
41
|
+
coalesced: bool
|
|
42
|
+
coalesced_event_count: int
|
|
43
|
+
coalesced_window_ms: int
|
|
44
|
+
settled_after_ms: int
|
|
45
|
+
path_churn_detected: bool
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _safe_snapshot(path: Path) -> Optional[FileSnapshot]:
|
|
49
|
+
try:
|
|
50
|
+
st = path.stat()
|
|
51
|
+
except OSError:
|
|
52
|
+
return None
|
|
53
|
+
return FileSnapshot(size_bytes=st.st_size, mtime_ns=st.st_mtime_ns)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ArrivalCoalescer:
|
|
57
|
+
"""Collapse repeated arrivals for the same path into settled objects."""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
debounce_window_ms: int = 350,
|
|
62
|
+
max_pending_age_ms: int = 5000,
|
|
63
|
+
high_churn_threshold: int = 5,
|
|
64
|
+
):
|
|
65
|
+
self.debounce_window_ms = debounce_window_ms
|
|
66
|
+
self.max_pending_age_ms = max_pending_age_ms
|
|
67
|
+
self.high_churn_threshold = high_churn_threshold
|
|
68
|
+
self._pending: dict[Path, PendingArrival] = {}
|
|
69
|
+
|
|
70
|
+
def ingest(self, event: WatchEvent) -> None:
|
|
71
|
+
snapshot = _safe_snapshot(event.full_path)
|
|
72
|
+
entry = self._pending.get(event.full_path)
|
|
73
|
+
if entry is None:
|
|
74
|
+
self._pending[event.full_path] = PendingArrival(
|
|
75
|
+
latest_event=event,
|
|
76
|
+
first_seen_ts=event.ts_unix,
|
|
77
|
+
last_seen_ts=event.ts_unix,
|
|
78
|
+
event_count=1,
|
|
79
|
+
last_snapshot=snapshot,
|
|
80
|
+
)
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
entry.latest_event = event
|
|
84
|
+
entry.last_seen_ts = event.ts_unix
|
|
85
|
+
entry.event_count += 1
|
|
86
|
+
entry.last_snapshot = snapshot
|
|
87
|
+
|
|
88
|
+
def flush_ready(
|
|
89
|
+
self,
|
|
90
|
+
now: Optional[float] = None,
|
|
91
|
+
) -> list[SettledArrival]:
|
|
92
|
+
now = time.time() if now is None else now
|
|
93
|
+
ready: list[SettledArrival] = []
|
|
94
|
+
expired_paths: list[Path] = []
|
|
95
|
+
|
|
96
|
+
for path, entry in list(self._pending.items()):
|
|
97
|
+
age_ms = int((now - entry.last_seen_ts) * 1000)
|
|
98
|
+
total_age_ms = int((now - entry.first_seen_ts) * 1000)
|
|
99
|
+
|
|
100
|
+
if age_ms < self.debounce_window_ms:
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
current = _safe_snapshot(path)
|
|
104
|
+
if current is None:
|
|
105
|
+
expired_paths.append(path)
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
if current != entry.last_snapshot:
|
|
109
|
+
entry.last_snapshot = current
|
|
110
|
+
entry.last_seen_ts = now
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
if total_age_ms > self.max_pending_age_ms:
|
|
114
|
+
entry.last_seen_ts = now
|
|
115
|
+
|
|
116
|
+
event = entry.latest_event
|
|
117
|
+
ready.append(
|
|
118
|
+
SettledArrival(
|
|
119
|
+
lane=event.lane,
|
|
120
|
+
name=event.name,
|
|
121
|
+
full_path=event.full_path,
|
|
122
|
+
flags=int(event.flags),
|
|
123
|
+
is_dir=event.is_dir,
|
|
124
|
+
ts_unix=event.ts_unix,
|
|
125
|
+
coalesced=entry.event_count > 1,
|
|
126
|
+
coalesced_event_count=entry.event_count,
|
|
127
|
+
coalesced_window_ms=self.debounce_window_ms,
|
|
128
|
+
settled_after_ms=total_age_ms,
|
|
129
|
+
path_churn_detected=(
|
|
130
|
+
entry.event_count > self.high_churn_threshold
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
expired_paths.append(path)
|
|
135
|
+
|
|
136
|
+
for path in expired_paths:
|
|
137
|
+
self._pending.pop(path, None)
|
|
138
|
+
|
|
139
|
+
return ready
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def pending_count(self) -> int:
|
|
143
|
+
return len(self._pending)
|