ct-toolkit 0.2.6__tar.gz → 0.2.9__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.
Files changed (51) hide show
  1. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/PKG-INFO +1 -1
  2. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/core/exceptions.py +15 -0
  3. ct_toolkit-0.2.9/ct_toolkit/core/integrity.py +56 -0
  4. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/core/wrapper.py +98 -5
  5. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/divergence/engine.py +4 -0
  6. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/divergence/l2_judge.py +20 -9
  7. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/divergence/l3_icm.py +18 -7
  8. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/identity/embedding.py +38 -22
  9. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit.egg-info/PKG-INFO +1 -1
  10. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit.egg-info/SOURCES.txt +1 -0
  11. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/pyproject.toml +7 -1
  12. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/LICENSE +0 -0
  13. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/README.md +0 -0
  14. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/__init__.py +0 -0
  15. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/core/__init__.py +0 -0
  16. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/core/compatibility.py +0 -0
  17. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/core/kernel.py +0 -0
  18. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/divergence/__init__.py +0 -0
  19. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/divergence/analysis.py +0 -0
  20. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/divergence/loss.py +0 -0
  21. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/divergence/scheduler.py +0 -0
  22. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/endorsement/__init__.py +0 -0
  23. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/endorsement/probes/__init__.py +0 -0
  24. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/endorsement/reflective.py +0 -0
  25. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/identity/__init__.py +0 -0
  26. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/identity/templates/__init__.py +0 -0
  27. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/identity/templates/defense.yaml +0 -0
  28. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/identity/templates/finance.yaml +0 -0
  29. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/identity/templates/general.yaml +0 -0
  30. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/identity/templates/medical.yaml +0 -0
  31. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/kernels/__init__.py +0 -0
  32. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/kernels/default.yaml +0 -0
  33. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/kernels/defense.yaml +0 -0
  34. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/middleware/__init__.py +0 -0
  35. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/middleware/autogen.py +0 -0
  36. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/middleware/crewai.py +0 -0
  37. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/middleware/deepagents.py +0 -0
  38. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/middleware/langchain.py +0 -0
  39. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/provenance/__init__.py +0 -0
  40. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/provenance/log.py +0 -0
  41. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/provenance/vault/__init__.py +0 -0
  42. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/utils/__init__.py +0 -0
  43. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit/utils/logger.py +0 -0
  44. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit.egg-info/dependency_links.txt +0 -0
  45. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit.egg-info/requires.txt +0 -0
  46. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/ct_toolkit.egg-info/top_level.txt +0 -0
  47. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/setup.cfg +0 -0
  48. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/tests/test_analysis.py +0 -0
  49. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/tests/test_deep_agents_integration.py +0 -0
  50. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/tests/test_loss.py +0 -0
  51. {ct_toolkit-0.2.6 → ct_toolkit-0.2.9}/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.2.6
3
+ Version: 0.2.9
4
4
  Summary: Computational Theseus Toolkit — Identity Continuity Guardrails for Agentic Systems
5
5
  Author: Hakan Damar
6
6
  License: Apache-2.0
@@ -103,3 +103,18 @@ class ChainIntegrityError(ProvenanceError):
103
103
 
104
104
  class VaultError(ProvenanceError):
105
105
  """Vault connection or authorization error."""
