fable-engine 1.3.1__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.
- fable_compressor.py +356 -0
- fable_engine/__init__.py +1 -0
- fable_engine/actions/__init__.py +291 -0
- fable_engine/actions/cas.py +182 -0
- fable_engine/actions/deliberation.py +523 -0
- fable_engine/actions/fleet.py +807 -0
- fable_engine/actions/lifecycle.py +298 -0
- fable_engine/actions/scrapers.py +116 -0
- fable_engine/actions/system3.py +815 -0
- fable_engine/browser.py +824 -0
- fable_engine/cas.py +974 -0
- fable_engine/fable_session.json +510 -0
- fable_engine/guards.py +283 -0
- fable_engine/schema.py +714 -0
- fable_engine/scrapers/__init__.py +32 -0
- fable_engine/scrapers/arxiv.py +115 -0
- fable_engine/scrapers/base.py +386 -0
- fable_engine/scrapers/github.py +129 -0
- fable_engine/scrapers/reddit.py +154 -0
- fable_engine/scrapers/web.py +120 -0
- fable_engine/scrapers/x.py +125 -0
- fable_engine/scrapers/youtube.py +132 -0
- fable_engine/server.py +414 -0
- fable_engine/session.py +1819 -0
- fable_engine/test_server.py +1362 -0
- fable_engine/updater.py +541 -0
- fable_engine-1.3.1.dist-info/LICENSE +22 -0
- fable_engine-1.3.1.dist-info/METADATA +173 -0
- fable_engine-1.3.1.dist-info/RECORD +104 -0
- fable_engine-1.3.1.dist-info/WHEEL +5 -0
- fable_engine-1.3.1.dist-info/entry_points.txt +5 -0
- fable_engine-1.3.1.dist-info/top_level.txt +6 -0
- fable_mode/__init__.py +3 -0
- fable_mode/__main__.py +4 -0
- fable_mode/adapters.py +1014 -0
- fable_mode/installer.py +553 -0
- fable_mode/launcher.py +437 -0
- fable_mode/manifest.py +142 -0
- fable_mode/resources.json +114 -0
- fable_mode/safety.py +103 -0
- fable_mode_entry.py +10 -0
- fable_v2/__init__.py +146 -0
- fable_v2/adapters.py +151 -0
- fable_v2/coder_fleet/__init__.py +100 -0
- fable_v2/coder_fleet/ast_tools.py +158 -0
- fable_v2/coder_fleet/compute.py +199 -0
- fable_v2/coder_fleet/design_engine.py +1316 -0
- fable_v2/coder_fleet/diagnostics.py +293 -0
- fable_v2/coder_fleet/fleet_dispatcher.py +214 -0
- fable_v2/coder_fleet/mock_auditor.py +306 -0
- fable_v2/coder_fleet/mutation.py +216 -0
- fable_v2/coder_fleet/property_oracle.py +260 -0
- fable_v2/coder_fleet/receipt_attestor.py +122 -0
- fable_v2/coder_fleet/red_team_swarm.py +908 -0
- fable_v2/coder_fleet/test_harness.py +198 -0
- fable_v2/coder_fleet/vector_engine.py +1287 -0
- fable_v2/coder_fleet/visual.py +357 -0
- fable_v2/coder_fleet/workspace.py +153 -0
- fable_v2/cortical/__init__.py +20 -0
- fable_v2/cortical/plasticity_engine.py +992 -0
- fable_v2/execution_broker.py +811 -0
- fable_v2/proof_engine.py +1141 -0
- fable_v2/protocol.py +485 -0
- fable_v2/runtime.py +1010 -0
- fable_v2/system3/__init__.py +204 -0
- fable_v2/system3/causal.py +558 -0
- fable_v2/system3/dialectical.py +577 -0
- fable_v2/system3/evolution.py +503 -0
- fable_v2/system3/executive.py +338 -0
- fable_v2/system3/free_energy.py +479 -0
- fable_v2/system3/hyperbolic.py +555 -0
- fable_v2/system3/induction.py +336 -0
- fable_v2/system3/kripke.py +548 -0
- fable_v2/system3/oracle.py +745 -0
- fable_v2/verifiers.py +72 -0
- tests/__init__.py +1 -0
- tests/test_anti_loop_circuit_breaker.py +64 -0
- tests/test_auto_updater.py +407 -0
- tests/test_coder_fleet.py +535 -0
- tests/test_delegation_compiler.py +54 -0
- tests/test_descriptor_boundaries.py +126 -0
- tests/test_design_engine.py +603 -0
- tests/test_epistemic_evidence_validator.py +66 -0
- tests/test_execution_broker.py +233 -0
- tests/test_fable_v2.py +406 -0
- tests/test_fleet_transitions.py +116 -0
- tests/test_fsm_redteam_evolution.py +406 -0
- tests/test_goal_rubric_and_pipeline.py +367 -0
- tests/test_hebbian_plasticity.py +585 -0
- tests/test_packaging_runtime.py +194 -0
- tests/test_proof_engine.py +259 -0
- tests/test_red_team_swarm.py +645 -0
- tests/test_redteam_remediation.py +169 -0
- tests/test_registration_transaction.py +375 -0
- tests/test_requested_regressions.py +467 -0
- tests/test_scrapers.py +370 -0
- tests/test_server_actions.py +93 -0
- tests/test_server_frontier_actions.py +269 -0
- tests/test_server_protocol.py +88 -0
- tests/test_stealth_browser.py +970 -0
- tests/test_system3.py +381 -0
- tests/test_system3_deep_integration.py +385 -0
- tests/test_system3_frontier.py +436 -0
- tests/test_vector_engine.py +608 -0
fable_engine/session.py
ADDED
|
@@ -0,0 +1,1819 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fable-Mode Cognitive Session Engine and State Machine.
|
|
3
|
+
Implements:
|
|
4
|
+
- FableSession: Invariant-checked, auditable System 2 deliberation session
|
|
5
|
+
- SessionState: Finite state machine with validated transitions
|
|
6
|
+
- Cross-process atomic session locking and persistence
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import collections
|
|
12
|
+
import copy
|
|
13
|
+
import hashlib
|
|
14
|
+
import hmac
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
import math
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import secrets
|
|
21
|
+
import stat
|
|
22
|
+
import sys
|
|
23
|
+
import tempfile
|
|
24
|
+
import threading
|
|
25
|
+
import time
|
|
26
|
+
from contextlib import contextmanager
|
|
27
|
+
from enum import Enum
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger("fable-engine.session")
|
|
32
|
+
|
|
33
|
+
BASE_DIR = Path(__file__).resolve().parent
|
|
34
|
+
if str(BASE_DIR.parent) not in sys.path:
|
|
35
|
+
sys.path.insert(0, str(BASE_DIR.parent))
|
|
36
|
+
|
|
37
|
+
from fable_engine.cas import (
|
|
38
|
+
DATA_DIR,
|
|
39
|
+
_assert_private_path,
|
|
40
|
+
_open_directory_nofollow,
|
|
41
|
+
)
|
|
42
|
+
from fable_engine.guards import GLOBAL_VELOCITY_PROFILER
|
|
43
|
+
|
|
44
|
+
_GLOBAL_RED_TEAM_SWARM = None
|
|
45
|
+
_GLOBAL_PLASTICITY_ENGINE = None
|
|
46
|
+
|
|
47
|
+
def get_red_team_swarm():
|
|
48
|
+
global _GLOBAL_RED_TEAM_SWARM
|
|
49
|
+
if _GLOBAL_RED_TEAM_SWARM is None:
|
|
50
|
+
from fable_v2.coder_fleet import RedTeamSwarm
|
|
51
|
+
_GLOBAL_RED_TEAM_SWARM = RedTeamSwarm(plasticity_engine=get_plasticity_engine())
|
|
52
|
+
return _GLOBAL_RED_TEAM_SWARM
|
|
53
|
+
|
|
54
|
+
def get_plasticity_engine():
|
|
55
|
+
global _GLOBAL_PLASTICITY_ENGINE
|
|
56
|
+
if _GLOBAL_PLASTICITY_ENGINE is None:
|
|
57
|
+
from fable_v2.cortical import HebbianPlasticityEngine
|
|
58
|
+
_GLOBAL_PLASTICITY_ENGINE = HebbianPlasticityEngine(cortex_dir=DATA_DIR / "cortex")
|
|
59
|
+
return _GLOBAL_PLASTICITY_ENGINE
|
|
60
|
+
|
|
61
|
+
get_default_plasticity_engine = get_plasticity_engine
|
|
62
|
+
|
|
63
|
+
def __getattr__(name: str) -> Any:
|
|
64
|
+
if name == "GLOBAL_RED_TEAM_SWARM":
|
|
65
|
+
return get_red_team_swarm()
|
|
66
|
+
if name == "GLOBAL_PLASTICITY_ENGINE":
|
|
67
|
+
return get_plasticity_engine()
|
|
68
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
from fable_engine.updater import AutoUpdater
|
|
72
|
+
except ImportError:
|
|
73
|
+
try:
|
|
74
|
+
from updater import AutoUpdater
|
|
75
|
+
except ImportError:
|
|
76
|
+
AutoUpdater = None
|
|
77
|
+
|
|
78
|
+
SESSIONS_DIR = DATA_DIR / "sessions"
|
|
79
|
+
_assert_private_path(SESSIONS_DIR)
|
|
80
|
+
SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
_assert_private_path(SESSIONS_DIR)
|
|
82
|
+
if SESSIONS_DIR.is_symlink() or not SESSIONS_DIR.is_dir():
|
|
83
|
+
raise RuntimeError("SESSIONS_DIR must be a real directory")
|
|
84
|
+
os.chmod(SESSIONS_DIR, 0o700)
|
|
85
|
+
|
|
86
|
+
PHASES = [
|
|
87
|
+
"Phase 1: Epistemic Grounding & Live Research",
|
|
88
|
+
"Phase 2: Invariant Specification & Blueprint",
|
|
89
|
+
"Phase 3: Adversarial Red-Teaming & Falsification",
|
|
90
|
+
"Phase 4: Subagent Fleet Delegation",
|
|
91
|
+
"Phase 5: Multi-Tier Verification & Gatekeeping",
|
|
92
|
+
"Phase 6: Final Walkthrough & Reporting"
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
PHASE_INDEX_MAP = {phase: i + 1 for i, phase in enumerate(PHASES)}
|
|
96
|
+
|
|
97
|
+
MIN_TIME_BUDGET_MINUTES = 2.0
|
|
98
|
+
MAX_TIME_BUDGET_MINUTES = 7 * 24 * 60
|
|
99
|
+
MAX_REMEDIATION_ATTEMPTS = 5
|
|
100
|
+
MAX_REMEDIATION_SECONDS = 900.0
|
|
101
|
+
RED_TEAM_ATTACK_VECTORS = (
|
|
102
|
+
"chaos_environment",
|
|
103
|
+
"byzantine_payload",
|
|
104
|
+
"concurrency_race",
|
|
105
|
+
"resource_exhaustion",
|
|
106
|
+
"state_invariant",
|
|
107
|
+
)
|
|
108
|
+
FORCE_UNLOCK_ENV = "FABLE_FORCE_UNLOCK_TOKEN"
|
|
109
|
+
SESSION_NAME_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$")
|
|
110
|
+
|
|
111
|
+
SILENT_DELIBERATION_REMINDER = (
|
|
112
|
+
"\n\n> [!IMPORTANT]\n"
|
|
113
|
+
"> 🛑 **SILENT-DELIBERATION ACTIVE (Zero-Chat Lockout)**: Do NOT emit conversational responses "
|
|
114
|
+
"or prompt the user while time-lock is active. Continue internal tool-reasoning, terminal benchmarks "
|
|
115
|
+
"(`run_command`), artifact authoring, and rethink-refine cycles until the authority deadline elapses."
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def _validate_time_budget(value: Any, field_name: str = "time_budget_minutes") -> float:
|
|
119
|
+
"""Validate a duration before it can influence an execution deadline."""
|
|
120
|
+
try:
|
|
121
|
+
minutes = float(value)
|
|
122
|
+
except (TypeError, ValueError) as exc:
|
|
123
|
+
raise ValueError(f"Invalid {field_name}: expected a finite number of minutes.") from exc
|
|
124
|
+
if not math.isfinite(minutes) or not (MIN_TIME_BUDGET_MINUTES <= minutes <= MAX_TIME_BUDGET_MINUTES):
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"Invalid {field_name}: minimum allowed time budget is {MIN_TIME_BUDGET_MINUTES} minutes "
|
|
127
|
+
f"(must be between {MIN_TIME_BUDGET_MINUTES} and {MAX_TIME_BUDGET_MINUTES} finite minutes to ensure adequate epistemic grounding and deliberation). "
|
|
128
|
+
f"Provided: {minutes} minutes."
|
|
129
|
+
)
|
|
130
|
+
return minutes
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _validate_session_name(name: str) -> str:
|
|
134
|
+
"""Keep session persistence inside SESSIONS_DIR and make identifiers portable."""
|
|
135
|
+
clean_name = (name or "").strip()
|
|
136
|
+
if not SESSION_NAME_PATTERN.fullmatch(clean_name):
|
|
137
|
+
raise ValueError(
|
|
138
|
+
"Invalid session_name: use 1-128 letters, numbers, '.', '_' or '-' "
|
|
139
|
+
"and do not include path separators."
|
|
140
|
+
)
|
|
141
|
+
return clean_name
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class SessionState(str, Enum):
|
|
146
|
+
INIT = "INIT"
|
|
147
|
+
DEEPTHINK_TIMELOCK = "DEEPTHINK_TIMELOCK"
|
|
148
|
+
IMPLEMENTATION = "IMPLEMENTATION"
|
|
149
|
+
RED_TEAM_GATE = "RED_TEAM_GATE"
|
|
150
|
+
ARBITRATION = "ARBITRATION"
|
|
151
|
+
REMEDIATION_REQUIRED = "REMEDIATION_REQUIRED"
|
|
152
|
+
ESCALATION_UNRESOLVED_BREAKAGES = "ESCALATION_UNRESOLVED_BREAKAGES"
|
|
153
|
+
SEALED = "SEALED"
|
|
154
|
+
EVOLVED = "EVOLVED"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
VALID_TRANSITIONS = {
|
|
158
|
+
SessionState.INIT: {SessionState.DEEPTHINK_TIMELOCK},
|
|
159
|
+
SessionState.DEEPTHINK_TIMELOCK: {SessionState.IMPLEMENTATION},
|
|
160
|
+
SessionState.IMPLEMENTATION: {SessionState.RED_TEAM_GATE},
|
|
161
|
+
SessionState.RED_TEAM_GATE: {SessionState.ARBITRATION, SessionState.REMEDIATION_REQUIRED},
|
|
162
|
+
SessionState.ARBITRATION: {SessionState.REMEDIATION_REQUIRED, SessionState.SEALED},
|
|
163
|
+
SessionState.REMEDIATION_REQUIRED: {
|
|
164
|
+
SessionState.ARBITRATION,
|
|
165
|
+
SessionState.SEALED,
|
|
166
|
+
SessionState.REMEDIATION_REQUIRED,
|
|
167
|
+
SessionState.ESCALATION_UNRESOLVED_BREAKAGES,
|
|
168
|
+
},
|
|
169
|
+
SessionState.ESCALATION_UNRESOLVED_BREAKAGES: {SessionState.ESCALATION_UNRESOLVED_BREAKAGES},
|
|
170
|
+
SessionState.SEALED: {SessionState.EVOLVED},
|
|
171
|
+
SessionState.EVOLVED: {SessionState.EVOLVED},
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class FableSession:
|
|
176
|
+
"""Represents an active Fable reasoning & pacing session."""
|
|
177
|
+
|
|
178
|
+
def __init__(
|
|
179
|
+
self,
|
|
180
|
+
session_name: str,
|
|
181
|
+
objective: str,
|
|
182
|
+
time_budget_minutes: float,
|
|
183
|
+
session_id: Optional[str] = None,
|
|
184
|
+
start_time: Optional[float] = None,
|
|
185
|
+
wall_clock: Optional[Any] = None,
|
|
186
|
+
monotonic_clock: Optional[Any] = None
|
|
187
|
+
):
|
|
188
|
+
self.session_name = _validate_session_name(session_name)
|
|
189
|
+
self.objective = objective
|
|
190
|
+
self._wall_clock = wall_clock or time.time
|
|
191
|
+
self._monotonic_clock = monotonic_clock or time.monotonic
|
|
192
|
+
self.start_time = start_time if start_time is not None else self._wall_clock()
|
|
193
|
+
self.session_id = session_id or f"fable_{session_name}_{int(self.start_time)}"
|
|
194
|
+
|
|
195
|
+
# The authority budget is immutable after session creation. A separate
|
|
196
|
+
# pacing timer may be shortened by the agent, but it can never grant
|
|
197
|
+
# execution permission or move this outer deadline earlier.
|
|
198
|
+
self.time_budget_minutes = _validate_time_budget(time_budget_minutes)
|
|
199
|
+
self.time_budget_seconds = self.time_budget_minutes * 60.0
|
|
200
|
+
self._authority_deadline_wall = self.start_time + self.time_budget_seconds
|
|
201
|
+
self._authority_deadline_monotonic = self._monotonic_clock() + self.time_budget_seconds
|
|
202
|
+
|
|
203
|
+
self.pacing_budget_minutes = self.time_budget_minutes
|
|
204
|
+
self.pacing_budget_seconds = self.time_budget_seconds
|
|
205
|
+
self._pacing_started_wall = self.start_time
|
|
206
|
+
self._pacing_started_monotonic = self._authority_deadline_monotonic - self.time_budget_seconds
|
|
207
|
+
self._pacing_deadline_wall = self._authority_deadline_wall
|
|
208
|
+
self._pacing_deadline_monotonic = self._authority_deadline_monotonic
|
|
209
|
+
|
|
210
|
+
self.active_phase = PHASES[0]
|
|
211
|
+
self.execution_locked = True
|
|
212
|
+
self.can_execute_code = False
|
|
213
|
+
|
|
214
|
+
self.current_state = SessionState.INIT
|
|
215
|
+
self.iteration_count = 0
|
|
216
|
+
self.remediation_attempt_count = 0
|
|
217
|
+
self.remediation_started_at: Optional[float] = None
|
|
218
|
+
self.active_breakages: List[Dict[str, Any]] = []
|
|
219
|
+
self.remediation_history: List[Dict[str, Any]] = []
|
|
220
|
+
self._timer_set = False
|
|
221
|
+
|
|
222
|
+
self.epistemic_ledger: List[Dict[str, Any]] = []
|
|
223
|
+
self.invariants: List[Dict[str, Any]] = []
|
|
224
|
+
self.refinement_cycles: List[Dict[str, Any]] = []
|
|
225
|
+
self.file_changes: List[Dict[str, Any]] = []
|
|
226
|
+
self.visual_mockups: Dict[str, Any] = {"mockups": [], "selected_concept": None}
|
|
227
|
+
self.proof_receipts: List[Dict[str, Any]] = []
|
|
228
|
+
self.goal_rubrics: List[Dict[str, Any]] = []
|
|
229
|
+
self.automation_pipelines: List[Dict[str, Any]] = []
|
|
230
|
+
self.breakage_reports: List[Dict[str, Any]] = []
|
|
231
|
+
self.phase_history: List[Dict[str, Any]] = [
|
|
232
|
+
{
|
|
233
|
+
"phase": self.active_phase,
|
|
234
|
+
"entered_at": self.start_time,
|
|
235
|
+
"summary": "Session initialized"
|
|
236
|
+
}
|
|
237
|
+
]
|
|
238
|
+
self.unlock_details: Optional[Dict[str, Any]] = None
|
|
239
|
+
self._restored_untrusted = False
|
|
240
|
+
self._sealing_authorized = False
|
|
241
|
+
self._red_team_receipt_key = secrets.token_bytes(32)
|
|
242
|
+
self._reviewed_change_id: Optional[str] = None
|
|
243
|
+
|
|
244
|
+
# System 3 Meta-Cognitive State
|
|
245
|
+
self.system3_causal_graphs: List[Dict[str, Any]] = []
|
|
246
|
+
self.system3_syntheses: List[Dict[str, Any]] = []
|
|
247
|
+
self.system3_gene_pools: List[Dict[str, Any]] = []
|
|
248
|
+
self.system3_axioms: List[Dict[str, Any]] = []
|
|
249
|
+
self.system3_reflections: List[Dict[str, Any]] = []
|
|
250
|
+
self.system3_orchestrations: List[Dict[str, Any]] = []
|
|
251
|
+
self.system3_hyperbolic_embeddings: List[Dict[str, Any]] = []
|
|
252
|
+
self.system3_kripke_verifications: List[Dict[str, Any]] = []
|
|
253
|
+
self.system3_active_inferences: List[Dict[str, Any]] = []
|
|
254
|
+
self.system3_proof_oracle_verifications: List[Dict[str, Any]] = []
|
|
255
|
+
self.active_free_energy: Optional[Dict[str, Any]] = None
|
|
256
|
+
self.active_kripke_safety: Optional[Dict[str, Any]] = None
|
|
257
|
+
self.active_biases: List[Dict[str, Any]] = []
|
|
258
|
+
self._disk_mtime: float = 0.0
|
|
259
|
+
|
|
260
|
+
# Unified Fable V2 Runtime instance
|
|
261
|
+
try:
|
|
262
|
+
from fable_v2.runtime import new_run
|
|
263
|
+
from fable_v2.protocol import TaskSpec
|
|
264
|
+
task = TaskSpec(
|
|
265
|
+
task_id=f"task_{self.session_id}",
|
|
266
|
+
objective=self.objective or "Fable Mode Deliberation",
|
|
267
|
+
definition_of_done=("Epistemic evidence verified",),
|
|
268
|
+
)
|
|
269
|
+
self.fable_run: Optional[Any] = new_run(session_id=self.session_id, task=task)
|
|
270
|
+
except Exception:
|
|
271
|
+
self.fable_run = None
|
|
272
|
+
|
|
273
|
+
# Autonomous Silent Self-Updater Trigger
|
|
274
|
+
if (
|
|
275
|
+
AutoUpdater is not None
|
|
276
|
+
and not os.environ.get("FABLE_DISABLE_AUTO_UPDATE")
|
|
277
|
+
and not os.environ.get("PYTEST_CURRENT_TEST")
|
|
278
|
+
and not os.environ.get("GITHUB_ACTIONS")
|
|
279
|
+
and not os.environ.get("CI")
|
|
280
|
+
):
|
|
281
|
+
try:
|
|
282
|
+
AutoUpdater().trigger_silent_background_update()
|
|
283
|
+
except Exception as e:
|
|
284
|
+
logger.debug(f"Silent auto-updater background trigger failed on session init: {e}")
|
|
285
|
+
|
|
286
|
+
@property
|
|
287
|
+
def pacing_deadline_time(self) -> float:
|
|
288
|
+
"""Wall-clock representation of the internal pacing deadline."""
|
|
289
|
+
return self._pacing_deadline_wall
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def deadline_time(self) -> float:
|
|
293
|
+
"""Read-only wall-clock representation of the authority deadline."""
|
|
294
|
+
return self._authority_deadline_wall
|
|
295
|
+
|
|
296
|
+
def transition_to(self, new_state: Union[SessionState, str], rationale: str = "") -> SessionState:
|
|
297
|
+
"""Transitions the FSM to a new state after validating strict state machine invariants.
|
|
298
|
+
|
|
299
|
+
Valid transitions:
|
|
300
|
+
- INIT -> DEEPTHINK_TIMELOCK (requires set_timer and/or set_goal_rubric)
|
|
301
|
+
- DEEPTHINK_TIMELOCK -> IMPLEMENTATION (requires execution unlocked)
|
|
302
|
+
- IMPLEMENTATION -> RED_TEAM_GATE (requires code written / file changes logged)
|
|
303
|
+
- RED_TEAM_GATE -> ARBITRATION
|
|
304
|
+
- ARBITRATION -> REMEDIATION_REQUIRED (if broken_count > 0)
|
|
305
|
+
- ARBITRATION -> SEALED (if broken_count == 0)
|
|
306
|
+
- REMEDIATION_REQUIRED -> ARBITRATION
|
|
307
|
+
- REMEDIATION_REQUIRED -> SEALED (when remediation verification succeeds with 0 breakages)
|
|
308
|
+
- SEALED -> EVOLVED (upon evolve_cortex consolidation)
|
|
309
|
+
|
|
310
|
+
Attempting any illegal jump (e.g. INIT directly to SEALED or IMPLEMENTATION) must raise ValueError.
|
|
311
|
+
"""
|
|
312
|
+
if isinstance(new_state, str):
|
|
313
|
+
try:
|
|
314
|
+
target_state = SessionState(new_state)
|
|
315
|
+
except ValueError:
|
|
316
|
+
raise ValueError(f"Invalid SessionState: '{new_state}'. Allowed states: {[s.value for s in SessionState]}")
|
|
317
|
+
else:
|
|
318
|
+
target_state = new_state
|
|
319
|
+
|
|
320
|
+
allowed = VALID_TRANSITIONS.get(self.current_state, set())
|
|
321
|
+
if target_state not in allowed and target_state != self.current_state:
|
|
322
|
+
raise ValueError(
|
|
323
|
+
f"Illegal state transition from {self.current_state.value} to {target_state.value}. "
|
|
324
|
+
f"Allowed transitions from {self.current_state.value}: {[s.value for s in allowed]}"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
if (
|
|
328
|
+
target_state == SessionState.SEALED
|
|
329
|
+
and self.current_state != SessionState.SEALED
|
|
330
|
+
and not self._sealing_authorized
|
|
331
|
+
):
|
|
332
|
+
raise ValueError(
|
|
333
|
+
"Transition to SEALED requires a clean report validated by record_breakage_report()."
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
if self.current_state == SessionState.INIT and target_state == SessionState.DEEPTHINK_TIMELOCK:
|
|
337
|
+
timer_set = getattr(self, "_timer_set", False) or (self.pacing_budget_minutes != self.time_budget_minutes)
|
|
338
|
+
rubric_set = len(self.goal_rubrics) > 0
|
|
339
|
+
rationale_ok = any(k in rationale.lower() for k in ("timer", "rubric", "deepthink", "timelock", "set_timer", "set_goal_rubric"))
|
|
340
|
+
if not (timer_set or rubric_set or rationale_ok):
|
|
341
|
+
raise ValueError("Transition from INIT to DEEPTHINK_TIMELOCK requires set_timer and/or set_goal_rubric.")
|
|
342
|
+
|
|
343
|
+
elif self.current_state == SessionState.DEEPTHINK_TIMELOCK and target_state == SessionState.IMPLEMENTATION:
|
|
344
|
+
if self.execution_locked or not self.can_execute_code:
|
|
345
|
+
raise ValueError("Transition from DEEPTHINK_TIMELOCK to IMPLEMENTATION requires execution unlocked.")
|
|
346
|
+
|
|
347
|
+
elif self.current_state == SessionState.IMPLEMENTATION and target_state == SessionState.RED_TEAM_GATE:
|
|
348
|
+
has_changes = len(self.file_changes) > 0 or any(k in rationale.lower() for k in ("code", "file", "implement", "change", "written", "edit"))
|
|
349
|
+
if not has_changes:
|
|
350
|
+
raise ValueError("Transition from IMPLEMENTATION to RED_TEAM_GATE requires code written / file changes logged.")
|
|
351
|
+
|
|
352
|
+
elif self.current_state == SessionState.ARBITRATION and target_state == SessionState.SEALED:
|
|
353
|
+
if len(self.active_breakages) > 0:
|
|
354
|
+
raise ValueError(f"Transition from ARBITRATION to SEALED rejected: {len(self.active_breakages)} breakages remain unresolved.")
|
|
355
|
+
|
|
356
|
+
elif self.current_state == SessionState.REMEDIATION_REQUIRED and target_state == SessionState.SEALED:
|
|
357
|
+
if len(self.active_breakages) > 0:
|
|
358
|
+
raise ValueError(f"Transition from REMEDIATION_REQUIRED to SEALED rejected: {len(self.active_breakages)} breakages remain unresolved.")
|
|
359
|
+
|
|
360
|
+
self.current_state = target_state
|
|
361
|
+
return self.current_state
|
|
362
|
+
|
|
363
|
+
def set_timer(self, time_budget_minutes: float) -> Dict[str, Any]:
|
|
364
|
+
"""Set an agent pacing timer without changing the authority deadline.
|
|
365
|
+
|
|
366
|
+
This is deliberately a sub-timer: an agent may choose to pace itself
|
|
367
|
+
for 20 minutes inside an 80-minute session, but expiry of this timer
|
|
368
|
+
never unlocks execution. Only the immutable outer deadline can do that.
|
|
369
|
+
"""
|
|
370
|
+
pacing_minutes = _validate_time_budget(time_budget_minutes)
|
|
371
|
+
self.pacing_budget_minutes = pacing_minutes
|
|
372
|
+
self.pacing_budget_seconds = pacing_minutes * 60.0
|
|
373
|
+
now_wall = self._wall_clock()
|
|
374
|
+
now_monotonic = self._monotonic_clock()
|
|
375
|
+
self._pacing_started_wall = now_wall
|
|
376
|
+
self._pacing_started_monotonic = now_monotonic
|
|
377
|
+
self._pacing_deadline_wall = min(
|
|
378
|
+
self.deadline_time,
|
|
379
|
+
now_wall + self.pacing_budget_seconds
|
|
380
|
+
)
|
|
381
|
+
self._pacing_deadline_monotonic = min(
|
|
382
|
+
self._authority_deadline_monotonic,
|
|
383
|
+
now_monotonic + self.pacing_budget_seconds
|
|
384
|
+
)
|
|
385
|
+
self._timer_set = True
|
|
386
|
+
if self.current_state == SessionState.INIT:
|
|
387
|
+
self.transition_to(SessionState.DEEPTHINK_TIMELOCK, "Pacing timer configured")
|
|
388
|
+
return self.get_telemetry()
|
|
389
|
+
|
|
390
|
+
def _authority_remaining_seconds(self) -> float:
|
|
391
|
+
"""Use monotonic time while the process is alive to resist clock rollback."""
|
|
392
|
+
return self._authority_deadline_monotonic - self._monotonic_clock()
|
|
393
|
+
|
|
394
|
+
def _pacing_remaining_seconds(self) -> float:
|
|
395
|
+
return self._pacing_deadline_monotonic - self._monotonic_clock()
|
|
396
|
+
|
|
397
|
+
def _gate_report(self) -> Dict[str, Any]:
|
|
398
|
+
"""Return auditable gate state instead of relying on raw item counts."""
|
|
399
|
+
proven_items = [i for i in self.epistemic_ledger if i.get("tag") == "PROVEN" and not i.get("_restored_untrusted")]
|
|
400
|
+
proven_with_evidence = [i for i in proven_items if str(i.get("evidence", "")).strip()]
|
|
401
|
+
invariants_with_proof = [
|
|
402
|
+
inv for inv in self.invariants if not inv.get("_restored_untrusted") and str(inv.get("proof_or_rationale", "")).strip()
|
|
403
|
+
]
|
|
404
|
+
# Restored phase is reset to Phase 1; subsequent in-process
|
|
405
|
+
# transitions are legitimate fresh gates.
|
|
406
|
+
phase_index = PHASE_INDEX_MAP.get(self.active_phase, 1)
|
|
407
|
+
checks = {
|
|
408
|
+
"two_proven_evidence_items": len(proven_with_evidence) >= 2,
|
|
409
|
+
"one_proved_invariant": len(invariants_with_proof) >= 1,
|
|
410
|
+
"adversarial_phase_reached": phase_index >= 3,
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
"ready": all(checks.values()),
|
|
414
|
+
"checks": checks,
|
|
415
|
+
"proven_with_evidence": len(proven_with_evidence),
|
|
416
|
+
"invariants_with_proof": len(invariants_with_proof),
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
def get_telemetry(self) -> Dict[str, Any]:
|
|
420
|
+
"""Calculates runtime authority, pacing, and cognitive-gate telemetry."""
|
|
421
|
+
now = self._wall_clock()
|
|
422
|
+
now_monotonic = self._monotonic_clock()
|
|
423
|
+
elapsed_seconds = max(0.0, now - self.start_time)
|
|
424
|
+
pacing_elapsed_seconds = max(0.0, now_monotonic - self._pacing_started_monotonic)
|
|
425
|
+
authority_remaining = self._authority_remaining_seconds()
|
|
426
|
+
pacing_remaining = self._pacing_remaining_seconds()
|
|
427
|
+
pacing_ratio = pacing_elapsed_seconds / self.pacing_budget_seconds
|
|
428
|
+
|
|
429
|
+
proven_count = sum(1 for item in self.epistemic_ledger if item.get("tag") == "PROVEN")
|
|
430
|
+
hypothesis_count = sum(1 for item in self.epistemic_ledger if item.get("tag") == "HYPOTHESIS")
|
|
431
|
+
unknown_count = sum(1 for item in self.epistemic_ledger if item.get("tag") == "UNKNOWN")
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
"session_name": self.session_name,
|
|
435
|
+
"session_id": self.session_id,
|
|
436
|
+
"objective": self.objective,
|
|
437
|
+
"start_time": self.start_time,
|
|
438
|
+
"time_budget_minutes": self.time_budget_minutes,
|
|
439
|
+
"time_budget_seconds": self.time_budget_seconds,
|
|
440
|
+
"deadline_time": self.deadline_time,
|
|
441
|
+
"authority_deadline_time": self.deadline_time,
|
|
442
|
+
"elapsed_seconds": round(elapsed_seconds, 2),
|
|
443
|
+
"remaining_seconds": round(authority_remaining, 2),
|
|
444
|
+
"elapsed_formatted": self._format_duration(elapsed_seconds),
|
|
445
|
+
"remaining_formatted": self._format_duration(max(0.0, authority_remaining)),
|
|
446
|
+
"authority_remaining_seconds": round(authority_remaining, 2),
|
|
447
|
+
"authority_remaining_formatted": self._format_duration(max(0.0, authority_remaining)),
|
|
448
|
+
"pacing_budget_minutes": self.pacing_budget_minutes,
|
|
449
|
+
"pacing_started_time": self._pacing_started_wall,
|
|
450
|
+
"pacing_deadline_time": self._pacing_deadline_wall,
|
|
451
|
+
"pacing_remaining_seconds": round(pacing_remaining, 2),
|
|
452
|
+
"pacing_remaining_formatted": self._format_duration(max(0.0, pacing_remaining)),
|
|
453
|
+
"pacing_ratio": round(pacing_ratio, 4),
|
|
454
|
+
"pacing_percentage": f"{pacing_ratio * 100.0:.1f}%",
|
|
455
|
+
"active_phase": self.active_phase,
|
|
456
|
+
"phase_index": PHASE_INDEX_MAP.get(self.active_phase, 1),
|
|
457
|
+
"total_phases": len(PHASES),
|
|
458
|
+
"execution_locked": self.execution_locked,
|
|
459
|
+
"can_execute_code": self.can_execute_code,
|
|
460
|
+
"silent_deliberation_active": self.execution_locked,
|
|
461
|
+
"current_state": self.current_state.value if isinstance(self.current_state, SessionState) else str(self.current_state),
|
|
462
|
+
"iteration_count": self.iteration_count,
|
|
463
|
+
"remediation_attempt_count": self.remediation_attempt_count,
|
|
464
|
+
"remediation_started_at": self.remediation_started_at,
|
|
465
|
+
"active_breakages_count": len(self.active_breakages),
|
|
466
|
+
"active_breakages": self.active_breakages,
|
|
467
|
+
"remediation_history": self.remediation_history,
|
|
468
|
+
"epistemic_counts": {
|
|
469
|
+
"proven": proven_count,
|
|
470
|
+
"hypothesis": hypothesis_count,
|
|
471
|
+
"unknown": unknown_count,
|
|
472
|
+
"total": len(self.epistemic_ledger)
|
|
473
|
+
},
|
|
474
|
+
"invariants_count": len(self.invariants),
|
|
475
|
+
"refinement_count": len(self.refinement_cycles),
|
|
476
|
+
"refinement_cycles": self.refinement_cycles,
|
|
477
|
+
"file_changes_count": len(self.file_changes),
|
|
478
|
+
"visual_mockups": self.visual_mockups,
|
|
479
|
+
"proof_receipts_count": len(self.proof_receipts),
|
|
480
|
+
"goal_rubrics_count": len(self.goal_rubrics),
|
|
481
|
+
"automation_pipelines_count": len(self.automation_pipelines),
|
|
482
|
+
"latest_goal_rubric": self.goal_rubrics[-1] if self.goal_rubrics else None,
|
|
483
|
+
"velocity_profile": GLOBAL_VELOCITY_PROFILER.get_velocity_profile(),
|
|
484
|
+
"cognitive_gates": self._gate_report(),
|
|
485
|
+
"unlock_details": self.unlock_details,
|
|
486
|
+
"system3_cognitive_state": {
|
|
487
|
+
"free_energy_f": self.active_free_energy.get("variational_free_energy_f", 1.25) if self.active_free_energy else 1.25,
|
|
488
|
+
"complexity_kl": self.active_free_energy.get("complexity_kl", 0.35) if self.active_free_energy else 0.35,
|
|
489
|
+
"accuracy_log_likelihood": self.active_free_energy.get("accuracy_log_likelihood", -0.90) if self.active_free_energy else -0.90,
|
|
490
|
+
"kripke_safety_invariant": "AG(safe) -> True" if (self.active_kripke_safety.get("is_satisfied", True) if self.active_kripke_safety else True) else "AG(safe) -> VIOLATED",
|
|
491
|
+
"kripke_safety_verified": self.active_kripke_safety.get("is_satisfied", True) if self.active_kripke_safety else True,
|
|
492
|
+
"active_biases_count": len(self.active_biases),
|
|
493
|
+
"active_biases": self.active_biases,
|
|
494
|
+
"contradiction_density": round(sum(len(s.get("resolved_contradictions", [])) for s in self.system3_syntheses) / max(1, len(self.system3_syntheses)), 2) if self.system3_syntheses else 0.0,
|
|
495
|
+
"hyperbolic_metric": {
|
|
496
|
+
"embeddings_count": len(self.system3_hyperbolic_embeddings),
|
|
497
|
+
"curvature": 1.0,
|
|
498
|
+
"status": "CONVERGED_POINCARE_BALL" if self.system3_hyperbolic_embeddings else "INITIALIZED",
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
"system3_counts": {
|
|
502
|
+
"causal_graphs": len(self.system3_causal_graphs),
|
|
503
|
+
"syntheses": len(self.system3_syntheses),
|
|
504
|
+
"gene_pools": len(self.system3_gene_pools),
|
|
505
|
+
"axioms": len(self.system3_axioms),
|
|
506
|
+
"reflections": len(self.system3_reflections),
|
|
507
|
+
"orchestrations": len(self.system3_orchestrations),
|
|
508
|
+
"hyperbolic_embeddings": len(self.system3_hyperbolic_embeddings),
|
|
509
|
+
"kripke_verifications": len(self.system3_kripke_verifications),
|
|
510
|
+
"active_inferences": len(self.system3_active_inferences),
|
|
511
|
+
"proof_oracle_verifications": len(self.system3_proof_oracle_verifications),
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
@staticmethod
|
|
516
|
+
def _format_duration(seconds: float) -> str:
|
|
517
|
+
sec = int(abs(seconds))
|
|
518
|
+
hours = sec // 3600
|
|
519
|
+
mins = (sec % 3600) // 60
|
|
520
|
+
s = sec % 60
|
|
521
|
+
if hours > 0:
|
|
522
|
+
return f"{hours}h {mins}m {s}s"
|
|
523
|
+
elif mins > 0:
|
|
524
|
+
return f"{mins}m {s}s"
|
|
525
|
+
else:
|
|
526
|
+
return f"{s}s"
|
|
527
|
+
|
|
528
|
+
def advance_phase(self, next_phase: str, phase_summary: str) -> Dict[str, Any]:
|
|
529
|
+
"""Advances the session to the requested phase and records history."""
|
|
530
|
+
if any(term in next_phase.upper() for term in ("SEALED", "EVOLVED")):
|
|
531
|
+
raise ValueError(
|
|
532
|
+
"Invalid phase transition: Cannot advance directly to SEALED or EVOLVED via advance_phase."
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
matched_phase = None
|
|
536
|
+
for p in PHASES:
|
|
537
|
+
if next_phase.strip().lower() == p.lower() or next_phase.strip().lower() in p.lower():
|
|
538
|
+
matched_phase = p
|
|
539
|
+
break
|
|
540
|
+
|
|
541
|
+
if not matched_phase:
|
|
542
|
+
valid_list = "\n".join([f"- {p}" for p in PHASES])
|
|
543
|
+
raise ValueError(
|
|
544
|
+
f"Invalid phase '{next_phase}'. Must be one of:\n{valid_list}"
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
current_phase_idx = PHASE_INDEX_MAP.get(self.active_phase, 1)
|
|
548
|
+
target_phase_idx = PHASE_INDEX_MAP[matched_phase]
|
|
549
|
+
if target_phase_idx != current_phase_idx + 1:
|
|
550
|
+
raise ValueError(
|
|
551
|
+
f"Invalid phase transition: move one phase at a time from "
|
|
552
|
+
f"Phase {current_phase_idx} to Phase {current_phase_idx + 1}."
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
if target_phase_idx >= 4 and self.current_state in (SessionState.INIT, SessionState.DEEPTHINK_TIMELOCK):
|
|
556
|
+
raise ValueError(
|
|
557
|
+
f"Cannot advance to {matched_phase}: Implementation gate not reached (current state: {self.current_state.value}). Execution must be unlocked."
|
|
558
|
+
)
|
|
559
|
+
if target_phase_idx >= 5 and self.current_state == SessionState.IMPLEMENTATION:
|
|
560
|
+
raise ValueError(
|
|
561
|
+
f"Cannot advance to {matched_phase}: Red team gate and arbitration have not been completed (current state: {self.current_state.value}). Skipping red team gates is strictly prohibited."
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
now = self._wall_clock()
|
|
565
|
+
self.active_phase = matched_phase
|
|
566
|
+
self.phase_history.append({
|
|
567
|
+
"phase": matched_phase,
|
|
568
|
+
"entered_at": now,
|
|
569
|
+
"summary": phase_summary
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
# Run System 3 Executive bias detection & reflection
|
|
573
|
+
from fable_v2.system3 import (
|
|
574
|
+
CognitiveBiasDetector,
|
|
575
|
+
ActiveInferenceEngine,
|
|
576
|
+
Policy,
|
|
577
|
+
create_default_architecture_pomdp,
|
|
578
|
+
KripkeStructure,
|
|
579
|
+
KripkeModelChecker,
|
|
580
|
+
)
|
|
581
|
+
detector = CognitiveBiasDetector()
|
|
582
|
+
findings = detector.audit_session({
|
|
583
|
+
"epistemic_ledger": self.epistemic_ledger,
|
|
584
|
+
"refinement_cycles": self.refinement_cycles,
|
|
585
|
+
"phase_history": self.phase_history,
|
|
586
|
+
"invariants": self.invariants,
|
|
587
|
+
})
|
|
588
|
+
self.active_biases = [f.to_dict() for f in findings]
|
|
589
|
+
if findings:
|
|
590
|
+
self.system3_reflections.append({
|
|
591
|
+
"phase": matched_phase,
|
|
592
|
+
"findings": self.active_biases,
|
|
593
|
+
"timestamp": now,
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
# Update live Free Energy F
|
|
597
|
+
pomdp_model = create_default_architecture_pomdp()
|
|
598
|
+
fe_engine = ActiveInferenceEngine(pomdp_model)
|
|
599
|
+
proven_count = sum(1 for i in self.epistemic_ledger if i.get("tag") == "PROVEN")
|
|
600
|
+
obs = "HIGH_THROUGHPUT_CLEAN" if proven_count >= 2 else "LOCK_CONTENTION_WARN"
|
|
601
|
+
fe_policies = [Policy(policy_id=f"p_{act}", actions=[act]) for act in pomdp_model.actions]
|
|
602
|
+
fe_report = fe_engine.select_action(obs, fe_policies)
|
|
603
|
+
self.active_free_energy = {
|
|
604
|
+
"variational_free_energy_f": round(fe_report.variational_free_energy_f, 4),
|
|
605
|
+
"complexity_kl": round(fe_report.complexity_kl, 4),
|
|
606
|
+
"accuracy_log_likelihood": round(fe_report.accuracy_log_likelihood, 4),
|
|
607
|
+
"selected_action": fe_report.selected_action,
|
|
608
|
+
"observation": obs,
|
|
609
|
+
"phase": matched_phase,
|
|
610
|
+
"timestamp": now,
|
|
611
|
+
}
|
|
612
|
+
self.system3_active_inferences.append(self.active_free_energy)
|
|
613
|
+
|
|
614
|
+
# Update Kripke Safety Invariant
|
|
615
|
+
kripke = KripkeStructure()
|
|
616
|
+
kripke.add_world("w0", propositions={"entered", "safe"})
|
|
617
|
+
kripke.add_world("w_phase", propositions={f"phase_{target_phase_idx}", "safe"})
|
|
618
|
+
kripke.add_transition("w0", "w_phase")
|
|
619
|
+
kripke.add_transition("w_phase", "w_phase")
|
|
620
|
+
checker = KripkeModelChecker(kripke)
|
|
621
|
+
k_res = checker.check("AG(safe)", "w0")
|
|
622
|
+
self.active_kripke_safety = {
|
|
623
|
+
"formula": "AG(safe)",
|
|
624
|
+
"is_satisfied": k_res.is_satisfied,
|
|
625
|
+
"active_phase": matched_phase,
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return self.get_telemetry()
|
|
629
|
+
|
|
630
|
+
def log_epistemic_item(self, tag: str, claim: str, evidence: Optional[str] = None) -> Dict[str, Any]:
|
|
631
|
+
"""Logs an epistemic fact/hypothesis/unknown with structured tracking and evidence validation."""
|
|
632
|
+
tag_upper = tag.strip().upper()
|
|
633
|
+
if tag_upper not in ("PROVEN", "HYPOTHESIS", "UNKNOWN"):
|
|
634
|
+
raise ValueError(f"Invalid epistemic tag '{tag}'. Must be 'PROVEN', 'HYPOTHESIS', or 'UNKNOWN'.")
|
|
635
|
+
|
|
636
|
+
if not claim or not claim.strip():
|
|
637
|
+
raise ValueError("Claim description cannot be empty.")
|
|
638
|
+
|
|
639
|
+
proof_receipt = None
|
|
640
|
+
from fable_v2.proof_engine import DeterministicProofValidator
|
|
641
|
+
validator = DeterministicProofValidator()
|
|
642
|
+
if tag_upper == "PROVEN":
|
|
643
|
+
if validator.is_tautological(claim):
|
|
644
|
+
raise ValueError(
|
|
645
|
+
f"Invalid claim '{claim}': PROVEN claims must not be tautological or generic (e.g. 'tested', 'it works'). "
|
|
646
|
+
f"State a substantive, testable system property or measurement."
|
|
647
|
+
)
|
|
648
|
+
if not str(evidence or "").strip():
|
|
649
|
+
raise ValueError("PROVEN claims require concrete evidence (file, command output, test, or URL).")
|
|
650
|
+
valid, reason, rcpt = validator.validate_proven_claim(claim, str(evidence))
|
|
651
|
+
if not valid:
|
|
652
|
+
raise ValueError(f"Epistemic Evidence Validation Failed: {reason}")
|
|
653
|
+
proof_receipt = rcpt
|
|
654
|
+
if rcpt:
|
|
655
|
+
self.proof_receipts.append(rcpt)
|
|
656
|
+
|
|
657
|
+
item_id = f"epi_{len(self.epistemic_ledger) + 1:03d}"
|
|
658
|
+
item = {
|
|
659
|
+
"id": item_id,
|
|
660
|
+
"tag": tag_upper,
|
|
661
|
+
"claim": claim.strip(),
|
|
662
|
+
"evidence": (evidence or "").strip(),
|
|
663
|
+
"proof_receipt": proof_receipt,
|
|
664
|
+
"timestamp": self._wall_clock(),
|
|
665
|
+
"phase": self.active_phase
|
|
666
|
+
}
|
|
667
|
+
self.epistemic_ledger.append(item)
|
|
668
|
+
return item
|
|
669
|
+
|
|
670
|
+
def record_invariant(
|
|
671
|
+
self,
|
|
672
|
+
invariant_name: str,
|
|
673
|
+
formal_statement: str,
|
|
674
|
+
proof_or_rationale: str,
|
|
675
|
+
domain: str = "architecture"
|
|
676
|
+
) -> Dict[str, Any]:
|
|
677
|
+
"""Records a domain invariant with formal statement and proof."""
|
|
678
|
+
dom_clean = domain.strip().lower()
|
|
679
|
+
if dom_clean not in ("architecture", "design", "coding"):
|
|
680
|
+
dom_clean = "architecture"
|
|
681
|
+
|
|
682
|
+
from fable_v2.proof_engine import DeterministicProofValidator
|
|
683
|
+
validator = DeterministicProofValidator()
|
|
684
|
+
valid, reason, rcpt = validator.validate_invariant(invariant_name, formal_statement, proof_or_rationale)
|
|
685
|
+
if not valid:
|
|
686
|
+
raise ValueError(f"Invariant Validation Failed: {reason}")
|
|
687
|
+
|
|
688
|
+
inv_id = f"inv_{len(self.invariants) + 1:03d}"
|
|
689
|
+
inv = {
|
|
690
|
+
"id": inv_id,
|
|
691
|
+
"name": invariant_name.strip(),
|
|
692
|
+
"domain": dom_clean,
|
|
693
|
+
"formal_statement": formal_statement.strip(),
|
|
694
|
+
"proof_or_rationale": (proof_or_rationale or "").strip(),
|
|
695
|
+
"proof_receipt": rcpt,
|
|
696
|
+
"timestamp": self._wall_clock(),
|
|
697
|
+
"phase": self.active_phase
|
|
698
|
+
}
|
|
699
|
+
if rcpt:
|
|
700
|
+
self.proof_receipts.append(rcpt)
|
|
701
|
+
self.invariants.append(inv)
|
|
702
|
+
return inv
|
|
703
|
+
|
|
704
|
+
def track_file_change(
|
|
705
|
+
self,
|
|
706
|
+
file_path: str,
|
|
707
|
+
change_type: str,
|
|
708
|
+
diff_summary: str,
|
|
709
|
+
rationale: Optional[str] = None,
|
|
710
|
+
affected_invariants: Optional[Union[List[str], str]] = None
|
|
711
|
+
) -> Dict[str, Any]:
|
|
712
|
+
"""Tracks file mutations (modified, created, deleted, slated) with automatic SHA256 hashing."""
|
|
713
|
+
if not file_path or not str(file_path).strip():
|
|
714
|
+
raise ValueError("file_path cannot be empty.")
|
|
715
|
+
c_type = str(change_type).strip().lower()
|
|
716
|
+
if c_type not in ("modified", "created", "deleted", "slated"):
|
|
717
|
+
raise ValueError(f"Invalid change_type '{change_type}'. Must be 'modified', 'created', 'deleted', or 'slated'.")
|
|
718
|
+
if not diff_summary or not str(diff_summary).strip():
|
|
719
|
+
raise ValueError("diff_summary cannot be empty.")
|
|
720
|
+
|
|
721
|
+
p = Path(file_path)
|
|
722
|
+
if not p.is_absolute():
|
|
723
|
+
p = Path.cwd() / p
|
|
724
|
+
sha256 = None
|
|
725
|
+
if p.is_file():
|
|
726
|
+
try:
|
|
727
|
+
sha256 = hashlib.sha256(p.read_bytes()).hexdigest()
|
|
728
|
+
except Exception:
|
|
729
|
+
sha256 = None
|
|
730
|
+
|
|
731
|
+
invariants_list: List[str] = []
|
|
732
|
+
if affected_invariants:
|
|
733
|
+
if isinstance(affected_invariants, list):
|
|
734
|
+
invariants_list = [str(x) for x in affected_invariants]
|
|
735
|
+
else:
|
|
736
|
+
invariants_list = [str(affected_invariants)]
|
|
737
|
+
|
|
738
|
+
entry = {
|
|
739
|
+
"file_path": str(file_path).strip(),
|
|
740
|
+
"change_type": c_type,
|
|
741
|
+
"diff_summary": str(diff_summary).strip(),
|
|
742
|
+
"rationale": (rationale or "").strip(),
|
|
743
|
+
"affected_invariants": invariants_list,
|
|
744
|
+
"sha256": sha256,
|
|
745
|
+
"timestamp": self._wall_clock(),
|
|
746
|
+
"phase": self.active_phase,
|
|
747
|
+
}
|
|
748
|
+
self.file_changes.append(entry)
|
|
749
|
+
return entry
|
|
750
|
+
|
|
751
|
+
def record_visual_mockups(
|
|
752
|
+
self,
|
|
753
|
+
mockups: Union[List[Dict[str, Any]], str],
|
|
754
|
+
selected_concept: Optional[str] = None
|
|
755
|
+
) -> Dict[str, Any]:
|
|
756
|
+
"""Records visual mockup concepts, palettes, typography, and layout coordinate data."""
|
|
757
|
+
if isinstance(mockups, str):
|
|
758
|
+
try:
|
|
759
|
+
parsed_mockups = json.loads(mockups)
|
|
760
|
+
except Exception as e:
|
|
761
|
+
raise ValueError(f"Failed to parse mockups JSON string: {e}")
|
|
762
|
+
elif isinstance(mockups, list):
|
|
763
|
+
parsed_mockups = mockups
|
|
764
|
+
else:
|
|
765
|
+
raise ValueError(f"mockups must be a list of concept dictionaries, got {type(mockups).__name__}")
|
|
766
|
+
|
|
767
|
+
if not isinstance(parsed_mockups, list) or len(parsed_mockups) == 0:
|
|
768
|
+
raise ValueError("mockups list cannot be empty. Provide 5-6 architectural concept mockups.")
|
|
769
|
+
|
|
770
|
+
self.visual_mockups = {
|
|
771
|
+
"mockups": parsed_mockups,
|
|
772
|
+
"selected_concept": (selected_concept or "").strip() if selected_concept else (parsed_mockups[0].get("concept_name") if parsed_mockups else None),
|
|
773
|
+
"recorded_at": self._wall_clock(),
|
|
774
|
+
"phase": self.active_phase,
|
|
775
|
+
}
|
|
776
|
+
return self.visual_mockups
|
|
777
|
+
|
|
778
|
+
def set_goal_rubric(
|
|
779
|
+
self,
|
|
780
|
+
task_objective: str,
|
|
781
|
+
criteria: Union[List[Dict[str, Any]], str],
|
|
782
|
+
target_score: float = 0.95,
|
|
783
|
+
rubric_id: Optional[str] = None,
|
|
784
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
785
|
+
) -> Dict[str, Any]:
|
|
786
|
+
"""Registers a goal scoring rubric contract with target score threshold (default >= 0.95)."""
|
|
787
|
+
if not task_objective or not str(task_objective).strip():
|
|
788
|
+
task_objective = self.objective or "Task Objective"
|
|
789
|
+
|
|
790
|
+
target_score_val = float(target_score)
|
|
791
|
+
if not 0.0 <= target_score_val <= 1.0:
|
|
792
|
+
raise ValueError(f"target_score must be between 0.0 and 1.0, got {target_score_val}")
|
|
793
|
+
|
|
794
|
+
parsed_items: List[Dict[str, Any]] = []
|
|
795
|
+
raw_items: Any = criteria
|
|
796
|
+
if isinstance(raw_items, str):
|
|
797
|
+
try:
|
|
798
|
+
raw_items = json.loads(raw_items)
|
|
799
|
+
except Exception:
|
|
800
|
+
lines = [l.strip() for l in raw_items.splitlines() if l.strip()]
|
|
801
|
+
raw_items = [{"pointer_id": f"PTR-{idx+1:02d}", "description": l} for idx, l in enumerate(lines)]
|
|
802
|
+
|
|
803
|
+
if isinstance(raw_items, dict):
|
|
804
|
+
dict_items = []
|
|
805
|
+
for k, v in raw_items.items():
|
|
806
|
+
if isinstance(v, dict):
|
|
807
|
+
item_d = dict(v)
|
|
808
|
+
item_d.setdefault("pointer_id", k)
|
|
809
|
+
dict_items.append(item_d)
|
|
810
|
+
else:
|
|
811
|
+
dict_items.append({"pointer_id": k, "description": str(v)})
|
|
812
|
+
raw_items = dict_items
|
|
813
|
+
|
|
814
|
+
if not isinstance(raw_items, list) or not raw_items:
|
|
815
|
+
raise ValueError("criteria must be a non-empty list of criteria items/pointers or JSON string.")
|
|
816
|
+
|
|
817
|
+
for idx, item in enumerate(raw_items):
|
|
818
|
+
if isinstance(item, dict):
|
|
819
|
+
p_id = str(item.get("pointer_id") or f"PTR-{idx+1:02d}").strip()
|
|
820
|
+
desc = str(item.get("description") or item.get("desc") or item.get("name") or p_id).strip()
|
|
821
|
+
weight = float(item.get("weight", 1.0))
|
|
822
|
+
verifier = str(item.get("verifier_command", "")).strip()
|
|
823
|
+
satisfied = bool(item.get("satisfied", False))
|
|
824
|
+
score = float(item.get("score", 1.0 if satisfied else 0.0))
|
|
825
|
+
receipt_id = str(item.get("evidence_receipt_id", "")).strip()
|
|
826
|
+
meta = dict(item.get("metadata") or {})
|
|
827
|
+
else:
|
|
828
|
+
p_id = f"PTR-{idx+1:02d}"
|
|
829
|
+
desc = str(item).strip()
|
|
830
|
+
weight = 1.0
|
|
831
|
+
verifier = ""
|
|
832
|
+
satisfied = False
|
|
833
|
+
score = 0.0
|
|
834
|
+
receipt_id = ""
|
|
835
|
+
meta = {}
|
|
836
|
+
|
|
837
|
+
evidence_type = str(
|
|
838
|
+
(item.get("evidence_type") if isinstance(item, dict) else "")
|
|
839
|
+
or meta.get("evidence_type")
|
|
840
|
+
or ("verifier" if verifier else "")
|
|
841
|
+
).strip()
|
|
842
|
+
evidence_phase = str(
|
|
843
|
+
(item.get("evidence_phase") if isinstance(item, dict) else "")
|
|
844
|
+
or meta.get("evidence_phase")
|
|
845
|
+
or (self.active_phase if verifier else "")
|
|
846
|
+
).strip()
|
|
847
|
+
parsed_item = {
|
|
848
|
+
"pointer_id": p_id,
|
|
849
|
+
"description": desc,
|
|
850
|
+
"weight": max(0.0, weight),
|
|
851
|
+
"verifier_command": verifier,
|
|
852
|
+
"satisfied": satisfied,
|
|
853
|
+
"score": max(0.0, min(1.0, score)),
|
|
854
|
+
"evidence_receipt_id": receipt_id,
|
|
855
|
+
"evidence_type": evidence_type,
|
|
856
|
+
"evidence_phase": evidence_phase,
|
|
857
|
+
"metadata": meta
|
|
858
|
+
}
|
|
859
|
+
self._annotate_rubric_item_trust(parsed_item)
|
|
860
|
+
parsed_items.append(parsed_item)
|
|
861
|
+
|
|
862
|
+
total_weight = sum(it["weight"] for it in parsed_items)
|
|
863
|
+
if total_weight > 0:
|
|
864
|
+
weighted_sum = sum(it["score"] * it["weight"] for it in parsed_items)
|
|
865
|
+
current_score = round(weighted_sum / total_weight, 4)
|
|
866
|
+
else:
|
|
867
|
+
current_score = 0.0
|
|
868
|
+
|
|
869
|
+
has_untrusted_items = any(
|
|
870
|
+
bool(it.get("satisfied"))
|
|
871
|
+
and (it.get("metadata") or {}).get("evidence_trust") == "untrusted"
|
|
872
|
+
for it in parsed_items
|
|
873
|
+
)
|
|
874
|
+
status = "achieved" if current_score >= target_score_val and not has_untrusted_items else "pending"
|
|
875
|
+
r_id = (rubric_id or f"rubric_{self.session_name}_{len(self.goal_rubrics) + 1}").strip()
|
|
876
|
+
|
|
877
|
+
rubric_entry = {
|
|
878
|
+
"rubric_id": r_id,
|
|
879
|
+
"session_id": self.session_id,
|
|
880
|
+
"task_objective": str(task_objective).strip(),
|
|
881
|
+
"target_score": target_score_val,
|
|
882
|
+
"items": parsed_items,
|
|
883
|
+
"current_score": current_score,
|
|
884
|
+
"status": status,
|
|
885
|
+
"metadata": metadata or {},
|
|
886
|
+
"created_phase": self.active_phase,
|
|
887
|
+
"created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(self._wall_clock()))
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
existing_idx = next((i for i, r in enumerate(self.goal_rubrics) if r.get("rubric_id") == r_id), None)
|
|
891
|
+
if existing_idx is not None:
|
|
892
|
+
self.goal_rubrics[existing_idx] = rubric_entry
|
|
893
|
+
else:
|
|
894
|
+
self.goal_rubrics.append(rubric_entry)
|
|
895
|
+
|
|
896
|
+
if self.current_state == SessionState.INIT:
|
|
897
|
+
self.transition_to(SessionState.DEEPTHINK_TIMELOCK, "Goal rubric registered")
|
|
898
|
+
|
|
899
|
+
return rubric_entry
|
|
900
|
+
|
|
901
|
+
def evaluate_goal_rubric(
|
|
902
|
+
self,
|
|
903
|
+
rubric_id: Optional[str] = None,
|
|
904
|
+
item_evaluations: Optional[Union[List[Dict[str, Any]], Dict[str, Any], str]] = None
|
|
905
|
+
) -> Dict[str, Any]:
|
|
906
|
+
"""Evaluates rubric criteria items, updates scores, and determines goal attainment status."""
|
|
907
|
+
if not self.goal_rubrics:
|
|
908
|
+
raise ValueError("No goal rubric registered in this session. Call set_goal_rubric first.")
|
|
909
|
+
|
|
910
|
+
rubric: Optional[Dict[str, Any]] = None
|
|
911
|
+
if rubric_id:
|
|
912
|
+
rubric = next((r for r in self.goal_rubrics if r.get("rubric_id") == str(rubric_id).strip()), None)
|
|
913
|
+
if not rubric:
|
|
914
|
+
raise ValueError(f"Rubric with id '{rubric_id}' not found.")
|
|
915
|
+
else:
|
|
916
|
+
rubric = self.goal_rubrics[-1]
|
|
917
|
+
|
|
918
|
+
evals_list: List[Dict[str, Any]] = []
|
|
919
|
+
if item_evaluations is not None:
|
|
920
|
+
raw_evals = item_evaluations
|
|
921
|
+
if isinstance(raw_evals, str):
|
|
922
|
+
try:
|
|
923
|
+
raw_evals = json.loads(raw_evals)
|
|
924
|
+
except Exception:
|
|
925
|
+
raw_evals = []
|
|
926
|
+
if isinstance(raw_evals, dict):
|
|
927
|
+
for k, v in raw_evals.items():
|
|
928
|
+
if isinstance(v, dict):
|
|
929
|
+
ed = dict(v)
|
|
930
|
+
ed.setdefault("pointer_id", k)
|
|
931
|
+
evals_list.append(ed)
|
|
932
|
+
elif isinstance(v, (int, float)):
|
|
933
|
+
evals_list.append({"pointer_id": k, "score": float(v), "satisfied": float(v) >= 1.0})
|
|
934
|
+
elif isinstance(v, bool):
|
|
935
|
+
evals_list.append({"pointer_id": k, "satisfied": v, "score": 1.0 if v else 0.0})
|
|
936
|
+
elif isinstance(raw_evals, list):
|
|
937
|
+
evals_list = [dict(x) if isinstance(x, dict) else {"pointer_id": str(x), "satisfied": True, "score": 1.0} for x in raw_evals]
|
|
938
|
+
|
|
939
|
+
candidate = copy.deepcopy(rubric)
|
|
940
|
+
for ev in evals_list:
|
|
941
|
+
p_id = str(ev.get("pointer_id", "")).strip()
|
|
942
|
+
for it in candidate["items"]:
|
|
943
|
+
if it.get("pointer_id") == p_id:
|
|
944
|
+
if "satisfied" in ev:
|
|
945
|
+
it["satisfied"] = bool(ev["satisfied"])
|
|
946
|
+
if "score" in ev:
|
|
947
|
+
it["score"] = max(0.0, min(1.0, float(ev["score"])))
|
|
948
|
+
elif "satisfied" in ev:
|
|
949
|
+
it["score"] = 1.0 if it["satisfied"] else 0.0
|
|
950
|
+
if "evidence_receipt_id" in ev:
|
|
951
|
+
it["evidence_receipt_id"] = str(ev["evidence_receipt_id"]).strip()
|
|
952
|
+
if "verifier_command" in ev:
|
|
953
|
+
it["verifier_command"] = str(ev["verifier_command"]).strip()
|
|
954
|
+
if it["verifier_command"] and not str(it.get("evidence_type", "")).strip():
|
|
955
|
+
it["evidence_type"] = "verifier"
|
|
956
|
+
if it["verifier_command"] and not str(it.get("evidence_phase", "")).strip():
|
|
957
|
+
it["evidence_phase"] = self.active_phase
|
|
958
|
+
if "evidence_type" in ev:
|
|
959
|
+
it["evidence_type"] = str(ev["evidence_type"]).strip()
|
|
960
|
+
if "evidence_phase" in ev:
|
|
961
|
+
it["evidence_phase"] = str(ev["evidence_phase"]).strip()
|
|
962
|
+
if "metadata" in ev and isinstance(ev["metadata"], dict):
|
|
963
|
+
it.setdefault("metadata", {}).update(ev["metadata"])
|
|
964
|
+
|
|
965
|
+
for item in candidate["items"]:
|
|
966
|
+
self._annotate_rubric_item_trust(item)
|
|
967
|
+
|
|
968
|
+
total_weight = sum(it.get("weight", 1.0) for it in candidate["items"])
|
|
969
|
+
if total_weight > 0:
|
|
970
|
+
weighted_sum = sum(
|
|
971
|
+
float(it.get("score", 1.0 if it.get("satisfied") else 0.0))
|
|
972
|
+
* float(it.get("weight", 1.0))
|
|
973
|
+
for it in candidate["items"]
|
|
974
|
+
)
|
|
975
|
+
current_score = round(weighted_sum / total_weight, 4)
|
|
976
|
+
else:
|
|
977
|
+
current_score = 0.0
|
|
978
|
+
|
|
979
|
+
candidate["current_score"] = current_score
|
|
980
|
+
target_score = float(candidate.get("target_score", 0.95))
|
|
981
|
+
has_untrusted_items = any(
|
|
982
|
+
bool(it.get("satisfied"))
|
|
983
|
+
and (it.get("metadata") or {}).get("evidence_trust") == "untrusted"
|
|
984
|
+
for it in candidate["items"]
|
|
985
|
+
)
|
|
986
|
+
candidate["status"] = (
|
|
987
|
+
"achieved" if current_score >= target_score and not has_untrusted_items else "in_progress"
|
|
988
|
+
)
|
|
989
|
+
candidate["last_evaluated_at"] = time.strftime(
|
|
990
|
+
"%Y-%m-%dT%H:%M:%SZ", time.gmtime(self._wall_clock())
|
|
991
|
+
)
|
|
992
|
+
rubric.clear()
|
|
993
|
+
rubric.update(candidate)
|
|
994
|
+
return rubric
|
|
995
|
+
|
|
996
|
+
def get_goal_rubric(self, rubric_id: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
|
997
|
+
"""Returns the specified goal rubric or the latest active rubric."""
|
|
998
|
+
if not self.goal_rubrics:
|
|
999
|
+
return None
|
|
1000
|
+
if rubric_id:
|
|
1001
|
+
return next((r for r in self.goal_rubrics if r.get("rubric_id") == str(rubric_id).strip()), None)
|
|
1002
|
+
return self.goal_rubrics[-1]
|
|
1003
|
+
|
|
1004
|
+
def register_automation_pipeline(
|
|
1005
|
+
self,
|
|
1006
|
+
name: str,
|
|
1007
|
+
pipeline_type: str = "closed_loop",
|
|
1008
|
+
generator_command: str = "",
|
|
1009
|
+
evaluator_command: str = "",
|
|
1010
|
+
target_threshold: float = 0.95,
|
|
1011
|
+
max_iterations: int = 10,
|
|
1012
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
1013
|
+
) -> Dict[str, Any]:
|
|
1014
|
+
"""Registers an autonomous pipeline loop spec (generate -> evaluate -> iterate)."""
|
|
1015
|
+
if not name or not str(name).strip():
|
|
1016
|
+
raise ValueError("Pipeline 'name' cannot be empty.")
|
|
1017
|
+
|
|
1018
|
+
pipe_id = f"pipeline_{self.session_name}_{len(self.automation_pipelines) + 1}"
|
|
1019
|
+
spec = {
|
|
1020
|
+
"pipeline_id": pipe_id,
|
|
1021
|
+
"session_id": self.session_id,
|
|
1022
|
+
"name": str(name).strip(),
|
|
1023
|
+
"pipeline_type": (pipeline_type or "closed_loop").strip(),
|
|
1024
|
+
"generator_command": (generator_command or "").strip(),
|
|
1025
|
+
"evaluator_command": (evaluator_command or "").strip(),
|
|
1026
|
+
"target_threshold": max(0.0, min(1.0, float(target_threshold))),
|
|
1027
|
+
"max_iterations": max(1, int(max_iterations)),
|
|
1028
|
+
"status": "active",
|
|
1029
|
+
"created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(self._wall_clock())),
|
|
1030
|
+
"metadata": metadata or {}
|
|
1031
|
+
}
|
|
1032
|
+
self.automation_pipelines.append(spec)
|
|
1033
|
+
return spec
|
|
1034
|
+
|
|
1035
|
+
@staticmethod
|
|
1036
|
+
def _canonical_receipt_payload(value: Dict[str, Any]) -> bytes:
|
|
1037
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8")
|
|
1038
|
+
|
|
1039
|
+
def _attack_vector_results(self, report_data: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
|
|
1040
|
+
supplied = report_data.get("attack_vector_results")
|
|
1041
|
+
if isinstance(supplied, dict):
|
|
1042
|
+
return {str(key): dict(value) for key, value in supplied.items() if isinstance(value, dict)}
|
|
1043
|
+
results = {vector: {"passed": True, "finding_count": 0} for vector in RED_TEAM_ATTACK_VECTORS}
|
|
1044
|
+
for finding in report_data.get("findings", []):
|
|
1045
|
+
vector = finding.get("vector", "") if isinstance(finding, dict) else getattr(finding, "vector", "")
|
|
1046
|
+
vector = getattr(vector, "value", vector)
|
|
1047
|
+
if vector in results:
|
|
1048
|
+
results[vector]["finding_count"] += 1
|
|
1049
|
+
if bool(finding.get("broken", False) if isinstance(finding, dict) else getattr(finding, "broken", False)):
|
|
1050
|
+
results[vector]["passed"] = False
|
|
1051
|
+
return results
|
|
1052
|
+
|
|
1053
|
+
def issue_red_team_receipt(
|
|
1054
|
+
self,
|
|
1055
|
+
report_data: Dict[str, Any],
|
|
1056
|
+
reviewed_change_id: str,
|
|
1057
|
+
origin: str = "red_team_swarm",
|
|
1058
|
+
) -> Dict[str, Any]:
|
|
1059
|
+
"""Issue a process-local attestation for a complete, clean Red-Team report."""
|
|
1060
|
+
if int(report_data.get("broken_count", 0)) != 0:
|
|
1061
|
+
raise ValueError("Only a zero-breakage report can receive a red-team receipt.")
|
|
1062
|
+
change_id = str(reviewed_change_id or "").strip()
|
|
1063
|
+
if not change_id or len(change_id) > 256:
|
|
1064
|
+
raise ValueError("A bounded immutable reviewed_change_id is required.")
|
|
1065
|
+
vectors = self._attack_vector_results(report_data)
|
|
1066
|
+
if set(vectors) != set(RED_TEAM_ATTACK_VECTORS) or any(
|
|
1067
|
+
result.get("passed") is not True for result in vectors.values()
|
|
1068
|
+
):
|
|
1069
|
+
raise ValueError("A clean receipt requires passing results for all five attack vectors.")
|
|
1070
|
+
issued_at = float(self._wall_clock())
|
|
1071
|
+
report_body = {
|
|
1072
|
+
key: value
|
|
1073
|
+
for key, value in report_data.items()
|
|
1074
|
+
if key not in ("red_team_receipt", "receipt", "attestation")
|
|
1075
|
+
}
|
|
1076
|
+
receipt = {
|
|
1077
|
+
"receipt_type": "red_team_remediation",
|
|
1078
|
+
"receipt_id": f"rt_rcpt_{secrets.token_hex(12)}",
|
|
1079
|
+
"session_id": self.session_id,
|
|
1080
|
+
"report_id": str(report_data.get("report_id", "")).strip(),
|
|
1081
|
+
"report_origin": str(origin).strip(),
|
|
1082
|
+
"reviewed_change_id": change_id,
|
|
1083
|
+
"attack_vector_results": vectors,
|
|
1084
|
+
"issued_at": issued_at,
|
|
1085
|
+
"expires_at": issued_at + MAX_REMEDIATION_SECONDS,
|
|
1086
|
+
"report_digest": hashlib.sha256(self._canonical_receipt_payload(report_body)).hexdigest(),
|
|
1087
|
+
}
|
|
1088
|
+
receipt["signature"] = hmac.new(
|
|
1089
|
+
self._red_team_receipt_key,
|
|
1090
|
+
self._canonical_receipt_payload(receipt),
|
|
1091
|
+
hashlib.sha256,
|
|
1092
|
+
).hexdigest()
|
|
1093
|
+
return receipt
|
|
1094
|
+
|
|
1095
|
+
def _validate_red_team_receipt(self, report_data: Dict[str, Any]) -> None:
|
|
1096
|
+
receipt = report_data.get("red_team_receipt")
|
|
1097
|
+
if not isinstance(receipt, dict):
|
|
1098
|
+
raise ValueError("Zero-breakage reports require an authenticated red-team receipt.")
|
|
1099
|
+
required = {
|
|
1100
|
+
"receipt_type", "receipt_id", "session_id", "report_id", "report_origin",
|
|
1101
|
+
"reviewed_change_id", "attack_vector_results", "issued_at", "expires_at",
|
|
1102
|
+
"report_digest", "signature",
|
|
1103
|
+
}
|
|
1104
|
+
if not required.issubset(receipt):
|
|
1105
|
+
raise ValueError("Red-team receipt is incomplete.")
|
|
1106
|
+
if receipt.get("receipt_type") != "red_team_remediation" or receipt.get("session_id") != self.session_id:
|
|
1107
|
+
raise ValueError("Red-team receipt is not bound to this session.")
|
|
1108
|
+
if receipt.get("report_id") != str(report_data.get("report_id", "")).strip():
|
|
1109
|
+
raise ValueError("Red-team receipt is not bound to this report.")
|
|
1110
|
+
if receipt.get("report_origin") != "red_team_swarm" or report_data.get("report_origin") != "red_team_swarm":
|
|
1111
|
+
raise ValueError("Report origin is not an attested Red-Team origin.")
|
|
1112
|
+
now = float(self._wall_clock())
|
|
1113
|
+
issued_at = float(receipt["issued_at"])
|
|
1114
|
+
expires_at = float(receipt["expires_at"])
|
|
1115
|
+
if issued_at > now or expires_at < now or expires_at - issued_at > MAX_REMEDIATION_SECONDS:
|
|
1116
|
+
raise ValueError("Red-team receipt has invalid validity bounds.")
|
|
1117
|
+
vectors = receipt.get("attack_vector_results")
|
|
1118
|
+
if not isinstance(vectors, dict) or set(vectors) != set(RED_TEAM_ATTACK_VECTORS):
|
|
1119
|
+
raise ValueError("Red-team receipt must contain all five attack-vector results.")
|
|
1120
|
+
if any(not isinstance(value, dict) or value.get("passed") is not True for value in vectors.values()):
|
|
1121
|
+
raise ValueError("Red-team receipt does not attest five passing attack vectors.")
|
|
1122
|
+
if self._attack_vector_results(report_data) != vectors:
|
|
1123
|
+
raise ValueError("Red-team receipt attack-vector results do not match the report.")
|
|
1124
|
+
unsigned = {key: value for key, value in receipt.items() if key != "signature"}
|
|
1125
|
+
expected_signature = hmac.new(
|
|
1126
|
+
self._red_team_receipt_key,
|
|
1127
|
+
self._canonical_receipt_payload(unsigned),
|
|
1128
|
+
hashlib.sha256,
|
|
1129
|
+
).hexdigest()
|
|
1130
|
+
if not hmac.compare_digest(expected_signature, str(receipt.get("signature", ""))):
|
|
1131
|
+
raise ValueError("Red-team receipt authentication failed.")
|
|
1132
|
+
report_body = {
|
|
1133
|
+
key: value
|
|
1134
|
+
for key, value in report_data.items()
|
|
1135
|
+
if key not in ("red_team_receipt", "receipt", "attestation")
|
|
1136
|
+
}
|
|
1137
|
+
expected_digest = hashlib.sha256(self._canonical_receipt_payload(report_body)).hexdigest()
|
|
1138
|
+
if not hmac.compare_digest(expected_digest, str(receipt.get("report_digest", ""))):
|
|
1139
|
+
raise ValueError("Red-team receipt report binding failed.")
|
|
1140
|
+
if str(report_data.get("reviewed_change_id", "")).strip() != str(receipt["reviewed_change_id"]).strip():
|
|
1141
|
+
raise ValueError("Red-team receipt reviewed-change binding failed.")
|
|
1142
|
+
|
|
1143
|
+
def _fresh_stage_records(
|
|
1144
|
+
self,
|
|
1145
|
+
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]], List[Dict[str, Any]], List[Dict[str, Any]]]:
|
|
1146
|
+
return (
|
|
1147
|
+
[item for item in self.epistemic_ledger if not item.get("_restored_untrusted")],
|
|
1148
|
+
[item for item in self.refinement_cycles if not item.get("_restored_untrusted")],
|
|
1149
|
+
[item for item in self.file_changes if not item.get("_restored_untrusted")],
|
|
1150
|
+
[item for item in self.goal_rubrics if not item.get("_restored_untrusted")],
|
|
1151
|
+
)
|
|
1152
|
+
|
|
1153
|
+
def derive_reviewed_change_id(self) -> str:
|
|
1154
|
+
file_changes = self._fresh_stage_records()[2]
|
|
1155
|
+
if not file_changes:
|
|
1156
|
+
return ""
|
|
1157
|
+
payload = json.dumps(file_changes, sort_keys=True, separators=(",", ":"))
|
|
1158
|
+
return "chg_" + hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
1159
|
+
|
|
1160
|
+
def _validate_satisfied_rubric_item(self, item: Dict[str, Any]) -> None:
|
|
1161
|
+
if not item.get("satisfied"):
|
|
1162
|
+
return
|
|
1163
|
+
receipt_id = str(item.get("evidence_receipt_id", "")).strip()
|
|
1164
|
+
|
|
1165
|
+
def is_validated_receipt(receipt: Any) -> bool:
|
|
1166
|
+
if (
|
|
1167
|
+
not isinstance(receipt, dict)
|
|
1168
|
+
or receipt.get("_restored_untrusted")
|
|
1169
|
+
or str(receipt.get("receipt_id", "")) != receipt_id
|
|
1170
|
+
):
|
|
1171
|
+
return False
|
|
1172
|
+
if "verified" in receipt or "passed" in receipt:
|
|
1173
|
+
return receipt.get("verified") is True or receipt.get("passed") is True
|
|
1174
|
+
proof_fields = {
|
|
1175
|
+
"claim", "proof_type", "target_resource", "sha256_digest",
|
|
1176
|
+
"verified_at", "verifier_details",
|
|
1177
|
+
}
|
|
1178
|
+
return proof_fields.issubset(receipt)
|
|
1179
|
+
|
|
1180
|
+
valid_receipt = bool(receipt_id) and any(
|
|
1181
|
+
is_validated_receipt(receipt)
|
|
1182
|
+
for receipt in self.proof_receipts
|
|
1183
|
+
)
|
|
1184
|
+
if not valid_receipt:
|
|
1185
|
+
raise ValueError(
|
|
1186
|
+
"Satisfied rubric criteria require a validated evidence receipt; "
|
|
1187
|
+
"a verifier command alone is not execution evidence."
|
|
1188
|
+
)
|
|
1189
|
+
|
|
1190
|
+
def _annotate_rubric_item_trust(self, item: Dict[str, Any]) -> bool:
|
|
1191
|
+
metadata = item.get("metadata") if isinstance(item.get("metadata"), dict) else {}
|
|
1192
|
+
item["metadata"] = metadata
|
|
1193
|
+
metadata.pop("evidence_trust", None)
|
|
1194
|
+
if not item.get("satisfied"):
|
|
1195
|
+
return True
|
|
1196
|
+
try:
|
|
1197
|
+
self._validate_satisfied_rubric_item(item)
|
|
1198
|
+
except ValueError:
|
|
1199
|
+
metadata["evidence_trust"] = "untrusted"
|
|
1200
|
+
return False
|
|
1201
|
+
metadata["evidence_trust"] = "validated"
|
|
1202
|
+
return True
|
|
1203
|
+
|
|
1204
|
+
def _validate_clean_stage_evidence(self) -> None:
|
|
1205
|
+
epistemic, refinements, files, rubrics = self._fresh_stage_records()
|
|
1206
|
+
proven = [
|
|
1207
|
+
item for item in epistemic
|
|
1208
|
+
if item.get("tag") == "PROVEN" and str(item.get("evidence", "")).strip()
|
|
1209
|
+
]
|
|
1210
|
+
if len(proven) < 2 or not refinements or not files:
|
|
1211
|
+
raise ValueError("Clean report lacks current-process epistemic, refinement, and file-change evidence.")
|
|
1212
|
+
for rubric in rubrics:
|
|
1213
|
+
target_score = float(rubric.get("target_score", 0.95))
|
|
1214
|
+
if target_score <= 0:
|
|
1215
|
+
continue
|
|
1216
|
+
if rubric.get("status") == "achieved" or float(rubric.get("current_score", 0.0)) >= target_score:
|
|
1217
|
+
satisfied_items = [item for item in rubric.get("items", []) if item.get("satisfied")]
|
|
1218
|
+
if not satisfied_items:
|
|
1219
|
+
continue
|
|
1220
|
+
for item in satisfied_items:
|
|
1221
|
+
self._validate_satisfied_rubric_item(item)
|
|
1222
|
+
return
|
|
1223
|
+
raise ValueError("Clean report lacks an achieved rubric with validated criteria evidence.")
|
|
1224
|
+
|
|
1225
|
+
def _validate_breakage_report(self, report_data: Dict[str, Any]) -> int:
|
|
1226
|
+
if not isinstance(report_data, dict):
|
|
1227
|
+
raise ValueError("Breakage report must be a dictionary.")
|
|
1228
|
+
findings = report_data.get("findings", [])
|
|
1229
|
+
if not isinstance(findings, list):
|
|
1230
|
+
raise ValueError("Breakage report findings must be a list.")
|
|
1231
|
+
broken_count = int(report_data.get("broken_count", 0))
|
|
1232
|
+
total_probes = int(report_data.get("total_probes", len(findings)))
|
|
1233
|
+
actual_broken = sum(
|
|
1234
|
+
1 for finding in findings
|
|
1235
|
+
if bool(finding.get("broken", True) if isinstance(finding, dict) else getattr(finding, "broken", True))
|
|
1236
|
+
)
|
|
1237
|
+
if broken_count < 0 or total_probes < len(findings) or broken_count != actual_broken:
|
|
1238
|
+
raise ValueError("Breakage report counts do not match its findings.")
|
|
1239
|
+
if bool(report_data.get("passed", broken_count == 0)) != (broken_count == 0):
|
|
1240
|
+
raise ValueError("Breakage report passed flag does not match broken_count.")
|
|
1241
|
+
if broken_count == 0:
|
|
1242
|
+
self._validate_red_team_receipt(report_data)
|
|
1243
|
+
self._validate_clean_stage_evidence()
|
|
1244
|
+
return broken_count
|
|
1245
|
+
|
|
1246
|
+
def record_breakage_report(self, report_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
1247
|
+
"""Validate and record a report, enforcing durable remediation bounds."""
|
|
1248
|
+
if self.current_state in (
|
|
1249
|
+
SessionState.INIT,
|
|
1250
|
+
SessionState.DEEPTHINK_TIMELOCK,
|
|
1251
|
+
SessionState.SEALED,
|
|
1252
|
+
SessionState.EVOLVED,
|
|
1253
|
+
):
|
|
1254
|
+
raise ValueError(f"Breakage report cannot be recorded from {self.current_state.value}.")
|
|
1255
|
+
if self.current_state == SessionState.ESCALATION_UNRESOLVED_BREAKAGES:
|
|
1256
|
+
raise ValueError("Session is already escalated for unresolved breakages.")
|
|
1257
|
+
|
|
1258
|
+
broken_count = self._validate_breakage_report(report_data)
|
|
1259
|
+
now = self._wall_clock()
|
|
1260
|
+
if broken_count == 0:
|
|
1261
|
+
if self.current_state not in (
|
|
1262
|
+
SessionState.RED_TEAM_GATE,
|
|
1263
|
+
SessionState.ARBITRATION,
|
|
1264
|
+
SessionState.REMEDIATION_REQUIRED,
|
|
1265
|
+
):
|
|
1266
|
+
raise ValueError(f"Clean report cannot be recorded from {self.current_state.value}.")
|
|
1267
|
+
self.breakage_reports.append(dict(report_data))
|
|
1268
|
+
self._reviewed_change_id = str(report_data.get("reviewed_change_id", "")).strip()
|
|
1269
|
+
self.active_breakages = []
|
|
1270
|
+
if self.current_state == SessionState.RED_TEAM_GATE:
|
|
1271
|
+
self.transition_to(SessionState.ARBITRATION, "Arbitration of clean report")
|
|
1272
|
+
self._sealing_authorized = True
|
|
1273
|
+
try:
|
|
1274
|
+
self.transition_to(SessionState.SEALED, "Zero breakages verified")
|
|
1275
|
+
finally:
|
|
1276
|
+
self._sealing_authorized = False
|
|
1277
|
+
return report_data
|
|
1278
|
+
|
|
1279
|
+
if self.current_state == SessionState.IMPLEMENTATION:
|
|
1280
|
+
self.transition_to(SessionState.RED_TEAM_GATE)
|
|
1281
|
+
|
|
1282
|
+
started_at = self.remediation_started_at if self.remediation_started_at is not None else now
|
|
1283
|
+
attempt = self.remediation_attempt_count + 1
|
|
1284
|
+
elapsed = max(0.0, now - started_at)
|
|
1285
|
+
active: List[Dict[str, Any]] = []
|
|
1286
|
+
for finding in report_data.get("findings", []):
|
|
1287
|
+
if bool(finding.get("broken", True) if isinstance(finding, dict) else getattr(finding, "broken", True)):
|
|
1288
|
+
active.append({
|
|
1289
|
+
"scenario_id": finding.get("scenario_id", "") if isinstance(finding, dict) else getattr(finding, "scenario_id", ""),
|
|
1290
|
+
"hypothesis": finding.get("hypothesis", "") if isinstance(finding, dict) else getattr(finding, "hypothesis", ""),
|
|
1291
|
+
"reproduction_code": finding.get("reproduction_code", "") if isinstance(finding, dict) else getattr(finding, "reproduction_code", ""),
|
|
1292
|
+
"severity": finding.get("severity", "MEDIUM") if isinstance(finding, dict) else getattr(finding, "severity", "MEDIUM"),
|
|
1293
|
+
"error_message": finding.get("error_message", "") if isinstance(finding, dict) else getattr(finding, "error_message", ""),
|
|
1294
|
+
"vector": finding.get("vector", "") if isinstance(finding, dict) else getattr(finding, "vector", ""),
|
|
1295
|
+
})
|
|
1296
|
+
|
|
1297
|
+
escalated = attempt >= MAX_REMEDIATION_ATTEMPTS or elapsed >= MAX_REMEDIATION_SECONDS
|
|
1298
|
+
if escalated:
|
|
1299
|
+
for finding in active:
|
|
1300
|
+
tag = "HYPOTHESIS" if str(finding.get("hypothesis", "")).strip() else "UNKNOWN"
|
|
1301
|
+
finding["epistemic_status"] = tag
|
|
1302
|
+
finding["human_arbitration_required"] = True
|
|
1303
|
+
ledger_item = self.log_epistemic_item(
|
|
1304
|
+
tag,
|
|
1305
|
+
str(finding.get("hypothesis") or finding.get("scenario_id") or "Unresolved Red-Team breakage"),
|
|
1306
|
+
evidence=str(finding.get("error_message") or finding.get("reproduction_code") or ""),
|
|
1307
|
+
)
|
|
1308
|
+
finding["epistemic_ledger_item_id"] = ledger_item["id"]
|
|
1309
|
+
|
|
1310
|
+
self.breakage_reports.append(dict(report_data))
|
|
1311
|
+
self.remediation_attempt_count = attempt
|
|
1312
|
+
self.remediation_started_at = started_at
|
|
1313
|
+
self.iteration_count += 1
|
|
1314
|
+
self.active_breakages = active
|
|
1315
|
+
self.remediation_history.append({
|
|
1316
|
+
"iteration": self.iteration_count,
|
|
1317
|
+
"attempt": attempt,
|
|
1318
|
+
"report_id": report_data.get("report_id"),
|
|
1319
|
+
"broken_count": broken_count,
|
|
1320
|
+
"timestamp": now,
|
|
1321
|
+
"breakages": list(active),
|
|
1322
|
+
"remediation_directives": report_data.get("remediation_directives", []),
|
|
1323
|
+
})
|
|
1324
|
+
if self.current_state == SessionState.RED_TEAM_GATE:
|
|
1325
|
+
self.transition_to(SessionState.ARBITRATION, "Arbitration of breakages")
|
|
1326
|
+
if self.current_state == SessionState.ARBITRATION:
|
|
1327
|
+
self.transition_to(SessionState.REMEDIATION_REQUIRED, f"{broken_count} breakages detected")
|
|
1328
|
+
if escalated:
|
|
1329
|
+
self.transition_to(
|
|
1330
|
+
SessionState.ESCALATION_UNRESOLVED_BREAKAGES,
|
|
1331
|
+
"Remediation bounds exceeded; human architecture arbitration required",
|
|
1332
|
+
)
|
|
1333
|
+
elif self.current_state != SessionState.REMEDIATION_REQUIRED:
|
|
1334
|
+
self.transition_to(SessionState.REMEDIATION_REQUIRED, f"{broken_count} breakages detected")
|
|
1335
|
+
return report_data
|
|
1336
|
+
|
|
1337
|
+
def log_refinement_cycle(
|
|
1338
|
+
self,
|
|
1339
|
+
refinement_type: str,
|
|
1340
|
+
focus_area: str,
|
|
1341
|
+
critique_or_bottleneck: str,
|
|
1342
|
+
architectural_refinement: str,
|
|
1343
|
+
terminal_probe_results: Optional[str] = None,
|
|
1344
|
+
artifact_path: Optional[str] = None
|
|
1345
|
+
) -> Dict[str, Any]:
|
|
1346
|
+
"""Logs a structured rethink-refine cycle to continuously deepen cognitive quality."""
|
|
1347
|
+
if not refinement_type or not str(refinement_type).strip():
|
|
1348
|
+
raise ValueError("Refinement type cannot be empty.")
|
|
1349
|
+
if not focus_area or not str(focus_area).strip():
|
|
1350
|
+
raise ValueError("Focus area cannot be empty.")
|
|
1351
|
+
if not critique_or_bottleneck or not str(critique_or_bottleneck).strip():
|
|
1352
|
+
raise ValueError("Critique or bottleneck cannot be empty.")
|
|
1353
|
+
if not architectural_refinement or not str(architectural_refinement).strip():
|
|
1354
|
+
raise ValueError("Architectural refinement cannot be empty.")
|
|
1355
|
+
|
|
1356
|
+
cycle_num = len(self.refinement_cycles) + 1
|
|
1357
|
+
entry = {
|
|
1358
|
+
"cycle_number": cycle_num,
|
|
1359
|
+
"refinement_type": str(refinement_type).strip(),
|
|
1360
|
+
"focus_area": str(focus_area).strip(),
|
|
1361
|
+
"critique_or_bottleneck": str(critique_or_bottleneck).strip(),
|
|
1362
|
+
"architectural_refinement": str(architectural_refinement).strip(),
|
|
1363
|
+
"terminal_probe_results": (terminal_probe_results or "").strip() if terminal_probe_results else None,
|
|
1364
|
+
"artifact_path": (artifact_path or "").strip() if artifact_path else None,
|
|
1365
|
+
"timestamp": self._wall_clock(),
|
|
1366
|
+
"phase": self.active_phase
|
|
1367
|
+
}
|
|
1368
|
+
self.refinement_cycles.append(entry)
|
|
1369
|
+
|
|
1370
|
+
# Update Session Active Free Energy F
|
|
1371
|
+
from fable_v2.system3 import ActiveInferenceEngine, create_default_architecture_pomdp
|
|
1372
|
+
fe_engine = ActiveInferenceEngine(create_default_architecture_pomdp())
|
|
1373
|
+
obs = "HIGH_THROUGHPUT_CLEAN" if terminal_probe_results and any(
|
|
1374
|
+
kw in terminal_probe_results.lower() for kw in ("pass", "ok", "success")
|
|
1375
|
+
) else "LOCK_CONTENTION_WARN"
|
|
1376
|
+
f_val, comp, acc = fe_engine.update_beliefs(obs)
|
|
1377
|
+
self.active_free_energy = {
|
|
1378
|
+
"variational_free_energy_f": round(f_val, 4),
|
|
1379
|
+
"complexity_kl": round(comp, 4),
|
|
1380
|
+
"accuracy_log_likelihood": round(acc, 4),
|
|
1381
|
+
"observation": obs,
|
|
1382
|
+
"cycle_number": cycle_num,
|
|
1383
|
+
"timestamp": self._wall_clock(),
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
# Update Causal DAG nodes (initialize default DAG if none exists)
|
|
1387
|
+
if not self.system3_causal_graphs:
|
|
1388
|
+
self.system3_causal_graphs.append({
|
|
1389
|
+
"dag": {"name": f"Session_{self.session_name}_DAG", "nodes": [], "edges": []},
|
|
1390
|
+
"nodes": [],
|
|
1391
|
+
"edges": [],
|
|
1392
|
+
"topological_order": [],
|
|
1393
|
+
"timestamp": self._wall_clock(),
|
|
1394
|
+
})
|
|
1395
|
+
if self.system3_causal_graphs:
|
|
1396
|
+
causal_node_id = f"refine_cycle_{cycle_num}"
|
|
1397
|
+
causal_node_data = {
|
|
1398
|
+
"node_id": causal_node_id,
|
|
1399
|
+
"name": f"Refinement {cycle_num}: {focus_area}",
|
|
1400
|
+
"node_type": "INTERVENTION",
|
|
1401
|
+
"value": 1.0,
|
|
1402
|
+
"parents": [f"refine_cycle_{cycle_num - 1}"] if cycle_num > 1 else [],
|
|
1403
|
+
"metadata": {
|
|
1404
|
+
"refinement_type": refinement_type,
|
|
1405
|
+
"focus_area": focus_area,
|
|
1406
|
+
"critique": critique_or_bottleneck,
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
latest_graph = self.system3_causal_graphs[-1]
|
|
1410
|
+
if "nodes" in latest_graph and isinstance(latest_graph["nodes"], list):
|
|
1411
|
+
if not any(n.get("node_id") == causal_node_id for n in latest_graph["nodes"] if isinstance(n, dict)):
|
|
1412
|
+
latest_graph["nodes"].append(causal_node_data)
|
|
1413
|
+
inner_dag = latest_graph.get("dag", latest_graph)
|
|
1414
|
+
nodes = inner_dag.setdefault("nodes", [])
|
|
1415
|
+
if isinstance(nodes, list):
|
|
1416
|
+
if not any(n.get("node_id") == causal_node_id for n in nodes if isinstance(n, dict)):
|
|
1417
|
+
nodes.append(causal_node_data)
|
|
1418
|
+
elif isinstance(nodes, dict):
|
|
1419
|
+
nodes[causal_node_id] = causal_node_data
|
|
1420
|
+
if cycle_num > 1:
|
|
1421
|
+
prev_id = f"refine_cycle_{cycle_num - 1}"
|
|
1422
|
+
edge_data = {"source": prev_id, "target": causal_node_id, "weight": 1.0, "mechanism": "refinement_evolution"}
|
|
1423
|
+
if "edges" in latest_graph and isinstance(latest_graph["edges"], list):
|
|
1424
|
+
latest_graph["edges"].append(edge_data)
|
|
1425
|
+
edges = inner_dag.setdefault("edges", [])
|
|
1426
|
+
if isinstance(edges, list):
|
|
1427
|
+
edges.append(edge_data)
|
|
1428
|
+
|
|
1429
|
+
return entry
|
|
1430
|
+
|
|
1431
|
+
@staticmethod
|
|
1432
|
+
def _force_override_authorized(token: Optional[str]) -> bool:
|
|
1433
|
+
"""Allow emergency override only through an out-of-band secret.
|
|
1434
|
+
|
|
1435
|
+
The old implementation accepted a public hard-coded string, which
|
|
1436
|
+
allowed any model with tool access to self-authorize an early unlock.
|
|
1437
|
+
"""
|
|
1438
|
+
configured = os.environ.get(FORCE_UNLOCK_ENV)
|
|
1439
|
+
if not configured or not token:
|
|
1440
|
+
return False
|
|
1441
|
+
try:
|
|
1442
|
+
return hmac.compare_digest(str(token), configured)
|
|
1443
|
+
except TypeError:
|
|
1444
|
+
return False
|
|
1445
|
+
|
|
1446
|
+
def unlock_execution(self, rationale: str, force_override_token: Optional[str] = None) -> Dict[str, Any]:
|
|
1447
|
+
"""
|
|
1448
|
+
Anti-Rush Lockout & Hard Time-Lock Validator:
|
|
1449
|
+
Ensures cognitive rigor and pacing compliance before code execution is permitted:
|
|
1450
|
+
1. Immutable authority deadline has elapsed. An internal pacing timer is never sufficient.
|
|
1451
|
+
2. At least 2 evidence-backed PROVEN items are recorded.
|
|
1452
|
+
3. At least 1 formal invariant includes a proof or rationale.
|
|
1453
|
+
4. Active phase is at least Phase 3 (Phase 3, 4, 5, or 6).
|
|
1454
|
+
5. Anti-Idle check: at least 1 refinement cycle per 5 minutes of budget (minimum 2 cycles).
|
|
1455
|
+
|
|
1456
|
+
Emergency overrides are intentionally not exposed through the MCP
|
|
1457
|
+
tool schema. A host application may configure an out-of-band secret
|
|
1458
|
+
for direct administrative use, but the model cannot self-authorize it.
|
|
1459
|
+
"""
|
|
1460
|
+
now = self._wall_clock()
|
|
1461
|
+
force_override_used = self._force_override_authorized(force_override_token)
|
|
1462
|
+
remaining_sec = self._authority_remaining_seconds()
|
|
1463
|
+
if remaining_sec > 0 and not force_override_used:
|
|
1464
|
+
rem_formatted = self._format_duration(remaining_sec)
|
|
1465
|
+
raise PermissionError(
|
|
1466
|
+
f"🛑 HARD TIME-LOCK VIOLATION: Execution unlock rejected! The immutable "
|
|
1467
|
+
f"{self.time_budget_minutes}m authority budget has not elapsed yet "
|
|
1468
|
+
f"(Remaining: {rem_formatted} / {remaining_sec:.1f}s). An internal pacing "
|
|
1469
|
+
f"timer cannot unlock execution. Continue the Rethink-Refine Cognitive Loop."
|
|
1470
|
+
)
|
|
1471
|
+
|
|
1472
|
+
gate_report = self._gate_report()
|
|
1473
|
+
proven_items = [i for i in self.epistemic_ledger if i.get("tag") == "PROVEN" and not i.get("_restored_untrusted")]
|
|
1474
|
+
errors: List[str] = []
|
|
1475
|
+
if not gate_report["checks"]["two_proven_evidence_items"]:
|
|
1476
|
+
errors.append(
|
|
1477
|
+
f"Requires at least 2 [PROVEN] items with evidence "
|
|
1478
|
+
f"(currently {gate_report['proven_with_evidence']})."
|
|
1479
|
+
)
|
|
1480
|
+
if not gate_report["checks"]["one_proved_invariant"]:
|
|
1481
|
+
errors.append("Requires at least 1 formal Invariant with a proof or rationale.")
|
|
1482
|
+
if not gate_report["checks"]["adversarial_phase_reached"]:
|
|
1483
|
+
errors.append(
|
|
1484
|
+
f"Requires phase progression to at least Phase 3: Adversarial Red-Teaming & Falsification "
|
|
1485
|
+
f"(currently in {self.active_phase})."
|
|
1486
|
+
)
|
|
1487
|
+
|
|
1488
|
+
# Anti-idle check: require minimum refinement cycles (at least 1 cycle per 5 minutes of budget, min 2 cycles)
|
|
1489
|
+
min_refinements = max(2, math.ceil(self.time_budget_minutes / 5.0))
|
|
1490
|
+
valid_refinements = [r for r in self.refinement_cycles if not r.get("_restored_untrusted")]
|
|
1491
|
+
if len(valid_refinements) < min_refinements:
|
|
1492
|
+
errors.append(
|
|
1493
|
+
f"Anti-Idle Requirement Not Satisfied: Requires at least {min_refinements} rethink-refine cycles for a "
|
|
1494
|
+
f"{self.time_budget_minutes}m budget (currently {len(valid_refinements)} recorded). "
|
|
1495
|
+
f"Continuous cognitive refinement is mandatory during deliberation."
|
|
1496
|
+
)
|
|
1497
|
+
|
|
1498
|
+
if errors:
|
|
1499
|
+
reasons = "\n".join([f" - {e}" for e in errors])
|
|
1500
|
+
raise PermissionError(
|
|
1501
|
+
f"🛑 Anti-Rush Lockout Active! Execution unlock denied due to missing cognitive gates:\n{reasons}\n\n"
|
|
1502
|
+
f"Please log required proven facts, formal invariants, refinement cycles, and advance to Phase 3+ before unlocking."
|
|
1503
|
+
)
|
|
1504
|
+
|
|
1505
|
+
self.execution_locked = False
|
|
1506
|
+
self.can_execute_code = True
|
|
1507
|
+
if self.current_state == SessionState.INIT:
|
|
1508
|
+
self.transition_to(SessionState.DEEPTHINK_TIMELOCK, "Pre-unlock transition to DEEPTHINK_TIMELOCK")
|
|
1509
|
+
if self.current_state == SessionState.DEEPTHINK_TIMELOCK:
|
|
1510
|
+
self.transition_to(SessionState.IMPLEMENTATION, rationale)
|
|
1511
|
+
|
|
1512
|
+
self.unlock_details = {
|
|
1513
|
+
"unlocked_at": now,
|
|
1514
|
+
"rationale": rationale.strip(),
|
|
1515
|
+
"proven_count": len(proven_items),
|
|
1516
|
+
"invariants_count": len(self.invariants),
|
|
1517
|
+
"refinement_cycles_count": len(self.refinement_cycles),
|
|
1518
|
+
"phase": self.active_phase,
|
|
1519
|
+
"force_override_used": force_override_used,
|
|
1520
|
+
"authority_deadline_elapsed": remaining_sec <= 0
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
return {
|
|
1524
|
+
"status": "UNLOCKED",
|
|
1525
|
+
"execution_locked": False,
|
|
1526
|
+
"can_execute_code": True,
|
|
1527
|
+
"unlock_details": self.unlock_details
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1531
|
+
"""Serializes session to dictionary."""
|
|
1532
|
+
return {
|
|
1533
|
+
"version": "1.3.0",
|
|
1534
|
+
"session_name": self.session_name,
|
|
1535
|
+
"session_id": self.session_id,
|
|
1536
|
+
"objective": self.objective,
|
|
1537
|
+
"start_time": self.start_time,
|
|
1538
|
+
"time_budget_minutes": self.time_budget_minutes,
|
|
1539
|
+
"time_budget_seconds": self.time_budget_seconds,
|
|
1540
|
+
"authority_deadline_time": self.deadline_time,
|
|
1541
|
+
"deadline_time": self.deadline_time,
|
|
1542
|
+
"pacing_budget_minutes": self.pacing_budget_minutes,
|
|
1543
|
+
"pacing_budget_seconds": self.pacing_budget_seconds,
|
|
1544
|
+
"pacing_started_time": self._pacing_started_wall,
|
|
1545
|
+
"pacing_deadline_time": self._pacing_deadline_wall,
|
|
1546
|
+
"active_phase": self.active_phase,
|
|
1547
|
+
"execution_locked": self.execution_locked,
|
|
1548
|
+
"can_execute_code": self.can_execute_code,
|
|
1549
|
+
"current_state": self.current_state.value if isinstance(self.current_state, SessionState) else str(self.current_state),
|
|
1550
|
+
"iteration_count": self.iteration_count,
|
|
1551
|
+
"remediation_attempt_count": self.remediation_attempt_count,
|
|
1552
|
+
"remediation_started_at": self.remediation_started_at,
|
|
1553
|
+
"reviewed_change_id": self._reviewed_change_id,
|
|
1554
|
+
"active_breakages": self.active_breakages,
|
|
1555
|
+
"remediation_history": self.remediation_history,
|
|
1556
|
+
"epistemic_ledger": self.epistemic_ledger,
|
|
1557
|
+
"invariants": self.invariants,
|
|
1558
|
+
"refinement_cycles": self.refinement_cycles,
|
|
1559
|
+
"file_changes": self.file_changes,
|
|
1560
|
+
"visual_mockups": self.visual_mockups,
|
|
1561
|
+
"proof_receipts": self.proof_receipts,
|
|
1562
|
+
"goal_rubrics": self.goal_rubrics,
|
|
1563
|
+
"automation_pipelines": self.automation_pipelines,
|
|
1564
|
+
"breakage_reports": self.breakage_reports,
|
|
1565
|
+
"phase_history": self.phase_history,
|
|
1566
|
+
"unlock_details": self.unlock_details,
|
|
1567
|
+
"system3_causal_graphs": self.system3_causal_graphs,
|
|
1568
|
+
"system3_syntheses": self.system3_syntheses,
|
|
1569
|
+
"system3_gene_pools": self.system3_gene_pools,
|
|
1570
|
+
"system3_axioms": self.system3_axioms,
|
|
1571
|
+
"system3_reflections": self.system3_reflections,
|
|
1572
|
+
"system3_orchestrations": self.system3_orchestrations,
|
|
1573
|
+
"system3_hyperbolic_embeddings": self.system3_hyperbolic_embeddings,
|
|
1574
|
+
"system3_kripke_verifications": self.system3_kripke_verifications,
|
|
1575
|
+
"system3_active_inferences": self.system3_active_inferences,
|
|
1576
|
+
"system3_proof_oracle_verifications": self.system3_proof_oracle_verifications,
|
|
1577
|
+
"active_free_energy": self.active_free_energy,
|
|
1578
|
+
"active_kripke_safety": self.active_kripke_safety,
|
|
1579
|
+
"active_biases": self.active_biases,
|
|
1580
|
+
"fable_run": self.fable_run.to_dict() if getattr(self, "fable_run", None) else None,
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
@classmethod
|
|
1584
|
+
def from_dict(cls, data: Dict[str, Any]) -> "FableSession":
|
|
1585
|
+
"""Restore data without importing persisted execution authority.
|
|
1586
|
+
|
|
1587
|
+
Persistence is an interchange format, not an authority token. The
|
|
1588
|
+
deadline, phase, evidence, invariants, and unlock flags are all treated
|
|
1589
|
+
as untrusted; a restored process starts a fresh locked authority clock.
|
|
1590
|
+
Historical data remains visible, but cannot satisfy fresh gates.
|
|
1591
|
+
"""
|
|
1592
|
+
budget = data.get("time_budget_minutes", 60.0)
|
|
1593
|
+
session = cls(session_name=data["session_name"], objective=data.get("objective", ""),
|
|
1594
|
+
time_budget_minutes=budget, session_id=data.get("session_id"))
|
|
1595
|
+
session._restored_untrusted = True
|
|
1596
|
+
state_str = data.get("current_state", SessionState.INIT.value)
|
|
1597
|
+
try:
|
|
1598
|
+
restored_state = SessionState(state_str)
|
|
1599
|
+
except ValueError:
|
|
1600
|
+
restored_state = SessionState.INIT
|
|
1601
|
+
if restored_state in (SessionState.SEALED, SessionState.EVOLVED):
|
|
1602
|
+
restored_state = SessionState.INIT
|
|
1603
|
+
session.current_state = restored_state
|
|
1604
|
+
session.iteration_count = int(data.get("iteration_count", 0))
|
|
1605
|
+
session.remediation_attempt_count = int(data.get("remediation_attempt_count", 0))
|
|
1606
|
+
session.remediation_started_at = data.get("remediation_started_at")
|
|
1607
|
+
session._reviewed_change_id = data.get("reviewed_change_id")
|
|
1608
|
+
session.active_breakages = list(data.get("active_breakages", []))
|
|
1609
|
+
session.remediation_history = list(data.get("remediation_history", []))
|
|
1610
|
+
session.epistemic_ledger = [dict(item, _restored_untrusted=True) for item in data.get("epistemic_ledger", []) if isinstance(item, dict)]
|
|
1611
|
+
session.invariants = [dict(item, _restored_untrusted=True) for item in data.get("invariants", []) if isinstance(item, dict)]
|
|
1612
|
+
session.refinement_cycles = [dict(item, _restored_untrusted=True) for item in data.get("refinement_cycles", []) if isinstance(item, dict)]
|
|
1613
|
+
session.file_changes = [
|
|
1614
|
+
dict(item, _restored_untrusted=True)
|
|
1615
|
+
for item in data.get("file_changes", [])
|
|
1616
|
+
if isinstance(item, dict)
|
|
1617
|
+
]
|
|
1618
|
+
session.visual_mockups = data.get("visual_mockups", {"mockups": [], "selected_concept": None})
|
|
1619
|
+
session.proof_receipts = [
|
|
1620
|
+
dict(item, _restored_untrusted=True)
|
|
1621
|
+
for item in data.get("proof_receipts", [])
|
|
1622
|
+
if isinstance(item, dict)
|
|
1623
|
+
]
|
|
1624
|
+
session.goal_rubrics = [
|
|
1625
|
+
dict(item, _restored_untrusted=True)
|
|
1626
|
+
for item in data.get("goal_rubrics", [])
|
|
1627
|
+
if isinstance(item, dict)
|
|
1628
|
+
]
|
|
1629
|
+
session.automation_pipelines = data.get("automation_pipelines", [])
|
|
1630
|
+
session.breakage_reports = data.get("breakage_reports", [])
|
|
1631
|
+
session.active_phase = PHASES[0]
|
|
1632
|
+
session.phase_history = [{"phase": PHASES[0], "entered_at": session.start_time,
|
|
1633
|
+
"summary": "Restored in safe locked state; fresh gates required"}]
|
|
1634
|
+
session.execution_locked = True
|
|
1635
|
+
session.can_execute_code = False
|
|
1636
|
+
session.unlock_details = None
|
|
1637
|
+
session.system3_causal_graphs = data.get("system3_causal_graphs", [])
|
|
1638
|
+
session.system3_syntheses = data.get("system3_syntheses", [])
|
|
1639
|
+
session.system3_gene_pools = data.get("system3_gene_pools", [])
|
|
1640
|
+
session.system3_axioms = data.get("system3_axioms", [])
|
|
1641
|
+
session.system3_reflections = data.get("system3_reflections", [])
|
|
1642
|
+
session.system3_orchestrations = data.get("system3_orchestrations", [])
|
|
1643
|
+
session.system3_hyperbolic_embeddings = data.get("system3_hyperbolic_embeddings", [])
|
|
1644
|
+
session.system3_kripke_verifications = data.get("system3_kripke_verifications", [])
|
|
1645
|
+
session.system3_active_inferences = data.get("system3_active_inferences", [])
|
|
1646
|
+
session.system3_proof_oracle_verifications = data.get("system3_proof_oracle_verifications", [])
|
|
1647
|
+
session.active_free_energy = data.get("active_free_energy")
|
|
1648
|
+
session.active_kripke_safety = data.get("active_kripke_safety")
|
|
1649
|
+
session.active_biases = data.get("active_biases", [])
|
|
1650
|
+
fable_run_data = data.get("fable_run")
|
|
1651
|
+
if fable_run_data and isinstance(fable_run_data, dict):
|
|
1652
|
+
try:
|
|
1653
|
+
from fable_v2.runtime import FableRun
|
|
1654
|
+
session.fable_run = FableRun.from_dict(fable_run_data)
|
|
1655
|
+
except Exception:
|
|
1656
|
+
pass
|
|
1657
|
+
return session
|
|
1658
|
+
|
|
1659
|
+
def save(self, target_path: Optional[Path] = None) -> Path:
|
|
1660
|
+
"""Atomically save a session using a unique no-follow temporary file and cross-process lock."""
|
|
1661
|
+
path = Path(target_path or (SESSIONS_DIR / f"{self.session_name}.json")).expanduser().absolute()
|
|
1662
|
+
with session_file_lock(path):
|
|
1663
|
+
parent = path.parent
|
|
1664
|
+
_assert_private_path(parent)
|
|
1665
|
+
parent.mkdir(parents=True, exist_ok=True)
|
|
1666
|
+
_assert_private_path(parent)
|
|
1667
|
+
if parent.is_symlink() or not parent.is_dir() or path.is_symlink():
|
|
1668
|
+
raise OSError("session path or parent is a symlink/reparse point")
|
|
1669
|
+
os.chmod(parent, 0o700)
|
|
1670
|
+
if os.name == "posix" and hasattr(os, "O_NOFOLLOW"):
|
|
1671
|
+
parent_fd = _open_directory_nofollow(parent, create=True)
|
|
1672
|
+
temp_name = f".{path.name}.{os.getpid()}-{os.urandom(8).hex()}.tmp"
|
|
1673
|
+
fd = None
|
|
1674
|
+
try:
|
|
1675
|
+
fd = os.open(temp_name, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600, dir_fd=parent_fd)
|
|
1676
|
+
os.fchmod(fd, 0o600)
|
|
1677
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
1678
|
+
fd = None
|
|
1679
|
+
json.dump(self.to_dict(), handle, indent=2)
|
|
1680
|
+
handle.flush()
|
|
1681
|
+
os.fsync(handle.fileno())
|
|
1682
|
+
os.replace(temp_name, path.name, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)
|
|
1683
|
+
try:
|
|
1684
|
+
os.fsync(parent_fd)
|
|
1685
|
+
except OSError:
|
|
1686
|
+
# Linux O_PATH directory descriptors are suitable for
|
|
1687
|
+
# descriptor-relative publication but not fsync targets.
|
|
1688
|
+
pass
|
|
1689
|
+
finally:
|
|
1690
|
+
if fd is not None:
|
|
1691
|
+
os.close(fd)
|
|
1692
|
+
try:
|
|
1693
|
+
os.unlink(temp_name, dir_fd=parent_fd)
|
|
1694
|
+
except OSError:
|
|
1695
|
+
pass
|
|
1696
|
+
os.close(parent_fd)
|
|
1697
|
+
else:
|
|
1698
|
+
fd, temporary = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=str(parent))
|
|
1699
|
+
temp_path = Path(temporary)
|
|
1700
|
+
try:
|
|
1701
|
+
if hasattr(os, "fchmod"):
|
|
1702
|
+
os.fchmod(fd, 0o600)
|
|
1703
|
+
else:
|
|
1704
|
+
os.chmod(temporary, 0o600)
|
|
1705
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
1706
|
+
json.dump(self.to_dict(), handle, indent=2)
|
|
1707
|
+
handle.flush()
|
|
1708
|
+
os.fsync(handle.fileno())
|
|
1709
|
+
os.replace(temp_path, path)
|
|
1710
|
+
finally:
|
|
1711
|
+
if temp_path.exists() or temp_path.is_symlink():
|
|
1712
|
+
temp_path.unlink()
|
|
1713
|
+
try:
|
|
1714
|
+
self._disk_mtime = path.stat().st_mtime
|
|
1715
|
+
except OSError:
|
|
1716
|
+
pass
|
|
1717
|
+
logger.info(f"Fable session '{self.session_name}' saved to {path}")
|
|
1718
|
+
return path
|
|
1719
|
+
|
|
1720
|
+
|
|
1721
|
+
@contextmanager
|
|
1722
|
+
def session_file_lock(session_path: Path, timeout: float = 5.0):
|
|
1723
|
+
"""Cross-process file lock using msvcrt on Windows and fcntl on POSIX."""
|
|
1724
|
+
lock_file = session_path.parent / f".{session_path.name}.lock"
|
|
1725
|
+
start = time.time()
|
|
1726
|
+
handle = None
|
|
1727
|
+
try:
|
|
1728
|
+
handle = open(lock_file, "a+b")
|
|
1729
|
+
acquired = False
|
|
1730
|
+
while time.time() - start < timeout:
|
|
1731
|
+
try:
|
|
1732
|
+
if os.name == "nt":
|
|
1733
|
+
import msvcrt
|
|
1734
|
+
handle.seek(0)
|
|
1735
|
+
msvcrt.locking(handle.fileno(), msvcrt.LK_NBLCK, 1)
|
|
1736
|
+
else:
|
|
1737
|
+
import fcntl
|
|
1738
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
1739
|
+
acquired = True
|
|
1740
|
+
break
|
|
1741
|
+
except (OSError, BlockingIOError):
|
|
1742
|
+
time.sleep(0.02)
|
|
1743
|
+
if not acquired:
|
|
1744
|
+
logger.warning(f"Session lock acquisition timed out for {session_path}")
|
|
1745
|
+
yield
|
|
1746
|
+
finally:
|
|
1747
|
+
if handle is not None:
|
|
1748
|
+
try:
|
|
1749
|
+
if os.name == "nt":
|
|
1750
|
+
import msvcrt
|
|
1751
|
+
handle.seek(0)
|
|
1752
|
+
msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
|
|
1753
|
+
else:
|
|
1754
|
+
import fcntl
|
|
1755
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
|
|
1756
|
+
except OSError:
|
|
1757
|
+
pass
|
|
1758
|
+
try:
|
|
1759
|
+
handle.close()
|
|
1760
|
+
except OSError:
|
|
1761
|
+
pass
|
|
1762
|
+
|
|
1763
|
+
|
|
1764
|
+
# In-Memory Active Sessions Table
|
|
1765
|
+
ACTIVE_SESSIONS: Dict[str, FableSession] = {}
|
|
1766
|
+
|
|
1767
|
+
|
|
1768
|
+
def _safe_session_file(path: Path) -> None:
|
|
1769
|
+
_assert_private_path(path.parent)
|
|
1770
|
+
try:
|
|
1771
|
+
st = path.lstat()
|
|
1772
|
+
except FileNotFoundError:
|
|
1773
|
+
raise ValueError(f"session file does not exist: {path.name}")
|
|
1774
|
+
attrs = int(getattr(st, "st_file_attributes", 0))
|
|
1775
|
+
if (attrs & 0x400 or stat.S_ISLNK(st.st_mode) or not stat.S_ISREG(st.st_mode)
|
|
1776
|
+
or st.st_nlink != 1 or (os.name != "nt" and stat.S_IMODE(st.st_mode) & 0o077)):
|
|
1777
|
+
raise ValueError("session file must be a private regular non-hardlinked file")
|
|
1778
|
+
|
|
1779
|
+
|
|
1780
|
+
def get_or_load_session(session_name: str) -> FableSession:
|
|
1781
|
+
"""Retrieves session from memory or loads from disk if exists, checking mtime for cross-process synchronization."""
|
|
1782
|
+
clean_name = _validate_session_name(session_name)
|
|
1783
|
+
file_path = SESSIONS_DIR / f"{clean_name}.json"
|
|
1784
|
+
|
|
1785
|
+
if clean_name in ACTIVE_SESSIONS:
|
|
1786
|
+
session = ACTIVE_SESSIONS[clean_name]
|
|
1787
|
+
if file_path.exists():
|
|
1788
|
+
try:
|
|
1789
|
+
disk_mtime = file_path.stat().st_mtime
|
|
1790
|
+
if disk_mtime > getattr(session, "_disk_mtime", 0.0):
|
|
1791
|
+
with session_file_lock(file_path):
|
|
1792
|
+
_safe_session_file(file_path)
|
|
1793
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
1794
|
+
data = json.load(f)
|
|
1795
|
+
session = FableSession.from_dict(data)
|
|
1796
|
+
session._disk_mtime = disk_mtime
|
|
1797
|
+
ACTIVE_SESSIONS[clean_name] = session
|
|
1798
|
+
except Exception as exc:
|
|
1799
|
+
logger.warning(f"Failed to check disk mtime reload for session {clean_name}: {exc}")
|
|
1800
|
+
return session
|
|
1801
|
+
|
|
1802
|
+
if file_path.exists() or file_path.is_symlink():
|
|
1803
|
+
try:
|
|
1804
|
+
with session_file_lock(file_path):
|
|
1805
|
+
_safe_session_file(file_path)
|
|
1806
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
1807
|
+
data = json.load(f)
|
|
1808
|
+
session = FableSession.from_dict(data)
|
|
1809
|
+
try:
|
|
1810
|
+
session._disk_mtime = file_path.stat().st_mtime
|
|
1811
|
+
except OSError:
|
|
1812
|
+
pass
|
|
1813
|
+
ACTIVE_SESSIONS[clean_name] = session
|
|
1814
|
+
return session
|
|
1815
|
+
except Exception as e:
|
|
1816
|
+
logger.error(f"Failed to load session file {file_path}: {e}")
|
|
1817
|
+
raise RuntimeError(f"Corrupt or unreadable session file for '{clean_name}': {e}")
|
|
1818
|
+
|
|
1819
|
+
raise ValueError(f"Session '{clean_name}' does not exist. Call 'create_session' first.")
|