differix 0.1.0a1__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.
Files changed (62) hide show
  1. agents/editor.py +455 -0
  2. agents/researcher.py +405 -0
  3. agents/reviewer.py +374 -0
  4. cli/model_cli.py +430 -0
  5. config.py +557 -0
  6. differix-0.1.0a1.dist-info/METADATA +123 -0
  7. differix-0.1.0a1.dist-info/RECORD +62 -0
  8. differix-0.1.0a1.dist-info/WHEEL +5 -0
  9. differix-0.1.0a1.dist-info/entry_points.txt +2 -0
  10. differix-0.1.0a1.dist-info/top_level.txt +6 -0
  11. main.py +2796 -0
  12. memory/__init__.py +59 -0
  13. memory/embeddings/__init__.py +29 -0
  14. memory/embeddings/factory.py +178 -0
  15. memory/embeddings/providers.py +259 -0
  16. memory/manager.py +186 -0
  17. memory/migrator.py +253 -0
  18. memory/ranker.py +203 -0
  19. memory/schemas.py +127 -0
  20. memory/storage.py +552 -0
  21. tools/agents_chat.py +152 -0
  22. tools/asset_extractor.py +545 -0
  23. tools/asset_manager.py +115 -0
  24. tools/atomic_changeset.py +264 -0
  25. tools/backup_manager.py +122 -0
  26. tools/changelog_engine.py +261 -0
  27. tools/coherence_checker.py +771 -0
  28. tools/compiler.py +531 -0
  29. tools/component_normalizer.py +130 -0
  30. tools/context_assembler.py +631 -0
  31. tools/contract_validator.py +478 -0
  32. tools/cost_predictor.py +141 -0
  33. tools/drift_guard.py +196 -0
  34. tools/file_manager.py +119 -0
  35. tools/handshake.py +351 -0
  36. tools/lesson_manager.py +570 -0
  37. tools/lesson_memory_bridge.py +398 -0
  38. tools/llm_client.py +103 -0
  39. tools/llm_selector.py +362 -0
  40. tools/model_ranker.py +535 -0
  41. tools/palette_extractor.py +1043 -0
  42. tools/plugin_manager.py +204 -0
  43. tools/project_context_gatherer.py +404 -0
  44. tools/project_scanner.py +41 -0
  45. tools/project_structure.py +244 -0
  46. tools/reasoning_logger.py +78 -0
  47. tools/reflection.py +452 -0
  48. tools/sandbox_utils.py +56 -0
  49. tools/security/__init__.py +0 -0
  50. tools/security/behavior_guard.py +83 -0
  51. tools/security/verification_firewall.py +113 -0
  52. tools/security_filter.py +213 -0
  53. tools/session_adr.py +311 -0
  54. tools/skill_manager.py +129 -0
  55. tools/state_manager.py +383 -0
  56. tools/structure_index.py +570 -0
  57. tools/test_schema_consistency.py +19 -0
  58. tools/verification_firewall.py +113 -0
  59. tools/visual_capture.py +65 -0
  60. tools/visual_diff.py +220 -0
  61. tools/visual_reviewer.py +79 -0
  62. tools/web_search.py +371 -0