106
+
107
+
108
+ # ── File Integrity Errors ────────────────────────────────────────────────────
109
+
110
+ class ConfigurationTamperingError(CTToolkitError):
111
+ """
112
+ Raised when a configuration file has been modified after initialization.
113
+ Indicates a potential security breach or unintended change.
114
+ """
115
+ def __init__(self, file_path: str):
116
+ self.file_path = file_path
117
+ super().__init__(
118
+ f"SECURITY ALERT: Tampering detected in configuration file: {file_path}. "
119
+ "Execution halted to prevent security bypass."
120
+ )
@@ -0,0 +1,56 @@
1
+ """
2
+ ct_toolkit.core.integrity
3
+ -------------------------
4
+ Cryptographic integrity monitoring for configuration files.
5
+ """
6
+ import hashlib
7
+ from pathlib import Path
8
+ from ct_toolkit.utils.logger import get_logger
9
+ from ct_toolkit.core.exceptions import ConfigurationTamperingError
10
+
11
+ logger = get_logger(__name__)
12
+
13
+ class IntegrityMonitor:
14
+ """
15
+ Monitors the cryptographic integrity of CT configuration files.
16
+ Prevents runtime tampering by agents or malicious actors.
17
+ """
18
+ def __init__(self):
19
+ self._file_hashes: dict[str, str] = {}
20
+
21
+ def _calculate_hash(self, file_path: Path) -> str:
22
+ """Calculates the SHA-256 hash of a given file."""
23
+ hasher = hashlib.sha256()
24
+ try:
25
+ with open(file_path, 'rb') as f:
26
+ buf = f.read()
27
+ hasher.update(buf)
28
+ return hasher.hexdigest()
29
+ except FileNotFoundError:
30
+ logger.error(f"Integrity check failed: File not found at {file_path}")
31
+ raise
32
+
33
+ def register_file(self, file_path: Path) -> None:
34
+ """Registers a file and stores its initial hash."""
35
+ if not file_path.is_file():
36
+ logger.warning(f"Integrity check: Cannot register non-file path: {file_path}")
37
+ return
38
+
39
+ file_hash = self._calculate_hash(file_path)
40
+ self._file_hashes[str(file_path)] = file_hash
41
+ logger.debug(f"Registered integrity hash for {file_path.name}")
42
+
43
+ def verify_integrity(self) -> None:
44
+ """
45
+ Verifies that none of the registered files have been modified.
46
+ Raises ConfigurationTamperingError if a mismatch is found.
47
+ """
48
+ if not self._file_hashes:
49
+ return # Nothing to verify
50
+
51
+ for path_str, original_hash in self._file_hashes.items():
52
+ current_hash = self._calculate_hash(Path(path_str))
53
+ if current_hash != original_hash:
54
+ raise ConfigurationTamperingError(path_str)
55
+ logger.debug("Integrity verification passed for all registered files.")
56
+
@@ -21,15 +21,18 @@ from __future__ import annotations
21
21
 
22
22
  import time
23
23
  import hashlib
24
+ import os
24
25
  from dataclasses import dataclass, field
25
26
  from pathlib import Path
26
27
  from typing import Any, Iterator
27
28
  import any_llm
28
29
  from any_llm import AnyLLM
30
+ import glob
29
31
 
30
32
  from ct_toolkit.core.kernel import ConstitutionalKernel
31
33
  from ct_toolkit.core.compatibility import CompatibilityLayer, CompatibilityResult
32
34
  from ct_toolkit.core.exceptions import CTToolkitError, MissingClientError
35
+ from ct_toolkit.core.integrity import IntegrityMonitor
33
36
  from ct_toolkit.divergence.scheduler import RiskProfile
34
37
  from ct_toolkit.utils.logger import get_logger
35
38
 
@@ -39,6 +42,7 @@ logger = get_logger(__name__)
39
42
  @dataclass
40
43
  class WrapperConfig:
41
44
  """TheseusWrapper configuration."""
45
+ project_root: str | Path | None = None
42
46
  kernel_path: str | Path | None = None
43
47
  template: str = "general"
44
48
  kernel_name: str = "default"
@@ -108,6 +112,7 @@ class TheseusWrapper:
108
112
  kernel_path: str | Path | None = None,
109
113
  template: str = "general",
110
114
  kernel_name: str = "default",
115
+ project_root: str | Path | None = None,
111
116
  ) -> None:
112
117
  """
113
118
  Initializes TheseusWrapper.
@@ -118,11 +123,14 @@ class TheseusWrapper:
118
123
  config: Optional WrapperConfig instance.
119
124
  provider: Provider name (e.g. "openai", "anthropic", "ollama", "google", "cohere").
120
125
  If provided, 'any-llm' will be used to initialize the client internally.
126
+ project_root: Path to the user's project root directory. Used to find a custom 'config/' folder.
127
+ Defaults to the current working directory.
121
128
  """
122
129
  self._config = config or WrapperConfig(
123
130
  kernel_path=kernel_path,
124
131
  template=template,
125
132
  kernel_name=kernel_name,
133
+ project_root=project_root,
126
134
  )
127
135
 
128
136
  if client is None and provider is None:
@@ -131,6 +139,11 @@ class TheseusWrapper:
131
139
 
132
140
  self._client = client
133
141
  self._provider = provider or self._detect_provider(client)
142
+ self._project_root = (
143
+ Path(self._config.project_root)
144
+ if self._config.project_root
145
+ else Path(os.getcwd())
146
+ )
134
147
  self._kernel = self._load_kernel()
