ct-toolkit 0.3.6__tar.gz → 0.3.8__tar.gz
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.
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/PKG-INFO +5 -3
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/README.md +1 -1
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/__init__.py +1 -1
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/cli.py +32 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/core/exceptions.py +16 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/core/wrapper.py +158 -109
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/divergence/l3_icm.py +53 -16
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/endorsement/reflective.py +306 -15
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/identity/embedding.py +16 -14
- ct_toolkit-0.3.8/ct_toolkit/middleware/litellm.py +344 -0
- ct_toolkit-0.3.8/ct_toolkit/server.py +174 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit.egg-info/PKG-INFO +5 -3
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit.egg-info/SOURCES.txt +2 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit.egg-info/requires.txt +3 -1
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/pyproject.toml +4 -2
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/LICENSE +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/__main__.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/core/__init__.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/core/compatibility.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/core/compression_guard.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/core/integrity.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/core/kernel.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/divergence/__init__.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/divergence/analysis.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/divergence/engine.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/divergence/l2_judge.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/divergence/loss.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/divergence/scheduler.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/endorsement/__init__.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/endorsement/probes/__init__.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/identity/__init__.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/identity/templates/__init__.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/identity/templates/defense.yaml +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/identity/templates/finance.yaml +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/identity/templates/general.yaml +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/identity/templates/medical.yaml +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/kernels/__init__.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/kernels/default.yaml +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/kernels/defense.yaml +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/middleware/__init__.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/middleware/autogen.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/middleware/crewai.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/middleware/deepagents.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/middleware/langchain.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/provenance/__init__.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/provenance/log.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/provenance/vault/__init__.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/utils/__init__.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit/utils/logger.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit.egg-info/dependency_links.txt +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit.egg-info/entry_points.txt +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/ct_toolkit.egg-info/top_level.txt +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/setup.cfg +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/tests/test_analysis.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/tests/test_cli.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/tests/test_cli_extended.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/tests/test_deep_agents_integration.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/tests/test_loss.py +0 -0
- {ct_toolkit-0.3.6 → ct_toolkit-0.3.8}/tests/test_summarization_drift.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ct-toolkit
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.8
|
|
4
4
|
Summary: Computational Theseus Toolkit — Identity Continuity Guardrails for Agentic Systems
|
|
5
5
|
Author: Hakan Damar
|
|
6
6
|
License: Apache-2.0
|
|
@@ -9,7 +9,7 @@ Requires-Python: >=3.11
|
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Requires-Dist: instructor>=1.3.0
|
|
12
|
-
Requires-Dist:
|
|
12
|
+
Requires-Dist: litellm>=1.40.0
|
|
13
13
|
Requires-Dist: numpy>=1.26.0
|
|
14
14
|
Requires-Dist: pyyaml>=6.0
|
|
15
15
|
Requires-Dist: pydantic>=2.5.0
|
|
@@ -20,6 +20,8 @@ Requires-Dist: rich>=13.0.0
|
|
|
20
20
|
Requires-Dist: jinja2>=3.1.0
|
|
21
21
|
Requires-Dist: pyasn1>=0.6.3
|
|
22
22
|
Requires-Dist: typer[all]>=0.12.0
|
|
23
|
+
Requires-Dist: fastapi>=0.110.0
|
|
24
|
+
Requires-Dist: uvicorn>=0.29.0
|
|
23
25
|
Provides-Extra: ollama
|
|
24
26
|
Requires-Dist: ollama>=0.2.0; extra == "ollama"
|
|
25
27
|
Provides-Extra: langchain
|
|
@@ -116,7 +118,7 @@ ct-toolkit list-templates
|
|
|
116
118
|
|
|
117
119
|
| Metric | Status |
|
|
118
120
|
| :--------------- | :------------------------------------------------------ |
|
|
119
|
-
| **Tests** | ✅
|
|
121
|
+
| **Tests** | ✅ 281/292 passing (95% coverage) |
|
|
120
122
|
| **Last Phase** | ✅ Phase 6: Auditor Mode (Complete) |
|
|
121
123
|
| **Current Goal** | 🔶 Phase 7: Multi-Agent Synchronization (Integration) |
|
|
122
124
|
|
|
@@ -75,7 +75,7 @@ ct-toolkit list-templates
|
|
|
75
75
|
|
|
76
76
|
| Metric | Status |
|
|
77
77
|
| :--------------- | :------------------------------------------------------ |
|
|
78
|
-
| **Tests** | ✅
|
|
78
|
+
| **Tests** | ✅ 281/292 passing (95% coverage) |
|
|
79
79
|
| **Last Phase** | ✅ Phase 6: Auditor Mode (Complete) |
|
|
80
80
|
| **Current Goal** | 🔶 Phase 7: Multi-Agent Synchronization (Integration) |
|
|
81
81
|
|
|
@@ -17,6 +17,7 @@ from rich.table import Table
|
|
|
17
17
|
from ct_toolkit import TheseusWrapper, WrapperConfig, __version__
|
|
18
18
|
from ct_toolkit.divergence.l3_icm import ICMRunner
|
|
19
19
|
from ct_toolkit.core.kernel import ConstitutionalKernel
|
|
20
|
+
from ct_toolkit.server import start_server
|
|
20
21
|
|
|
21
22
|
app = typer.Typer(
|
|
22
23
|
help="Computational Theseus Toolkit — Identity Continuity Guardrails for Agentic Systems.",
|
|
@@ -135,6 +136,37 @@ def audit(
|
|
|
135
136
|
if not report.is_healthy:
|
|
136
137
|
raise typer.Exit(code=1)
|
|
137
138
|
|
|
139
|
+
@app.command()
|
|
140
|
+
def serve(
|
|
141
|
+
host: str = typer.Option("127.0.0.1", "--host", help="Host to bind the server to."),
|
|
142
|
+
port: int = typer.Option(8001, "--port", help="Port to bind the server to."),
|
|
143
|
+
kernel: str = typer.Option("general", "--kernel", help="Name of the Constitutional Kernel to use."),
|
|
144
|
+
template: str = typer.Option("general", "--template", help="Name of the Identity Template to use."),
|
|
145
|
+
vault_path: str = typer.Option("./ct_provenance.db", "--vault", help="Path to the provenance log database."),
|
|
146
|
+
judge_provider: str = typer.Option("openai", "--judge-provider", help="LLM provider for the L2 judge."),
|
|
147
|
+
judge_model: Optional[str] = typer.Option(None, "--judge-model", help="Model ID for the L2 judge."),
|
|
148
|
+
):
|
|
149
|
+
"""Start the CT-Toolkit Guardrail Server for LiteLLM integration."""
|
|
150
|
+
console.print(f"Starting Guardrail Server...")
|
|
151
|
+
console.print(f" - Kernel: [bold yellow]{kernel}[/bold yellow]")
|
|
152
|
+
console.print(f" - Template: [bold green]{template}[/bold green]")
|
|
153
|
+
console.print(f" - Bind: [bold cyan]{host}:{port}[/bold cyan]")
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
config = WrapperConfig(
|
|
157
|
+
kernel_name=kernel,
|
|
158
|
+
template=template,
|
|
159
|
+
vault_path=vault_path,
|
|
160
|
+
project_root=Path.cwd()
|
|
161
|
+
)
|
|
162
|
+
wrapper = TheseusWrapper(provider=judge_provider, config=config)
|
|
163
|
+
|
|
164
|
+
# Start the uvicorn server
|
|
165
|
+
start_server(wrapper=wrapper, host=host, port=port)
|
|
166
|
+
except Exception as e:
|
|
167
|
+
console.print(f"[bold red]Failed to start server:[/bold red] {e}")
|
|
168
|
+
raise typer.Exit(code=1)
|
|
169
|
+
|
|
138
170
|
@app.command()
|
|
139
171
|
def list_kernels():
|
|
140
172
|
"""List available Constitutional Kernels."""
|
|
@@ -85,6 +85,22 @@ class CriticalDivergenceError(DivergenceError):
|
|
|
85
85
|
)
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
class CriticalSandboxDivergenceError(DivergenceError):
|
|
89
|
+
"""
|
|
90
|
+
Raised when a sandbox (staged) agent exhibits critical divergence during cooldown.
|
|
91
|
+
The staged kernel update is rejected and the sandbox is torn down.
|
|
92
|
+
"""
|
|
93
|
+
def __init__(self, endorsement_id: str, l1_score: float | None, reason: str):
|
|
94
|
+
self.endorsement_id = endorsement_id
|
|
95
|
+
self.l1_score = l1_score
|
|
96
|
+
self.reason = reason
|
|
97
|
+
super().__init__(
|
|
98
|
+
f"Sandbox agent for staged endorsement '{endorsement_id[:8]}...' exhibited "
|
|
99
|
+
f"critical divergence during cooldown (score={l1_score}, reason={reason!r}). "
|
|
100
|
+
"Staged update rejected and sandbox torn down."
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
88
104
|
# ── Provenance Errors ────────────────────────────────────────────────────────
|
|
89
105
|
|
|
90
106
|
class ProvenanceError(CTToolkitError):
|
|
@@ -16,25 +16,33 @@ In the background:
|
|
|
16
16
|
2. Each request/response is written to the Provenance Log with HMAC signature.
|
|
17
17
|
3. Embedding cosine similarity (L1 ECS) is calculated.
|
|
18
18
|
4. If the threshold is exceeded, L2/L3 is triggered.
|
|
19
|
+
5. Staged Approval (Cooldown): Risky kernel updates are monitored via shadow requests
|
|
20
|
+
before being promoted to production.
|
|
19
21
|
"""
|
|
20
22
|
from __future__ import annotations
|
|
21
23
|
|
|
22
24
|
import time
|
|
23
|
-
import hashlib
|
|
24
25
|
import os
|
|
25
26
|
from dataclasses import dataclass, field
|
|
26
27
|
from pathlib import Path
|
|
27
|
-
from typing import Any,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
import glob
|
|
28
|
+
from typing import Any, Optional, Callable, Dict, List
|
|
29
|
+
|
|
30
|
+
import litellm
|
|
31
31
|
|
|
32
32
|
from ct_toolkit.core.kernel import ConstitutionalKernel
|
|
33
33
|
from ct_toolkit.core.compatibility import CompatibilityLayer, CompatibilityResult
|
|
34
|
-
from ct_toolkit.core.exceptions import
|
|
34
|
+
from ct_toolkit.core.exceptions import (
|
|
35
|
+
MissingClientError,
|
|
36
|
+
CriticalSandboxDivergenceError
|
|
37
|
+
)
|
|
35
38
|
from ct_toolkit.core.integrity import IntegrityMonitor
|
|
36
39
|
from ct_toolkit.divergence.scheduler import RiskProfile
|
|
37
40
|
from ct_toolkit.core.compression_guard import ContextCompressionGuard
|
|
41
|
+
from ct_toolkit.endorsement.reflective import (
|
|
42
|
+
ReflectiveEndorsement,
|
|
43
|
+
StagedUpdateManager,
|
|
44
|
+
CooldownCalculator
|
|
45
|
+
)
|
|
38
46
|
from ct_toolkit.utils.logger import get_logger
|
|
39
47
|
|
|
40
48
|
logger = get_logger(__name__)
|
|
@@ -76,6 +84,11 @@ class WrapperConfig:
|
|
|
76
84
|
compression_threshold: float = 0.85
|
|
77
85
|
compression_passive_detection: bool = True
|
|
78
86
|
|
|
87
|
+
# -- Staged Approval (Cooldown) --
|
|
88
|
+
endorsement_cooldown_base: int = CooldownCalculator.DEFAULT_BASE_SECONDS
|
|
89
|
+
endorsement_cooldown_max: int = CooldownCalculator.DEFAULT_MAX_SECONDS
|
|
90
|
+
endorsement_no_probe_penalty: int = CooldownCalculator.DEFAULT_NO_PROBE_PENALTY_S
|
|
91
|
+
|
|
79
92
|
|
|
80
93
|
@dataclass
|
|
81
94
|
class CTResponse:
|
|
@@ -87,6 +100,8 @@ class CTResponse:
|
|
|
87
100
|
divergence_tier: str | None = None # "ok" | "l1_warning" | "l2_judge" | "l3_icm"
|
|
88
101
|
provenance_id: str | None = None
|
|
89
102
|
raw_response: Any = field(default=None, repr=False)
|
|
103
|
+
# Sandbox metadata if staged updates are active
|
|
104
|
+
sandbox_divergence: float | None = None
|
|
90
105
|
|
|
91
106
|
def __str__(self) -> str:
|
|
92
107
|
return self.content
|
|
@@ -97,7 +112,7 @@ class TheseusWrapper:
|
|
|
97
112
|
Identity-continuity proxy wrapping the LLM API client.
|
|
98
113
|
|
|
99
114
|
Supported providers: openai (default), anthropic, ollama, google, etc.
|
|
100
|
-
(Any provider supported by
|
|
115
|
+
(Any provider supported by LiteLLM)
|
|
101
116
|
|
|
102
117
|
Usage:
|
|
103
118
|
from ct_toolkit import TheseusWrapper
|
|
@@ -127,15 +142,6 @@ class TheseusWrapper:
|
|
|
127
142
|
) -> None:
|
|
128
143
|
"""
|
|
129
144
|
Initializes TheseusWrapper.
|
|
130
|
-
|
|
131
|
-
Args:
|
|
132
|
-
client: An existing API client instance (OpenAI, Anthropic, etc.).
|
|
133
|
-
Optional if 'provider' is specified.
|
|
134
|
-
config: Optional WrapperConfig instance.
|
|
135
|
-
provider: Provider name (e.g. "openai", "anthropic", "ollama", "google", "cohere").
|
|
136
|
-
If provided, 'any-llm' will be used to initialize the client internally.
|
|
137
|
-
project_root: Path to the user's project root directory. Used to find a custom 'config/' folder.
|
|
138
|
-
Defaults to the current working directory.
|
|
139
145
|
"""
|
|
140
146
|
self._config = config or WrapperConfig(
|
|
141
147
|
kernel_path=kernel_path,
|
|
@@ -177,6 +183,9 @@ class TheseusWrapper:
|
|
|
177
183
|
)
|
|
178
184
|
self._shadow_history: Optional[List[Dict[str, str]]] = None
|
|
179
185
|
|
|
186
|
+
# ── Staged Approval (Cooldown) ──
|
|
187
|
+
self._staged_manager = StagedUpdateManager()
|
|
188
|
+
|
|
180
189
|
# ── Integrity Monitoring ──
|
|
181
190
|
self._integrity_monitor = IntegrityMonitor()
|
|
182
191
|
self._register_monitored_files()
|
|
@@ -185,8 +194,7 @@ class TheseusWrapper:
|
|
|
185
194
|
|
|
186
195
|
def _register_monitored_files(self):
|
|
187
196
|
"""
|
|
188
|
-
Finds and registers all critical configuration files for integrity monitoring
|
|
189
|
-
including both built-in package files and user-provided custom configs.
|
|
197
|
+
Finds and registers all critical configuration files for integrity monitoring.
|
|
190
198
|
"""
|
|
191
199
|
logger.debug("Registering configuration files for integrity monitoring.")
|
|
192
200
|
|
|
@@ -207,10 +215,7 @@ class TheseusWrapper:
|
|
|
207
215
|
self._integrity_monitor.register_file(file_path)
|
|
208
216
|
|
|
209
217
|
except Exception:
|
|
210
|
-
# Fallback for older Python or different environments
|
|
211
218
|
package_root = Path(__file__).parent.parent
|
|
212
|
-
# This is less ideal as it might find non-package files, but it's a
|
|
213
|
-
# reasonable fallback for development environments.
|
|
214
219
|
for pattern in internal_patterns:
|
|
215
220
|
for file_path in package_root.rglob(pattern):
|
|
216
221
|
if file_path.is_file():
|
|
@@ -231,23 +236,16 @@ class TheseusWrapper:
|
|
|
231
236
|
# ── Factory / Init ─────────────────────────────────────────────────────────
|
|
232
237
|
|
|
233
238
|
def _detect_provider(self, client: Any) -> str:
|
|
234
|
-
"""
|
|
235
|
-
Detects the provider from the client instance or string.
|
|
236
|
-
Returns 'unknown' if detection fails.
|
|
237
|
-
"""
|
|
238
239
|
if client is None:
|
|
239
240
|
return "unknown"
|
|
240
241
|
|
|
241
242
|
if isinstance(client, str):
|
|
242
|
-
# client passed as a string (e.g. "openai")
|
|
243
243
|
return client.lower()
|
|
244
244
|
|
|
245
245
|
try:
|
|
246
|
-
# Check for any_llm wrapper
|
|
247
246
|
if hasattr(client, "provider") and isinstance(client.provider, str):
|
|
248
247
|
return client.provider
|
|
249
248
|
|
|
250
|
-
# Check module name
|
|
251
249
|
module = type(client).__module__.lower()
|
|
252
250
|
if "openai" in module: return "openai"
|
|
253
251
|
if "anthropic" in module: return "anthropic"
|
|
@@ -260,21 +258,18 @@ class TheseusWrapper:
|
|
|
260
258
|
return "unknown"
|
|
261
259
|
|
|
262
260
|
def _load_kernel(self) -> ConstitutionalKernel:
|
|
263
|
-
# 1. Check for explicit path
|
|
264
261
|
if self._config.kernel_path:
|
|
265
262
|
logger.debug(f"Loading kernel from explicit path: {self._config.kernel_path}")
|
|
266
263
|
return ConstitutionalKernel.from_yaml(self._config.kernel_path)
|
|
267
264
|
|
|
268
265
|
safe_name = os.path.basename(self._config.kernel_name)
|
|
269
266
|
|
|
270
|
-
# 2. Check for user-defined config directory
|
|
271
267
|
if self._project_root:
|
|
272
268
|
user_config_path = self._project_root / "config" / f"{safe_name}.yaml"
|
|
273
269
|
if user_config_path.exists():
|
|
274
270
|
logger.debug(f"Loading kernel from user config: {user_config_path}")
|
|
275
271
|
return ConstitutionalKernel.from_yaml(user_config_path)
|
|
276
272
|
|
|
277
|
-
# 3. Load built-in kernel using importlib.resources
|
|
278
273
|
try:
|
|
279
274
|
from importlib.resources import files
|
|
280
275
|
kernel_resource = files("ct_toolkit.kernels").joinpath(f"{safe_name}.yaml")
|
|
@@ -282,7 +277,6 @@ class TheseusWrapper:
|
|
|
282
277
|
logger.debug(f"Loading built-in kernel: {safe_name}.yaml")
|
|
283
278
|
return ConstitutionalKernel.from_yaml(kernel_resource)
|
|
284
279
|
except (ImportError, FileNotFoundError):
|
|
285
|
-
# Fallback for older python versions
|
|
286
280
|
kernel_path = (
|
|
287
281
|
Path(__file__).parent.parent
|
|
288
282
|
/ "kernels"
|
|
@@ -298,7 +292,6 @@ class TheseusWrapper:
|
|
|
298
292
|
return ConstitutionalKernel.default()
|
|
299
293
|
|
|
300
294
|
def _init_provenance_log(self) -> Any:
|
|
301
|
-
"""Initializes the Provenance Log vault adapter (Will be fully implemented in Step 2)."""
|
|
302
295
|
from ct_toolkit.provenance.log import ProvenanceLog
|
|
303
296
|
return ProvenanceLog(
|
|
304
297
|
vault_type=self._config.vault_type,
|
|
@@ -306,13 +299,10 @@ class TheseusWrapper:
|
|
|
306
299
|
)
|
|
307
300
|
|
|
308
301
|
def _init_identity_layer(self) -> Any:
|
|
309
|
-
"""Initializes the Identity Embedding Layer."""
|
|
310
302
|
from ct_toolkit.identity.embedding import IdentityEmbeddingLayer
|
|
311
303
|
|
|
312
|
-
# Guard against using raw clients that don't support embeddings
|
|
313
304
|
emb_client = self._config.embedding_client
|
|
314
305
|
if emb_client is None:
|
|
315
|
-
# Only default to main client if it's known to be OpenAI (standard for embeddings)
|
|
316
306
|
if self._provider == "openai":
|
|
317
307
|
emb_client = self._client
|
|
318
308
|
else:
|
|
@@ -327,7 +317,6 @@ class TheseusWrapper:
|
|
|
327
317
|
)
|
|
328
318
|
|
|
329
319
|
def _init_divergence_engine(self) -> Any:
|
|
330
|
-
"""Initializes the Divergence Engine (judge_client is required for L2/L3)."""
|
|
331
320
|
from ct_toolkit.divergence.engine import DivergenceEngine
|
|
332
321
|
from ct_toolkit.divergence.scheduler import ElasticityScheduler
|
|
333
322
|
|
|
@@ -385,15 +374,10 @@ class TheseusWrapper:
|
|
|
385
374
|
) -> CTResponse:
|
|
386
375
|
"""
|
|
387
376
|
Sends a single message, returning the response with identity protection.
|
|
388
|
-
|
|
389
|
-
Args:
|
|
390
|
-
message: User message
|
|
391
|
-
model: Model to use (provider default is used)
|
|
392
|
-
system: Additional system prompt (appended to kernel)
|
|
393
|
-
history: Conversation history [{"role": ..., "content": ...}]
|
|
394
377
|
"""
|
|
395
378
|
# --- Pre-flight Checks ---
|
|
396
379
|
self._integrity_monitor.verify_integrity()
|
|
380
|
+
self._process_staged_updates()
|
|
397
381
|
|
|
398
382
|
composed_system = self._compose_system_prompt(system)
|
|
399
383
|
messages = self._build_messages(message, history, composed_system)
|
|
@@ -451,6 +435,11 @@ class TheseusWrapper:
|
|
|
451
435
|
|
|
452
436
|
break
|
|
453
437
|
|
|
438
|
+
# --- Staged Approval Monitoring (Shadow Requests) ---
|
|
439
|
+
sandbox_divergence = None
|
|
440
|
+
if self._staged_manager.has_active_staged():
|
|
441
|
+
sandbox_divergence = self._run_shadow_requests(message, history, model, **kwargs)
|
|
442
|
+
|
|
454
443
|
# Provenance Log record
|
|
455
444
|
provenance_id = None
|
|
456
445
|
if self._config.log_requests:
|
|
@@ -466,6 +455,7 @@ class TheseusWrapper:
|
|
|
466
455
|
"template": self._config.template,
|
|
467
456
|
"kernel": self._kernel.name,
|
|
468
457
|
"drift_alert_enabled": self._config.drift_alert_callback is not None,
|
|
458
|
+
"sandbox_divergence": sandbox_divergence,
|
|
469
459
|
},
|
|
470
460
|
)
|
|
471
461
|
|
|
@@ -477,8 +467,99 @@ class TheseusWrapper:
|
|
|
477
467
|
divergence_tier=div_result.tier.value,
|
|
478
468
|
provenance_id=provenance_id,
|
|
479
469
|
raw_response=raw_response,
|
|
470
|
+
sandbox_divergence=sandbox_divergence,
|
|
480
471
|
)
|
|
481
472
|
|
|
473
|
+
# ── Shadow Request Logic ──────────────────────────────────────────────────
|
|
474
|
+
|
|
475
|
+
def _run_shadow_requests(
|
|
476
|
+
self,
|
|
477
|
+
message: str,
|
|
478
|
+
history: list[dict[str, str]] | None,
|
|
479
|
+
model: str | None,
|
|
480
|
+
**kwargs: Any,
|
|
481
|
+
) -> float | None:
|
|
482
|
+
"""
|
|
483
|
+
Runs the interaction against one or more 'staged' (sandbox) kernels
|
|
484
|
+
to observe if the proposed changes cause identity drift.
|
|
485
|
+
"""
|
|
486
|
+
active_staged = self._staged_manager.get_active()
|
|
487
|
+
if not active_staged:
|
|
488
|
+
return None
|
|
489
|
+
|
|
490
|
+
# To keep it simple, we monitor the first active staged update
|
|
491
|
+
staged_record = active_staged[0]
|
|
492
|
+
|
|
493
|
+
# 1. Clone current kernel and apply the staged rule
|
|
494
|
+
sandbox_kernel = ConstitutionalKernel.from_dict(self._kernel.to_dict())
|
|
495
|
+
try:
|
|
496
|
+
sandbox_kernel.update_commitment(
|
|
497
|
+
staged_record.commitment_id,
|
|
498
|
+
staged_record.rule_text
|
|
499
|
+
)
|
|
500
|
+
except Exception as e:
|
|
501
|
+
logger.error(f"Failed to apply staged rule to sandbox kernel: {e}")
|
|
502
|
+
return None
|
|
503
|
+
|
|
504
|
+
# 2. Call provider with sandbox kernel
|
|
505
|
+
sandbox_system = sandbox_kernel.get_system_prompt_injection()
|
|
506
|
+
sandbox_messages = self._build_messages(message, history, sandbox_system)
|
|
507
|
+
|
|
508
|
+
try:
|
|
509
|
+
# We use the same model/client as the live request
|
|
510
|
+
sandbox_response = self._call_provider(sandbox_messages, model=model, **kwargs)
|
|
511
|
+
sandbox_content = self._extract_content(sandbox_response)
|
|
512
|
+
|
|
513
|
+
# 3. Analyze sandbox divergence
|
|
514
|
+
sandbox_div_score = self._identity_layer.compute_divergence(sandbox_content)
|
|
515
|
+
|
|
516
|
+
# 4. Critical Divergence Check
|
|
517
|
+
if sandbox_div_score >= self._config.divergence_l3_threshold:
|
|
518
|
+
reason = f"Sandbox divergence L1 score {sandbox_div_score:.4f} exceeded L3 threshold."
|
|
519
|
+
self._staged_manager.reject_staged(staged_record.id, reason=reason)
|
|
520
|
+
raise CriticalSandboxDivergenceError(
|
|
521
|
+
endorsement_id=staged_record.id,
|
|
522
|
+
l1_score=sandbox_div_score,
|
|
523
|
+
reason=reason
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
return sandbox_div_score
|
|
527
|
+
|
|
528
|
+
except CriticalSandboxDivergenceError:
|
|
529
|
+
raise
|
|
530
|
+
except Exception as e:
|
|
531
|
+
logger.warning(f"Shadow request for staged update failed: {e}")
|
|
532
|
+
return None
|
|
533
|
+
|
|
534
|
+
def _process_staged_updates(self) -> None:
|
|
535
|
+
"""
|
|
536
|
+
Checks for staged updates whose cooldown has expired and
|
|
537
|
+
promotes them to the production kernel.
|
|
538
|
+
"""
|
|
539
|
+
promotable = self._staged_manager.get_promotable()
|
|
540
|
+
for record in promotable:
|
|
541
|
+
try:
|
|
542
|
+
logger.info(
|
|
543
|
+
f"Promoting staged endorsement to production: {record.id[:8]}... "
|
|
544
|
+
f"rule='{record.rule_text}'"
|
|
545
|
+
)
|
|
546
|
+
self._kernel.update_commitment(record.commitment_id, record.rule_text)
|
|
547
|
+
|
|
548
|
+
# Write promotion event to log
|
|
549
|
+
if self._config.log_requests:
|
|
550
|
+
self._provenance_log.record(
|
|
551
|
+
request_text=f"[COOLDOWN_EXPIRED] Promoting staged rule",
|
|
552
|
+
response_text=f"Rule applied to production kernel: {record.rule_text}",
|
|
553
|
+
metadata={
|
|
554
|
+
"event_type": "staged_promotion",
|
|
555
|
+
"endorsement_id": record.id,
|
|
556
|
+
"kernel": self._kernel.name,
|
|
557
|
+
"commitment_id": record.commitment_id,
|
|
558
|
+
}
|
|
559
|
+
)
|
|
560
|
+
except Exception as e:
|
|
561
|
+
logger.error(f"Failed to promote staged update {record.id[:8]}: {e}")
|
|
562
|
+
|
|
482
563
|
# ── Provider Dispatch ──────────────────────────────────────────────────────
|
|
483
564
|
|
|
484
565
|
def _has_env_credentials(self) -> bool:
|
|
@@ -489,7 +570,6 @@ class TheseusWrapper:
|
|
|
489
570
|
if self._provider == "anthropic":
|
|
490
571
|
return bool(os.environ.get("ANTHROPIC_API_KEY"))
|
|
491
572
|
if self._provider == "ollama":
|
|
492
|
-
# Ollama typically doesn't need a key locally
|
|
493
573
|
return True
|
|
494
574
|
if self._provider == "google":
|
|
495
575
|
return bool(os.environ.get("GOOGLE_API_KEY"))
|
|
@@ -501,55 +581,40 @@ class TheseusWrapper:
|
|
|
501
581
|
model: str | None,
|
|
502
582
|
**kwargs: Any,
|
|
503
583
|
) -> Any:
|
|
504
|
-
"""Uses
|
|
505
|
-
# --- Passive Context Compression Detection ---
|
|
584
|
+
"""Uses LiteLLM for unified provider calling."""
|
|
506
585
|
if self._config.compression_passive_detection and self._shadow_history:
|
|
507
|
-
# Simple heuristic: message count dropped significantly
|
|
508
|
-
# We use 0.7 as a threshold (30% drop)
|
|
509
586
|
if len(messages) < len(self._shadow_history) * 0.7:
|
|
510
587
|
self._compression_guard.on_passive_detection(
|
|
511
588
|
original=self._shadow_history,
|
|
512
589
|
compressed=messages
|
|
513
590
|
)
|
|
514
591
|
|
|
515
|
-
# Update shadow history for next call
|
|
516
592
|
self._shadow_history = list(messages)
|
|
517
|
-
# Convert messages to any_llm format if necessary
|
|
518
|
-
# any_llm.completion handles different formats internally
|
|
519
593
|
|
|
520
|
-
# Set default models per provider if not specified
|
|
521
594
|
if not model:
|
|
522
595
|
if self._provider == "openai": model = "gpt-4o-mini"
|
|
523
596
|
elif self._provider == "anthropic": model = "claude-3-5-sonnet-latest"
|
|
524
597
|
elif self._provider == "ollama": model = "llama3"
|
|
525
598
|
else: model = "gpt-4o-mini"
|
|
526
599
|
|
|
527
|
-
# any_llm expects "provider:model" format
|
|
528
600
|
full_model = model
|
|
529
|
-
if ":"
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
any_llm_kwargs = {
|
|
534
|
-
"model": full_model,
|
|
535
|
-
"messages": messages,
|
|
536
|
-
**kwargs
|
|
537
|
-
}
|
|
601
|
+
if ":" in model:
|
|
602
|
+
full_model = model.replace(":", "/", 1)
|
|
603
|
+
elif self._provider not in ("openai", "unknown"):
|
|
604
|
+
full_model = f"{self._provider}/{model}"
|
|
538
605
|
|
|
539
|
-
# Extract connection info from client if possible
|
|
540
606
|
if hasattr(self._client, "base_url") and self._client.base_url:
|
|
541
|
-
|
|
607
|
+
kwargs["api_base"] = str(self._client.base_url)
|
|
542
608
|
if hasattr(self._client, "api_key") and self._client.api_key:
|
|
543
|
-
|
|
609
|
+
kwargs["api_key"] = self._client.api_key
|
|
544
610
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
any_llm_kwargs["timeout"] = self._client.timeout
|
|
611
|
+
if hasattr(self._client, "timeout") and "timeout" not in kwargs:
|
|
612
|
+
kwargs["timeout"] = self._client.timeout
|
|
548
613
|
|
|
549
614
|
try:
|
|
550
|
-
return
|
|
615
|
+
return litellm.completion(model=full_model, messages=messages, **kwargs)
|
|
551
616
|
except Exception as e:
|
|
552
|
-
logger.error(f"
|
|
617
|
+
logger.error(f"litellm call failed: {e}")
|
|
553
618
|
raise
|
|
554
619
|
|
|
555
620
|
# ── Helper Methods ──────────────────────────────────────────────────────
|
|
@@ -557,7 +622,6 @@ class TheseusWrapper:
|
|
|
557
622
|
def _compose_system_prompt(self, extra: str | None) -> str:
|
|
558
623
|
kernel_injection = self._kernel.get_system_prompt_injection()
|
|
559
624
|
|
|
560
|
-
# If this kernel contains propagated constraints, add a specific header
|
|
561
625
|
if any(a.id.startswith("propagated_") for a in self._kernel.anchors):
|
|
562
626
|
kernel_injection = (
|
|
563
627
|
"# Mother Agent Constraints\n"
|
|
@@ -573,13 +637,11 @@ class TheseusWrapper:
|
|
|
573
637
|
def propagate_headers(self) -> dict[str, str]:
|
|
574
638
|
"""
|
|
575
639
|
Returns a dictionary of headers to be used when calling sub-agents.
|
|
576
|
-
Includes the current kernel serialized for propagation.
|
|
577
640
|
"""
|
|
578
641
|
import json
|
|
579
642
|
import base64
|
|
580
643
|
|
|
581
644
|
kernel_data = self._kernel.to_dict()
|
|
582
|
-
# Mark as read-only for sub-agents
|
|
583
645
|
kernel_data["is_readonly"] = True
|
|
584
646
|
|
|
585
647
|
encoded_kernel = base64.b64encode(json.dumps(kernel_data).encode()).decode()
|
|
@@ -603,25 +665,28 @@ class TheseusWrapper:
|
|
|
603
665
|
return messages
|
|
604
666
|
|
|
605
667
|
def _extract_content(self, raw: Any) -> str:
|
|
606
|
-
# Handle dict responses (e.g. from some mockers or raw APIs)
|
|
607
668
|
if isinstance(raw, dict):
|
|
608
669
|
if "choices" in raw and isinstance(raw["choices"], list) and raw["choices"]:
|
|
609
|
-
# OpenAI dict format
|
|
610
670
|
return raw["choices"][0].get("message", {}).get("content", "") or ""
|
|
611
671
|
if "message" in raw:
|
|
612
|
-
# Ollama dict format
|
|
613
672
|
return raw["message"].get("content", "") or ""
|
|
614
673
|
return str(raw)
|
|
615
674
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
675
|
+
try:
|
|
676
|
+
if hasattr(raw, "choices") and raw.choices:
|
|
677
|
+
choice = raw.choices[0]
|
|
678
|
+
if hasattr(choice, "message") and choice.message:
|
|
679
|
+
return choice.message.content or ""
|
|
680
|
+
if hasattr(choice, "text"):
|
|
681
|
+
return choice.text or ""
|
|
682
|
+
except Exception:
|
|
683
|
+
pass
|
|
684
|
+
|
|
620
685
|
if hasattr(raw, "content") and isinstance(raw.content, list):
|
|
621
|
-
|
|
622
|
-
# Ollama
|
|
686
|
+
return raw.content[0].text if raw.content else ""
|
|
623
687
|
if hasattr(raw, "message"):
|
|
624
|
-
|
|
688
|
+
return getattr(raw.message, "content", "") or ""
|
|
689
|
+
|
|
625
690
|
return str(raw)
|
|
626
691
|
|
|
627
692
|
def _extract_model(self, raw: Any, fallback: str | None) -> str:
|
|
@@ -632,7 +697,6 @@ class TheseusWrapper:
|
|
|
632
697
|
return fallback or "unknown"
|
|
633
698
|
|
|
634
699
|
def _run_divergence_engine(self, message: str, response: str, interaction_count: int = 0, skip_l3: bool = False) -> Any:
|
|
635
|
-
"""Runs the staged divergence engine (L1 -> L2 -> L3)."""
|
|
636
700
|
try:
|
|
637
701
|
return self._divergence_engine.analyze(message, response, interaction_count, skip_l3=skip_l3)
|
|
638
702
|
except Exception as e:
|
|
@@ -646,10 +710,6 @@ class TheseusWrapper:
|
|
|
646
710
|
# ── Kernel Management ────────────────────────────────────────────────────────
|
|
647
711
|
|
|
648
712
|
def validate_user_rule(self, rule_text: str) -> None:
|
|
649
|
-
"""
|
|
650
|
-
Validates a user-defined rule against the kernel.
|
|
651
|
-
Raises an exception if there is a conflict.
|
|
652
|
-
"""
|
|
653
713
|
self._kernel.validate_user_rule(rule_text)
|
|
654
714
|
|
|
655
715
|
def endorse_rule(
|
|
@@ -661,15 +721,7 @@ class TheseusWrapper:
|
|
|
661
721
|
) -> Any:
|
|
662
722
|
"""
|
|
663
723
|
Validates the rule; initiates the Reflective Endorsement
|
|
664
|
-
flow if there is a plastic conflict.
|
|
665
|
-
|
|
666
|
-
Args:
|
|
667
|
-
rule_text: The rule to be enforced
|
|
668
|
-
operator_id: The identity of the person granting approval
|
|
669
|
-
approval_channel: Custom approval channel (default: CLI)
|
|
670
|
-
commitment_new_value: New value to assign to the commitment
|
|
671
|
-
|
|
672
|
-
Returns: EndorsementRecord
|
|
724
|
+
flow if there is a plastic conflict.
|
|
673
725
|
"""
|
|
674
726
|
from ct_toolkit.endorsement.reflective import ReflectiveEndorsement
|
|
675
727
|
|
|
@@ -677,6 +729,11 @@ class TheseusWrapper:
|
|
|
677
729
|
kernel=self._kernel,
|
|
678
730
|
provenance_log=self._provenance_log,
|
|
679
731
|
approval_channel=approval_channel,
|
|
732
|
+
staged_manager=self._staged_manager,
|
|
733
|
+
cooldown_base_s=self._config.endorsement_cooldown_base,
|
|
734
|
+
cooldown_max_s=self._config.endorsement_cooldown_max,
|
|
735
|
+
no_probe_penalty_s=self._config.endorsement_no_probe_penalty,
|
|
736
|
+
template=self._config.template,
|
|
680
737
|
)
|
|
681
738
|
return re.validate_and_endorse(
|
|
682
739
|
rule_text=rule_text,
|
|
@@ -685,18 +742,6 @@ class TheseusWrapper:
|
|
|
685
742
|
)
|
|
686
743
|
|
|
687
744
|
def export_provenance_log(self) -> list[dict[str, Any]]:
|
|
688
|
-
"""
|
|
689
|
-
Exports the entire verified provenance log.
|
|
690
|
-
|
|
691
|
-
Verifies the integrity of the log before returning the data.
|
|
692
|
-
This is intended for external auditing and analysis.
|
|
693
|
-
|
|
694
|
-
Returns:
|
|
695
|
-
A list of all log entries as dictionaries.
|
|
696
|
-
|
|
697
|
-
Raises:
|
|
698
|
-
ChainIntegrityError: If the log's hash chain is broken.
|
|
699
|
-
"""
|
|
700
745
|
return self._provenance_log.export_log()
|
|
701
746
|
|
|
702
747
|
@property
|
|
@@ -711,6 +756,10 @@ class TheseusWrapper:
|
|
|
711
756
|
def divergence_engine(self) -> Any:
|
|
712
757
|
return self._divergence_engine
|
|
713
758
|
|
|
759
|
+
@property
|
|
760
|
+
def staged_manager(self) -> StagedUpdateManager:
|
|
761
|
+
return self._staged_manager
|
|
762
|
+
|
|
714
763
|
def __repr__(self) -> str:
|
|
715
764
|
return (
|
|
716
765
|
f"TheseusWrapper("
|