config.py ADDED
@@ -0,0 +1,557 @@
1
+ import os
2
+ import json
3
+ import re
4
+ from pathlib import Path
5
+
6
+ # ---------------------------------------------------------------------------
7
+ # Hardened secret / environment loading
8
+ # ---------------------------------------------------------------------------
9
+ # Only environment variable names matching this pattern may be set from a
10
+ # .env file. This blocks injection of arbitrary (or framework-critical)
11
+ # variables such as PYTHONPATH, LD_PRELOAD, or PATH via a malicious .env.
12
+ _ENV_NAME_RE = re.compile(r"^[A-Z][A-Z0-9_]{0,63}$")
13
+
14
+ # Values that must never be overridden from a .env file.
15
+ _PROTECTED_ENV_NAMES = frozenset({
16
+ "PATH", "PYTHONPATH", "PYTHONHOME", "LD_PRELOAD", "LD_LIBRARY_PATH",
17
+ "SHELL", "HOME", "USER", "IFS", "PS1",
18
+ })
19
+
20
+ # Substrings that mark a value as a secret for redaction purposes.
21
+ _SECRET_HINTS = ("KEY", "TOKEN", "SECRET", "PASSWORD", "CREDENTIAL")
22
+
23
+
24
+ def _is_secret_name(name: str) -> bool:
25
+ return any(hint in name.upper() for hint in _SECRET_HINTS)
26
+
27
+
28
+ def _redact(name: str, value: str) -> str:
29
+ """Return a redacted representation of a secret value for logs/errors."""
30
+ if not _is_secret_name(name) or not value:
31
+ return "<unset>" if not value else value
32
+ if len(value) <= 8:
33
+ return "***"
34
+ return value[:4] + "***" + value[-4:]
35
+
36
+
37
+ def redact_secret(name: str, value: str) -> str:
38
+ """Public redaction helper: safe to call on any name/value pair."""
39
+ return _redact(name, value)
40
+
41
+
42
+ def redact_mapping(mapping: dict) -> dict:
43
+ """
44
+ Return a copy of a mapping with all secret-looking values redacted.
45
+
46
+ Useful for logging environment snapshots, request payloads, or debug
47
+ dumps without leaking credentials.
48
+ """
49
+ if not isinstance(mapping, dict):
50
+ return {}
51
+ return {
52
+ str(k): _redact(str(k), v) if isinstance(v, str) else v
53
+ for k, v in mapping.items()
54
+ }
55
+
56
+
57
+ def safe_log(message: str, **kwargs) -> str:
58
+ """
59
+ Format a log message with redacted keyword arguments.
60
+
61
+ Any kwarg whose name looks secret-like (KEY/TOKEN/SECRET/PASSWORD/
62
+ CREDENTIAL) is redacted before interpolation. Returns the formatted
63
+ string; the caller decides how to emit it.
64
+ """
65
+ safe_kwargs = redact_mapping(kwargs)
66
+ try:
67
+ return message.format(**safe_kwargs)
68
+ except (KeyError, IndexError, ValueError):
69
+ return message
70
+
71
+
72
+ def _safe_load_env_file(env_path: str) -> dict:
73
+ """
74
+ Parse a .env file without eval/exec/shell expansion.
75
+
76
+ Rules:
77
+ - Only KEY=VALUE lines (and # comments) are accepted.
78
+ - KEY must match _ENV_NAME_RE and must not be protected.
79
+ - Values are taken literally; no interpolation, no backtick/$(...)
80
+ expansion, no escape interpretation beyond stripping matching
81
+ surrounding quotes.
82
+ - Malformed lines are skipped (never raise, never log the value).
83
+ """
84
+ loaded = {}
85
+ path = Path(env_path)
86
+ if not path.is_file():
87
+ return loaded
88
+ try:
89
+ text = path.read_text(encoding="utf-8", errors="replace")
90
+ except OSError:
91
+ return loaded
92
+
93
+ for lineno, raw_line in enumerate(text.splitlines(), start=1):
94
+ line = raw_line.strip()
95
+ if not line or line.startswith("#"):
96
+ continue
97
+ if "=" not in line:
98
+ continue
99
+ name, _, value = line.partition("=")
100
+ name = name.strip()
101
+ value = value.strip()
102
+ if not _ENV_NAME_RE.match(name) or name in _PROTECTED_ENV_NAMES:
103
+ continue
104
+ # Reject lines attempting shell-style expansion or command injection.
105
+ lowered = value.lower()
106
+ if any(tok in lowered for tok in ("$(", "`", "eval", "exec", "import ")):
107
+ continue
108
+ # Strip one pair of matching surrounding quotes.
109
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
110
+ value = value[1:-1]
111
+ if not value:
112
+ continue
113
+ loaded[name] = value
114
+ os.environ.setdefault(name, value)
115
+ return loaded
116
+
117
+
118
+ # Load .env from the module directory only (never cwd-relative, which could
119
+ # pick up an attacker-controlled file from a writable directory).
120
+ _ENV_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env")
121
+ _safe_load_env_file(_ENV_FILE)
122
+
123
+
124
+ def _get_secret(name: str) -> str:
125
+ """
126
+ Fetch a secret from the environment with strict validation.
127
+
128
+ Returns an empty string when unset. Never raises with the raw value;
129
+ errors reference the redacted form only.
130
+ """
131
+ value = os.environ.get(name, "")
132
+ if not value:
133
+ return ""
134
+ if len(value) > 512:
135
+ # Implausibly long secrets are rejected outright.
136
+ raise ValueError(
137
+ f"Environment variable {name} has an invalid length "
138
+ f"(>512 chars); refusing to use it."
139
+ )
140
+ if any(ch in value for ch in ("\n", "\r", "\x00")):
141
+ raise ValueError(
142
+ f"Environment variable {name} contains control characters; "
143
+ f"refusing to use it."
144
+ )
145
+ return value
146
+
147
+
148
+ OPENROUTER_API_KEY = _get_secret("OPENROUTER_API_KEY")
149
+ OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
150
+
151
+ # ---------------------------------------------------------------------------
152
+ # Secret-bearing key registry and strict validation rules
153
+ # ---------------------------------------------------------------------------
154
+ # Each entry: kind of secret -> validation rule. Rules are applied by
155
+ # validate_secret() below. A secret that fails validation is never used.
156
+ _SECRET_KEY_RULES = {
157
+ "openrouter_api_key": {
158
+ "env_names": ("OPENROUTER_API_KEY",),
159
+ "min_length": 20,
160
+ "max_length": 512,
161
+ # OpenRouter keys are `sk-or-v1-<hex/base62>`.
162
+ "required_prefix": "sk-or-",
163
+ "charset_re": re.compile(r"^[A-Za-z0-9_\-]+$"),
164
+ },
165
+ }
166
+
167
+ # Flat registry: every env var name that holds a secret, mapped to its rule.
168
+ SECRET_BEARING_KEYS = {
169
+ env_name: rule
170
+ for rule in _SECRET_KEY_RULES.values()
171
+ for env_name in rule["env_names"]
172
+ }
173
+
174
+
175
+ def validate_secret(name: str, value: str) -> str:
176
+ """
177
+ Strictly validate a secret-bearing env var against its registered rule.
178
+
179
+ Returns the value when it conforms. Raises ValueError (message never
180
+ contains the raw value) when the value is malformed, in which case the
181
+ caller must treat the secret as unset/refuse to use it.
182
+ """
183
+ rule = SECRET_BEARING_KEYS.get(name)
184
+ if rule is None:
185
+ # Unknown names have no strict rule; fall back to basic sanity only.
186
+ return value
187
+ if not value:
188
+ return value
189
+ if not (rule["min_length"] <= len(value) <= rule["max_length"]):
190
+ raise ValueError(
191
+ f"Secret '{name}' failed validation: invalid length "
192
+ f"(expected {rule['min_length']}..{rule['max_length']})."
193
+ )
194
+ prefix = rule.get("required_prefix")
195
+ if prefix and not value.startswith(prefix):
196
+ raise ValueError(
197
+ f"Secret '{name}' failed validation: unexpected shape "
198
+ f"(missing required prefix)."
199
+ )
200
+ charset_re = rule.get("charset_re")
201
+ if charset_re is not None and not charset_re.match(value):
202
+ raise ValueError(
203
+ f"Secret '{name}' failed validation: disallowed characters present."
204
+ )
205
+ return value
206
+
207
+
208
+ def validate_env_secret(name: str) -> str:
209
+ """
210
+ Strictly validate a secret-bearing environment variable at load time.
211
+
212
+ Reads the variable from the environment, applies the hardened
213
+ _get_secret() sanity checks (length, control characters), then applies
214
+ the registered strict rule via validate_secret().
215
+
216
+ Returns the validated value, or an empty string when the variable is
217
+ unset. Raises ValueError (message never contains the raw value) when
218
+ the value is malformed — the caller must treat the secret as unset
219
+ and refuse to use it.
220
+ """
221
+ if name not in SECRET_BEARING_KEYS:
222
+ # Not a registered secret-bearing key; nothing strict to enforce.
223
+ return os.environ.get(name, "")
224
+ value = _get_secret(name)
225
+ return validate_secret(name, value)
226
+
227
+
228
+ def validate_env_secrets(names=None) -> dict:
229
+ """
230
+ Batch-validate secret-bearing environment variables at startup.
231
+
232
+ Iterates the secret-bearing key registry (or the provided iterable of
233
+ names, intersected with the registry), calls validate_env_secret()
234
+ per key, and aggregates all failures into a single structured error
235
+ so misconfiguration fails fast with one clear report.
236
+
237
+ Returns a dict {name: validated_value} for every key that passed
238
+ (unset keys map to ""). Raises RuntimeError with a redacted,
239
+ structured summary when any key fails validation. Error messages
240
+ never contain raw secret values.
241
+ """
242
+ registry = SECRET_BEARING_KEYS
243
+ if names is None:
244
+ targets = sorted(registry)
245
+ else:
246
+ targets = [n for n in names if n in registry]
247
+
248
+ validated = {}
249
+ failures = {}
250
+ for name in targets:
251
+ try:
252
+ validated[name] = validate_env_secret(name)
253
+ except ValueError as exc:
254
+ # Store the redacted form of the current value for diagnostics.
255
+ failures[name] = {
256
+ "error": str(exc),
257
+ "value": redact_secret(name, os.environ.get(name, "")),
258
+ }
259
+
260
+ if failures:
261
+ # Use safe_log so no secret material can leak into the summary.
262
+ summary = safe_log(
263
+ "Batch secret validation failed for {count} key(s): {keys}",
264
+ count=len(failures),
265
+ keys=", ".join(sorted(failures)),
266
+ )
267
+ raise RuntimeError(
268
+ summary + " :: " + json.dumps(failures, sort_keys=True)
269
+ )
270
+ return validated
271
+
272
+
273
+ # ---------------------------------------------------------------------------
274
+ # Import-time batch validation
275
+ # ---------------------------------------------------------------------------
276
+ # Run the batch validator once at module import against the full
277
+ # secret-bearing key registry. A malformed secret fails fast here with a
278
+ # redacted, structured report (no raw values ever reach the traceback).
279
+ try:
280
+ VALIDATED_SECRETS = validate_env_secrets()
281
+ except RuntimeError as _secret_error:
282
+ # Re-raise with a clear, non-leaking message; the RuntimeError from
283
+ # validate_env_secrets already contains only redacted diagnostics.
284
+ raise RuntimeError(
285
+ "config.py: refusing to initialize with invalid secrets. "
286
+ f"{_secret_error}"
287
+ ) from None
288
+
289
+ WORKSPACE_DIR = os.path.abspath(os.path.dirname(__file__))
290
+
291
+ # Исправлено: заменена несуществующая google/gemini-2.0-flash-001 на рабочую deepseek/deepseek-chat
292
+ DEFAULT_MODELS = {
293
+ "researcher": "deepseek/deepseek-chat",
294
+ "editor": "deepseek/deepseek-chat",
295
+ "reviewer": "deepseek/deepseek-chat",
296
+ }
297
+ DEFAULT_PRICING = {
298
+ "prompt_price_per_1m": 0.14,
299
+ "completion_price_per_1m": 0.28,
300
+ "context_length": 1_000_000,
301
+ }
302
+
303
+ # Исправлен путь импорта: из папки tools/
304
+ try:
305
+ from tools.llm_selector import get_agent_bindings as _get_agent_bindings, get_model_details as _get_model_details
306
+ except ImportError:
307
+ try:
308
+ from llm_selector import get_agent_bindings as _get_agent_bindings, get_model_details as _get_model_details
309
+ except ImportError:
310
+ _get_agent_bindings = None
311
+ _get_model_details = None
312
+
313
+ ACTIVE_MODELS_FILE = os.path.join(WORKSPACE_DIR, "active_models.json")
314
+ MODELS_CACHE_FILE = os.path.join(WORKSPACE_DIR, "models_cache.json")
315
+
316
+
317
+ # Model IDs must be provider/name form with a safe charset; this blocks
318
+ # path traversal or header injection through a poisoned models cache.
319
+ _MODEL_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$")
320
+
321
+
322
+ def _is_valid_model_id(model_id) -> bool:
323
+ return (
324
+ isinstance(model_id, str)
325
+ and bool(_MODEL_ID_RE.match(model_id))
326
+ and ".." not in model_id
327
+ )
328
+
329
+
330
+ def _load_legacy_active_models() -> dict:
331
+ if os.path.exists(ACTIVE_MODELS_FILE):
332
+ try:
333
+ with open(ACTIVE_MODELS_FILE, "r", encoding="utf-8") as f:
334
+ data = json.load(f)
335
+ if isinstance(data, dict):
336
+ return {
337
+ agent: data.get(agent, DEFAULT_MODELS[agent])
338
+ for agent in DEFAULT_MODELS
339
+ if _is_valid_model_id(data.get(agent, DEFAULT_MODELS[agent]))
340
+ }
341
+ except Exception:
342
+ pass
343
+ return dict(DEFAULT_MODELS)
344
+
345
+
346
+ def _load_legacy_model_specs(model_id: str) -> dict:
347
+ if os.path.exists(MODELS_CACHE_FILE):
348
+ try:
349
+ with open(MODELS_CACHE_FILE, "r", encoding="utf-8") as f:
350
+ cached_data = json.load(f)
351
+ data = cached_data.get("data", cached_data) if isinstance(cached_data, dict) else cached_data
352
+ if isinstance(data, list):
353
+ for item in data:
354
+ if not isinstance(item, dict) or item.get("id") != model_id:
355
+ continue
356
+ if not _is_valid_model_id(item.get("id")):
357
+ continue
358
+ pricing = item.get("pricing", {}) or {}
359
+ if not isinstance(pricing, dict):
360
+ pricing = {}
361
+ try:
362
+ prompt_str = str(pricing.get("prompt", "0") or "0")
363
+ completion_str = str(pricing.get("completion", "0") or "0")
364
+ prompt_price = float(prompt_str) * 1_000_000
365
+ completion_price = float(completion_str) * 1_000_000
366
+ # Reject negative or absurd prices (poisoned cache).
367
+ if not (0 <= prompt_price <= 10_000) or not (0 <= completion_price <= 10_000):
368
+ raise ValueError
369
+ context_length = item.get("context_length", DEFAULT_PRICING["context_length"])
370
+ if not isinstance(context_length, int) or context_length <= 0:
371
+ context_length = DEFAULT_PRICING["context_length"]
372
+ except (TypeError, ValueError):
373
+ continue
374
+ return {
375
+ "prompt_price_per_1m": prompt_price if prompt_str not in ("", "0") else DEFAULT_PRICING["prompt_price_per_1m"],
376
+ "completion_price_per_1m": completion_price if completion_str not in ("", "0") else DEFAULT_PRICING["completion_price_per_1m"],
377
+ "context_length": context_length,
378
+ }
379
+ except Exception:
380
+ pass
381
+ return dict(DEFAULT_PRICING)
382
+
383
+
384
+ def get_active_model(agent: str) -> str:
385
+ """Динамический поиск активной модели для агента."""
386
+ if not isinstance(agent, str) or not agent:
387
+ return "deepseek/deepseek-chat"
388
+ if _get_agent_bindings is not None:
389
+ try:
390
+ bindings = _get_agent_bindings()
391
+ if isinstance(bindings, dict) and agent in bindings:
392
+ candidate = bindings[agent]
393
+ if _is_valid_model_id(candidate):
394
+ return candidate
395
+ except Exception:
396
+ pass
397
+ # Безопасный откат
398
+ fallback = _load_legacy_active_models().get(agent, DEFAULT_MODELS.get(agent, "deepseek/deepseek-chat"))
399
+ return fallback if _is_valid_model_id(fallback) else "deepseek/deepseek-chat"
400
+
401
+ get_agent_model = get_active_model
402
+
403
+
404
+ def get_model_specs(model_id: str) -> dict:
405
+ if not _is_valid_model_id(model_id):
406
+ return dict(DEFAULT_PRICING)
407
+ if _get_model_details is not None:
408
+ try:
409
+ details = _get_model_details(model_id)
410
+ if isinstance(details, dict):
411
+ def _safe_price(key: str) -> float:
412
+ try:
413
+ val = float(details.get(key, DEFAULT_PRICING[key]))
414
+ return val if 0 <= val <= 10_000 else DEFAULT_PRICING[key]
415
+ except (TypeError, ValueError):
416
+ return DEFAULT_PRICING[key]
417
+
418
+ context_length = details.get("context_length", DEFAULT_PRICING["context_length"])
419
+ if not isinstance(context_length, int) or context_length <= 0:
420
+ context_length = DEFAULT_PRICING["context_length"]
421
+ return {
422
+ "prompt_price_per_1m": _safe_price("price_input_1m"),
423
+ "completion_price_per_1m": _safe_price("price_output_1m"),
424
+ "context_length": context_length,
425
+ }
426
+ except Exception:
427
+ pass
428
+ return _load_legacy_model_specs(model_id)
429
+
430
+
431
+ def get_current_price(agent: str) -> tuple:
432
+ model_id = get_active_model(agent)
433
+ specs = get_model_specs(model_id)
434
+ return specs["prompt_price_per_1m"], specs["completion_price_per_1m"], specs["context_length"]
435
+
436
+ # Константы бюджета и итераций
437
+ MAX_BUDGET_USD = 5
438
+ MAX_ITERATIONS = 8 # вместо 10
439
+ HITL_EVERY_N_CYCLES = 3
440
+ PLAN_CHUNK_THRESHOLD = 100
441
+
442
+ # ============================================================================
443
+ # Lesson Memory Bridge (VegeX memory subsystem)
444
+ # ============================================================================
445
+ # The bridge syncs JSON lesson files into a SQLite A-Score index at runtime.
446
+ # JSON remains the source of truth for content; SQLite holds ranking signals.
447
+
448
+ LESSONS_MEMORY_ENABLED = os.getenv("VEGEX_LESSONS_MEMORY", "1") == "1"
449
+ LESSONS_ROOT = os.path.join(WORKSPACE_DIR, "lessons")
450
+ LESSONS_MEMORY_DIR = os.path.join(WORKSPACE_DIR, ".vegex")
451
+ LESSONS_MEMORY_DB_PATH = os.path.join(LESSONS_MEMORY_DIR, "lessons.db")
452
+ def _safe_int_env(name: str, default: int, minimum: int, maximum: int) -> int:
453
+ """Parse an integer env var with bounds; falls back to default on abuse."""
454
+ raw = os.getenv(name, "")
455
+ try:
456
+ val = int(raw)
457
+ except (TypeError, ValueError):
458
+ return default
459
+ if val < minimum or val > maximum:
460
+ return default
461
+ return val
462
+
463
+
464
+ LESSONS_EMBEDDING_DIM = _safe_int_env("VEGEX_LESSON_EMBED_DIM", 256, 1, 4096)
465
+
466
+ # Retrieval defaults
467
+ LESSONS_RETRIEVAL_LIMIT = 5
468
+ LESSONS_RETRIEVAL_STAGES_RESEARCHER = ("promoted", "candidates", "observations")
469
+ LESSONS_RETRIEVAL_STAGES_CONSUMER = ("promoted",)
470
+
471
+ # A-Score hyperparameters for the lesson ranker (see memory/ranker.py)
472
+ LESSONS_RANKING_ALPHA = 1.0
473
+ LESSONS_RANKING_BETA_PROTECTION = 2.0
474
+ LESSONS_RANKING_LAMBDA_BASE = 0.01
475
+ LESSONS_RANKING_EPSILON = 1e-5
476
+
477
+ # RSI mode: active project is DifferiX itself. Enables diff-based
478
+ # security scanning instead of full-content scanning.
479
+ RSI_MODE = os.getenv("VEGEX_RSI_MODE", "0") == "1"
480
+
481
+ # ============================================================================
482
+ # Embedding provider (VegeX memory subsystem)
483
+ # ============================================================================
484
+ # Provider selection: "auto" | "openrouter" | "local" | "hash"
485
+ # auto → OpenRouter if OPENROUTER_API_KEY is set, else local, else hash
486
+ # openrouter → require OPENROUTER_API_KEY (same key as chat completions)
487
+ # local → require fastembed installed (offline)
488
+ # hash → zero-dependency bag-of-words (low quality; emergency only)
489
+ EMBEDDING_PROVIDER = os.getenv("VEGEX_EMBEDDING_PROVIDER", "auto")
490
+ EMBEDDING_PREFER_LOCAL = os.getenv("VEGEX_EMBEDDING_PREFER_LOCAL", "0") == "1"
491
+
492
+ # Target vector dimension. text-embedding-3-small supports the `dimensions`
493
+ # parameter (truncation); 512 balances quality and storage. Local MiniLM
494
+ # is fixed at 384 and ignores this.
495
+ EMBEDDING_DIM = _safe_int_env("VEGEX_EMBEDDING_DIM", 512, 1, 4096)
496
+
497
+ # OpenRouter embedding model ID (provider-prefixed).
498
+ OPENROUTER_EMBEDDING_MODEL = os.getenv(
499
+ "VEGEX_OPENROUTER_EMBED_MODEL", "openai/text-embedding-3-small"
500
+ )
501
+
502
+ # ---------------------------------------------------------------------------
503
+ # Reflection — periodic self-analysis of recent failures
504
+ # ---------------------------------------------------------------------------
505
+ REFLECTION_INTERVAL = _safe_int_env("VEGEX_REFLECTION_INTERVAL", 5, 1, 1000)
506
+ REFLECTION_MODEL = os.getenv("VEGEX_REFLECTION_MODEL", "") # empty → top-tier
507
+ if REFLECTION_MODEL and not _is_valid_model_id(REFLECTION_MODEL):
508
+ REFLECTION_MODEL = ""
509
+ REFLECTION_MAX_OBSERVATIONS = 3
510
+ REFLECTION_MIN_OCCURRENCES = 2
511
+
512
+ # Note: no separate key is needed. The OpenRouterEmbeddingProvider uses
513
+ # OPENROUTER_API_KEY (defined at the top of this module).
514
+
515
+ # ============================================================================
516
+ # Tiered Context Assembly (TCA)
517
+ # ============================================================================
518
+ # Context is assembled with a fixed token budget per role. Tiers are packed
519
+ # in priority order (T0_STATIC -> T5_FEEDBACK). When the budget is exhausted,
520
+ # lower-priority content is degraded to its fallback or dropped entirely.
521
+ #
522
+ # See tools/context_assembler.py for the tier hierarchy.
523
+ #
524
+ # Setting a budget to a huge value effectively disables TCA truncation for
525
+ # that role (all content is included as-is).
526
+ TCA_ENABLED = os.getenv("VEGEX_TCA_ENABLED", "1") == "1"
527
+
528
+ TOKEN_BUDGET_EDITOR = _safe_int_env("VEGEX_BUDGET_EDITOR", 18000, 2000, 200000)
529
+ TOKEN_BUDGET_REVIEWER = _safe_int_env("VEGEX_BUDGET_REVIEWER", 14000, 2000, 200000)
530
+ TOKEN_BUDGET_RESEARCHER = _safe_int_env("VEGEX_BUDGET_RESEARCHER", 12000, 2000, 200000)
531
+
532
+ # Fraction of budget actually used (headroom for token estimate error).
533
+ TCA_SAFETY_MARGIN = 0.9
534
+
535
+ # Include feedback (past errors) on iteration 0? Default False — nothing
536
+ # useful on the first try, saves ~1-2k tokens per cycle.
537
+ TCA_INCLUDE_FEEDBACK_ON_FIRST_CYCLE = False
538
+
539
+ # Print one-line TCA summary after every assemble(). Set to 0 in production
540
+ # once budgets are tuned.
541
+ TCA_LOG_SUMMARY = True
542
+
543
+ # ============================================================================
544
+ # RSI Mode Safety (Phase 1.3)
545
+ # ============================================================================
546
+ # These files are the "brain" and "immune system" of DifferiX. In RSI mode,
547
+ # the agent can *propose* changes to them, but they will NOT be applied
548
+ # automatically. They are saved to .vegex/pending_critical_changes/ for human review.
549
+ RSI_CRITICAL_FILES = {
550
+ "main.py",
551
+ "config.py",
552
+ "tools/security_filter.py",
553
+ "tools/file_manager.py",
554
+ "tools/state_manager.py",
555
+ "tools/security/verification_firewall.py",
556
+ "tools/security/behavior_guard.py",
557
+ }
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: differix
3
+ Version: 0.1.0a1
4
+ Summary: The Deterministic, Local-First OS for Autonomous AI Agents.
5
+ Author: Tdahqu8
6
+ License: MIT
7
+ Project-URL: Homepage, https://differix.dev
8
+ Project-URL: Bug Tracker, https://github.com/Tdahqu8/DifferiX/issues
9
+ Project-URL: Source Code, https://github.com/Tdahqu8/DifferiX
10
+ Keywords: ai,llm,agents,multi-agent,openai,claude,autonomous,developer-tools
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Code Generators
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: python-dotenv>=1.0.0
22
+ Requires-Dist: openai>=1.0.0
23
+ Requires-Dist: requests>=2.31.0
24
+ Requires-Dist: httpx>=0.27.0
25
+ Requires-Dist: pydantic>=2.0.0
26
+ Requires-Dist: rich>=13.0.0
27
+ Requires-Dist: GitPython>=3.1.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
30
+ Provides-Extra: full
31
+ Requires-Dist: fastembed>=0.3.0; extra == "full"
32
+ Requires-Dist: PyYAML>=6.0; extra == "full"
33
+ Requires-Dist: playwright>=1.40.0; extra == "full"
34
+
35
+ <div align="center">
36
+ <h1>DifferiX</h1>
37
+ <p><strong>The Deterministic, Local-First OS for Autonomous AI Agents.</strong></p>
38
+ <p>
39
+ <a href="#-features">Features</a> •
40
+ <a href="#-quick-start">Quick Start</a> •
41
+ <a href="#-architecture">Architecture</a> •
42
+ <a href="#-plugins">Plugins</a>
43
+ </p>
44
+ </div>
45
+
46
+ ---
47
+
48
+ DifferiX is an open, model-agnostic multi-agent runtime. It doesn't just let LLMs write code; it forces them to write **verified, executable code** inside a deterministic Python shell.
49
+
50
+ Most "autonomous agents" fail because they hallucinate success and loop endlessly. DifferiX solves this with **Zero Trust Architecture**, **Tiered Context Assembly**, and a **Local Experience Engine**.
51
+
52
+ ## 🧠 Core Architectural Patterns
53
+
54
+ 1. **Zero Trust to Agent:** A stochastic LLM is never trusted with state mutation. Success is verified by compilers, ASTs, and fs-hashes, not by the agent's words.
55
+ 2. **Probabilistic in a Deterministic Shell:** The LLM acts purely as a translator. Python scripts act as the supreme judge, sandbox, and state machine.
56
+ 3. **Scarcity by Design (TCA):** Artificial token budgets force the LLM to focus on surgical context (reducing input tokens by 80%).
57
+ 4. **Evolutionary Memory (A-Score):** Experience is stored as abstract math. Unhelpful lessons fade out; helpful ones propagate without retraining the LLM.
58
+
59
+ ## ✨ Features
60
+
61
+ * **Verification Firewall:** Blocks agents from faking success. If the file on disk didn't change, the patch is rejected.
62
+ * **Behavior Guard:** Detects prompt-context bloat and repeated actions in real-time. Kills the loop before it burns your API budget.
63
+ * **Tiered Context Assembly (TCA):** Waterfall token budgeting. AST-slicing for Python, Class Inventory extraction for CSS.
64
+ * **Dynamic Skill Injection:** The Researcher analyzes the task and injects *only* the relevant skills (e.g., `style_brutalist` or `style_3d_webgl`).
65
+ * **Recursive Self-Improvement (RSI):** The framework can safely modify its own source code, protected by a critical-file gate.
66
+ * **Model Agnostic:** Works with OpenRouter, Anthropic, Google, or local Ollama models.
67
+
68
+ ## 🚀 Quick Start
69
+
70
+ ```bash
71
+ # Clone the repository
72
+ git clone https://github.com/Tdahqu8/DifferiX.git
73
+ cd DifferiX
74
+
75
+ # Setup environment
76
+ python3 -m venv venv
77
+ source venv/bin/activate
78
+ pip install -r requirements.txt
79
+
80
+ # Add your API key
81
+ cp .env.example .env
82
+ # Edit .env and add OPENROUTER_API_KEY=sk-or-v1-...
83
+
84
+ # Run DifferiX
85
+ export VEGEX_RSI_MODE=1
86
+ python main.py
87
+ ```
88
+
89
+ ## 🎨 Showcase (WebDev Pro Plugin)
90
+
91
+ DifferiX dynamically injects design skills to generate production-ready UIs.
92
+
93
+ > *"Create a portfolio landing page for a Web3 graffiti artist. Make it look raw and bold."* — Generated in 4 cycles using the `style_brutalist` skill.
94
+
95
+ ## 🏗️ Architecture
96
+
97
+ ```text
98
+ [ User ] <--> [ LLM Interpreter ]
99
+ |
100
+ +--------------+--------------+
101
+ | |
102
+ [ Symbolic Verifier ] [ Experience Engine ]
103
+ (Python / AST / Compilers) (A-Score / SQLite / Math)
104
+ | |
105
+ +--------------+--------------+
106
+ |
107
+ [ Verified Code ]
108
+ ```
109
+
110
+ ## 🔌 Ecosystem & Plugins
111
+
112
+ DifferiX supports `.differixplugin` files. Install plugins to give agents new superpowers (WebDev, EdTech, Crypto Auditing).
113
+
114
+ > **Note:** Executable code is strictly forbidden inside plugins to prevent RCE. Plugins are data-only (`.md`, `.json`, `.css`).
115
+
116
+ ```bash
117
+ # Install the official WebDev Pro plugin
118
+ differix plugin install https://github.com/Tdahqu8/DifferiX/releases/download/v0.1.0-alpha/webdev_pro.differixplugin
119
+ ```
120
+
121
+ ## 📜 License
122
+
123
+ MIT License. Build, fork, and create.