135
148
 
136
149
  # If a parent kernel exists, merge it into our own kernel as axiomatic constraints
@@ -146,8 +159,57 @@ class TheseusWrapper:
146
159
  self._divergence_engine = self._init_divergence_engine()
147
160
  self._last_model: str = "unknown"
148
161
 
162
+ # ── Integrity Monitoring ──
163
+ self._integrity_monitor = IntegrityMonitor()
164
+ self._register_monitored_files()
165
+
149
166
  self._log_init()
150
167
 
168
+ def _register_monitored_files(self):
169
+ """
170
+ Finds and registers all critical configuration files for integrity monitoring,
171
+ including both built-in package files and user-provided custom configs.
172
+ """
173
+ logger.debug("Registering configuration files for integrity monitoring.")
174
+
175
+ # 1. Scan built-in package files
176
+ try:
177
+ from importlib.resources import files
178
+ package_root = files("ct_toolkit")
179
+
180
+ internal_patterns = [
181
+ "kernels/**/*.yaml",
182
+ "identity/templates/**/*.yaml",
183
+ "endorsement/probes/**/*.json",
184
+ ]
185
+
186
+ for pattern in internal_patterns:
187
+ for file_path in package_root.rglob(pattern):
188
+ if file_path.is_file():
189
+ self._integrity_monitor.register_file(file_path)
190
+
191
+ except (ImportError, ModuleNotFoundError):
192
+ # Fallback for older Python or different environments
193
+ package_root = Path(__file__).parent.parent
194
+ # This is less ideal as it might find non-package files, but it's a
195
+ # reasonable fallback for development environments.
196
+ for pattern in internal_patterns:
197
+ for file_path in package_root.rglob(pattern):
198
+ if file_path.is_file():
199
+ self._integrity_monitor.register_file(file_path)
200
+
201
+ # 2. Scan user's custom config directory
202
+ if self._project_root:
203
+ user_config_dir = self._project_root / "config"
204
+ if user_config_dir.is_dir():
205
+ logger.debug(f"Scanning user config directory for integrity monitoring: {user_config_dir}")
206
+ user_patterns = ["*_kernel.yaml", "*_identity.yaml", "*_probes.json", "*.yaml", "*.json"]
207
+ for pattern in user_patterns:
208
+ for file_path in user_config_dir.rglob(pattern):
209
+ if file_path.is_file():
210
+ self._integrity_monitor.register_file(file_path)
211
+
212
+
151
213
  # ── Factory / Init ─────────────────────────────────────────────────────────
152
214
 
153
215
  def _detect_provider(self, client: Any) -> str:
@@ -180,30 +242,40 @@ class TheseusWrapper:
180
242
  return "unknown"
181
243
 
182
244
  def _load_kernel(self) -> ConstitutionalKernel:
245
+ # 1. Check for explicit path
183
246
  if self._config.kernel_path:
247
+ logger.debug(f"Loading kernel from explicit path: {self._config.kernel_path}")
184
248
  return ConstitutionalKernel.from_yaml(self._config.kernel_path)
185
-
186
- import os
249
+
187
250
  safe_name = os.path.basename(self._config.kernel_name)
188
251
 
189
- # Load built-in kernel using importlib.resources
252
+ # 2. Check for user-defined config directory
253
+ if self._project_root:
254
+ user_config_path = self._project_root / "config" / f"{safe_name}.yaml"
255
+ if user_config_path.exists():
256
+ logger.debug(f"Loading kernel from user config: {user_config_path}")
257
+ return ConstitutionalKernel.from_yaml(user_config_path)
258
+
259
+ # 3. Load built-in kernel using importlib.resources
190
260
  try:
191
261
  from importlib.resources import files
192
262
  kernel_resource = files("ct_toolkit.kernels").joinpath(f"{safe_name}.yaml")
193
263
  if kernel_resource.is_file():
264
+ logger.debug(f"Loading built-in kernel: {safe_name}.yaml")
194
265
  return ConstitutionalKernel.from_yaml(kernel_resource)
195
266
  except (ImportError, FileNotFoundError):
196
- # Fallback
267
+ # Fallback for older python versions
197
268
  kernel_path = (
198
269
  Path(__file__).parent.parent
199
270
  / "kernels"
200
271
  / f"{safe_name}.yaml"
201
272
  )
202
273
  if kernel_path.exists():
