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.
@@ -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,7 @@
1
+ """CLI entry: `python -m tibet_continuityd`."""
2
+ import sys
3
+
4
+ from tibet_continuityd.daemon import main
5
+
6
+ if __name__ == "__main__":
7
+ sys.exit(main())
@@ -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)