274
+ logger.debug(f"Loading built-in kernel (fallback): {kernel_path}")
203
275
  return ConstitutionalKernel.from_yaml(kernel_path)
204
276
 
205
277
  logger.warning(
206
- f"Kernel '{self._config.kernel_name}' not found, using default."
278
+ f"Kernel '{self._config.kernel_name}' not found in user config or built-ins. Using default."
207
279
  )
208
280
  return ConstitutionalKernel.default()
209
281
 
@@ -232,6 +304,7 @@ class TheseusWrapper:
232
304
  template=self._config.template,
233
305
  embedding_client=emb_client,
234
306
  embedding_model=self._config.embedding_model,
307
+ project_root=self._project_root,
235
308
  )
236
309
 
237
310
  def _init_divergence_engine(self) -> Any:
@@ -263,6 +336,7 @@ class TheseusWrapper:
263
336
  l3_threshold=self._config.divergence_l3_threshold,
264
337
  enterprise_mode=self._config.enterprise_mode,
265
338
  scheduler=scheduler,
339
+ project_root=self._project_root,
266
340
  provenance_log=self._provenance_log,
267
341
  )
268
342
 
@@ -299,6 +373,9 @@ class TheseusWrapper:
299
373
  system: Additional system prompt (appended to kernel)
300
374
  history: Conversation history [{"role": ..., "content": ...}]
301
375
  """
376
+ # --- Pre-flight Checks ---
377
+ self._integrity_monitor.verify_integrity()
378
+
302
379
  composed_system = self._compose_system_prompt(system)
303
380
  messages = self._build_messages(message, history, composed_system)
304
381
 
@@ -416,6 +493,10 @@ class TheseusWrapper:
416
493
  if hasattr(self._client, "api_key") and self._client.api_key:
417
494
  any_llm_kwargs["api_key"] = self._client.api_key
418
495
 
496
+ # Transfer common client configurations if available
497
+ if hasattr(self._client, "timeout") and "timeout" not in any_llm_kwargs:
498
+ any_llm_kwargs["timeout"] = self._client.timeout
499
+
419
500
  try:
420
501
  return any_llm.completion(**any_llm_kwargs)
421
502
  except Exception as e:
@@ -473,6 +554,16 @@ class TheseusWrapper:
473
554
  return messages
474
555
 
475
556
  def _extract_content(self, raw: Any) -> str:
557
+ # Handle dict responses (e.g. from some mockers or raw APIs)
558
+ if isinstance(raw, dict):
559
+ if "choices" in raw and isinstance(raw["choices"], list) and raw["choices"]:
560
+ # OpenAI dict format
561
+ return raw["choices"][0].get("message", {}).get("content", "") or ""
562
+ if "message" in raw:
563
+ # Ollama dict format
564
+ return raw["message"].get("content", "") or ""
565
+ return str(raw)
566
+
476
567
  # OpenAI
477
568
  if hasattr(raw, "choices"):
478
569
  return raw.choices[0].message.content or ""
@@ -485,6 +576,8 @@ class TheseusWrapper:
485
576
  return str(raw)
486
577
 
487
578
  def _extract_model(self, raw: Any, fallback: str | None) -> str:
579
+ if isinstance(raw, dict) and "model" in raw:
580
+ return raw["model"]
488
581
  if hasattr(raw, "model"):
489
582
  return raw.model
490
583
  return fallback or "unknown"
@@ -17,6 +17,7 @@ from __future__ import annotations
17
17
  import time
18
18
  from dataclasses import dataclass
19
19
  from enum import Enum
20
+ from pathlib import Path
20
21
  from typing import Any
21
22
 
22
23
  from ct_toolkit.identity.embedding import IdentityEmbeddingLayer
@@ -116,6 +117,7 @@ class DivergenceEngine:
116
117
  enterprise_mode: bool = False, # Run all tiers all the time
117
118
  scheduler: Any | None = None, # ElasticityScheduler instance
118
119
  provenance_log: Any | None = None,
120
+ project_root: Path | None = None,
119
121
  ) -> None:
120
122
  self._identity = identity_layer
121
123
  self._kernel = kernel
@@ -123,6 +125,7 @@ class DivergenceEngine:
123
125
  self._provider = provider
124
126
  self._judge_client = judge_client
125
127
  self._log = provenance_log
128
+ self._project_root = project_root
126
129
 
127
130
  # Base thresholds
128
131
  self._base_l1_threshold = l1_threshold
@@ -331,5 +334,6 @@ class DivergenceEngine:
331
334
  kernel=self._kernel,
332
335
  template=self._template,
333
336
  max_probes=max_probes,
337
+ project_root=self._project_root,
334
338
  )
335
339
  return runner.run()
@@ -139,15 +139,26 @@ class LLMJudge:
139
139
 
140
140
  try:
141
141
  # Call using instructor for structured data
142
- data: JudgeResponse = self._instructor_client.chat.completions.create(
143
- model=self._model,
144
- response_model=JudgeResponse,
145
- messages=[
146
- {"role": "system", "content": system_prompt},
147
- {"role": "user", "content": user_prompt},
148
- ],
149
- max_retries=2
150
- )
142
+ if self._provider == "anthropic":
143
+ data: JudgeResponse = self._instructor_client.messages.create(
144
+ model=self._model,
145
+ response_model=JudgeResponse,
146
+ system=system_prompt,
147
+ messages=[
148
+ {"role": "user", "content": user_prompt},
149
+ ],
150
+ max_retries=2
151
+ )
152
+ else:
153
+ data: JudgeResponse = self._instructor_client.chat.completions.create(
154
+ model=self._model,
155
+ response_model=JudgeResponse,
156
+ messages=[
157
+ {"role": "system", "content": system_prompt},
158
+ {"role": "user", "content": user_prompt},
159
+ ],
160
+ max_retries=2
161
+ )
151
162
 
152
163
  result = JudgeResult(
153
164
  verdict=data.verdict,
@@ -209,6 +209,7 @@ class ICMRunner:
209
209
  model: str | None = None,
210
210
  include_domain_probes: bool = True,
211
211
  max_probes: int | None = None,
212
+ project_root: Path | None = None,
212
213
  ) -> None:
213
214
  self._client = client
214
215
  self._provider = provider
@@ -216,6 +217,7 @@ class ICMRunner:
216
217
  self._template = template
217
218
  self._model = model or self._default_model(provider)
218
219
  self._include_domain = include_domain_probes
220
+ self._project_root = project_root
219
221
  self._max_probes = max_probes
220
222
 
221
223
  @staticmethod
@@ -332,13 +334,22 @@ class ICMRunner:
332
334
 
333
335
  # Domain probes — based on template
334
336
  if self._include_domain:
335
- domain_path = (
336
- self.PROBES_DIR / "domain_probes" / f"{self._template}_probes.json"
337
- )
338
- if domain_path.exists():
339
- with open(domain_path, encoding="utf-8") as f:
340
- probes.extend(json.load(f))
341
- logger.info(f"Domain probes loaded: {self._template}")
337
+ # Check user config first
338
+ domain_loaded = False
339
+ if self._project_root:
340
+ user_probe_path = self._project_root / "config" / f"{self._template}_probes.json"
341
+ if user_probe_path.exists():
342
+ with open(user_probe_path, encoding="utf-8") as f:
343
+ probes.extend(json.load(f))
344
+ logger.info(f"Custom domain probes loaded: {user_probe_path}")
345
+ domain_loaded = True
346
+
347
+ if not domain_loaded:
348
+ domain_path = self.PROBES_DIR / "domain_probes" / f"{self._template}_probes.json"
349
+ if domain_path.exists():
350
+ with open(domain_path, encoding="utf-8") as f:
351
+ probes.extend(json.load(f))
352
+ logger.info(f"Domain probes loaded: {self._template}")
342
353
 
343
354
  if not probes:
344
355
  logger.warning("Probe file not found. Empty battery.")
@@ -43,6 +43,7 @@ class IdentityEmbeddingLayer:
43
43
  template: str = "general",
44
44
  embedding_client: Any = None, # Provider client for real embeddings
45
45
  embedding_model: str = "text-embedding-3-small",
46
+ project_root: Path | None = None,
46
47
  ) -> None:
47
48
  self._template = template
48
49
  self._embedding_client = embedding_client
@@ -50,6 +51,7 @@ class IdentityEmbeddingLayer:
50
51
  self._reference_vector: np.ndarray | None = None
51
52
  self._template_keywords: list[str] = []
52
53
  self._reference_text: str = "" # stored for lazy computation
54
+ self._project_root = project_root
53
55
  self._embedding_method: str = "uninitialized"
54
56
  self._load_template()
55
57
 
@@ -58,34 +60,48 @@ class IdentityEmbeddingLayer:
58
60
  def _load_template(self) -> None:
59
61
  import os
60
62
  import yaml
61
- from importlib.resources import files
63
+
64
+ data = None
62
65
 
63
66
  # Sanitize template name to prevent path traversal
64
67
  safe_template = os.path.basename(self._template)
65
68
 
69
+ # 1. Check user config first if project_root is provided
70
+ if self._project_root:
71
+ # Check for both "name.yaml" and "name_identity.yaml"
72
+ for fname in [f"{safe_template}.yaml", f"{safe_template}_identity.yaml"]:
73
+ user_path = self._project_root / "config" / fname
74
+ if user_path.exists():
75
+ logger.debug(f"Loading identity template from user config: {user_path}")
76
+ with open(user_path, "r", encoding="utf-8") as f:
77
+ data = yaml.safe_load(f)
78
+ break
79
+
66
80
  # Load built-in template using importlib.resources
67
- try:
68
- template_resource = files("ct_toolkit.identity.templates").joinpath(f"{safe_template}.yaml")
69
- if template_resource.is_file():
70
- with template_resource.open("r", encoding="utf-8") as f:
71
- data = yaml.safe_load(f)
72
- else:
73
- raise FileNotFoundError(f"Template resource not found: {template_resource}")
74
- except (ImportError, FileNotFoundError):
75
- # Fallback for development or older environments
76
- template_path = (
77
- Path(__file__).parent
78
- / "templates"
79
- / f"{safe_template}.yaml"
80
- )
81
- if not template_path.exists():
82
- logger.warning(
83
- f"Template '{self._template}' not found, using 'general'."
81
+ if data is None:
82
+ try:
83
+ from importlib.resources import files
84
+ template_resource = files("ct_toolkit.identity.templates").joinpath(f"{safe_template}.yaml")
85
+ if template_resource.is_file():
86
+ with template_resource.open("r", encoding="utf-8") as f:
87
+ data = yaml.safe_load(f)
88
+ else:
89
+ raise FileNotFoundError(f"Template resource not found: {template_resource}")
90
+ except (ImportError, FileNotFoundError):
91
+ # Fallback for development or older environments
92
+ template_path = (
93
+ Path(__file__).parent
94
+ / "templates"
95
+ / f"{safe_template}.yaml"
84
96
  )
85
- template_path = Path(__file__).parent / "templates" / "general.yaml"
86
-
87
- with open(template_path, "r", encoding="utf-8") as f:
88
- data = yaml.safe_load(f)
97
+ if not template_path.exists():
98
+ logger.warning(
99
+ f"Template '{self._template}' not found, using 'general'."
100
+ )
101
+ template_path = Path(__file__).parent / "templates" / "general.yaml"
102
+
103
+ with open(template_path, "r", encoding="utf-8") as f:
104
+ data = yaml.safe_load(f)
89
105
 
90
106
  self._template_keywords = data.get("identity_keywords", [])
91
107
  self._reference_text = data.get("reference_text", " ".join(self._template_keywords))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ct-toolkit
3
- Version: 0.2.6
3
+ Version: 0.2.9
4
4
  Summary: Computational Theseus Toolkit — Identity Continuity Guardrails for Agentic Systems
5
5
  Author: Hakan Damar
6
6
  License: Apache-2.0
@@ -10,6 +10,7 @@ ct_toolkit.egg-info/top_level.txt
10
10
  ct_toolkit/core/__init__.py
11
11
  ct_toolkit/core/compatibility.py
12
12
  ct_toolkit/core/exceptions.py
13
+ ct_toolkit/core/integrity.py
13
14
  ct_toolkit/core/kernel.py
14
15
  ct_toolkit/core/wrapper.py
15
16
  ct_toolkit/divergence/__init__.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ct-toolkit"
7
- version = "0.2.6"
7
+ version = "0.2.9"
8
8
  description = "Computational Theseus Toolkit — Identity Continuity Guardrails for Agentic Systems"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -71,3 +71,9 @@ dev = [
71
71
  "pytest-cov>=7.0.0",
72
72
  "twine>=6.2.0",
73
73
  ]
74
+
75
+ [tool.semantic_release]
76
+ version_toml = ["pyproject.toml:project.version"]
77
+ branch = "main"
78
+ changelog_file = "CHANGELOG.md"
79
+ build_command = "pip install build && python -m build"
File without changes
File without changes
File without changes