Files
aiturk-hermes-ide/apps/desktop/assets/codex-bridge/jev_settings.py
T
yilsemandClaude Opus 5.5 7c4ef18df0 feat(codex-bridge): decision-model stages, Dify rules and beta.39
Owner request, 14 October 2026: use the decision model (Jev / Celeris) at
several stages, let Dify supply the rules, code it without new bugs.
Every new stage is switched on in Settings > Kademeli Gecis and starts
disabled, so the upgrade changes no behaviour by itself.

- Settings schema 2 (Python and TypeScript validate the same rules); a
  schema 1 file is filled from the defaults. Default chain is now
  Katman-1 -> Katman-2 -> Katman-3 with Katman-3, Chat-Model and
  bekci-sesli-bekci always passing (owner decision of the same day).
- watchModels: the Bekci correction channel follows a settings list
  instead of the hard-coded Sesli-Hizli-Zeki.
- prefilter: a predicted refusal starts the step at the chosen layer.
- autoRoute: virtual "Otomatik" model; one decision per user turn picks
  the combo, tool steps of the same turn keep it.
- finishCheck: an answer that stops early goes back to the same model once.
- A layer whose window cannot hold the request is skipped.
- policy: stage rules from a Dify workflow (karar_politikasi.py), fetched
  in the background, validated, last good copy kept; turns never wait.
- karar_sor.py: verifierCheck for the dynamic-workflow acceptance step.
- Bridge version 19, IDE 1.0.0-beta.39.

Tests: 299 bridge tests, 36 electron tests; live run on a side bridge
(port 8797) with Celeris and the AITURK gateway for every stage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:48:33 +03:00

213 lines
9.2 KiB
Python

"""Per-user settings, re-read for new evaluations without restarting active chats.
Schema 2 (14 Oct 2026) adds the decision stages: watchModels, prefilter, autoRoute, finishCheck,
verifierCheck and policy. A schema 1 file stays valid: every missing section is filled from the
defaults, and every new stage starts disabled, so an upgrade changes no behaviour by itself.
With policy.source "dify" the stage settings may be overridden by the last good rules fetched
from Dify (jev-policy.json); invalid or missing rules leave the local settings in force.
"""
import base64
import copy
import ctypes
import json
import os
import sys
import threading
import time
from pathlib import Path
from urllib.parse import urlsplit
DEFAULTS = json.loads(Path(__file__).with_name('jev-settings-defaults.json').read_text(encoding='utf-8'))
POLICY_FILE = 'jev-policy.json'
# Sections Dify rules may change. The transition chain and the always-pass list stay local.
POLICY_KEYS = ('prefilter', 'autoRoute', 'finishCheck', 'verifierCheck', 'thresholds', 'questions')
SECTIONS = ('watchModels', 'prefilter', 'autoRoute', 'finishCheck', 'verifierCheck', 'policy')
_condition = threading.Condition()
_active = 0
def home():
if '--home' in sys.argv:
return Path(sys.argv[sys.argv.index('--home') + 1])
return Path(os.environ.get('AITURK_CODEX_BRIDGE_HOME') or Path(__file__).parent)
def _fill(value, default):
"""Missing keys of a schema 1 file come from the defaults; present values are kept as written."""
if isinstance(default, dict) and isinstance(value, dict):
return {k: _fill(value[k], d) if k in value else copy.deepcopy(d) for k, d in default.items()} | \
{k: v for k, v in value.items() if k not in default}
return value
def _text(value, limit=10000):
return isinstance(value, str) and bool(value.strip()) and len(value) <= limit
def _prob(value):
return isinstance(value, (int, float)) and not isinstance(value, bool) and 0 <= value <= 1
def _int(value, low, high):
return isinstance(value, int) and not isinstance(value, bool) and low <= value <= high
def _https(value):
if not _text(value, 2048):
return False
url = urlsplit(value)
return url.scheme == 'https' and bool(url.netloc) and not url.username and not url.password
def validate(settings):
"""Raise ValueError when a value could route to the wrong place or wait without bound."""
# A corrupted file must not silently change routing or contact another host.
if not _https(settings['endpoint']):
raise ValueError('Invalid Jev endpoint')
for name in ('ret', 'sart'):
n = settings['thresholds'][name]
if not isinstance(n, (int, float)) or not 0 <= n <= 1 or not settings['questions'][name]:
raise ValueError('Invalid Jev rule')
for name in ('timeoutSeconds', 'queueWaitSeconds'):
if not isinstance(settings[name], int) or not 1 <= settings[name] <= 3600:
raise ValueError('Invalid Jev timeout')
if not isinstance(settings['maxConcurrent'], int) or not 0 <= settings['maxConcurrent'] <= 512:
raise ValueError('Invalid Jev concurrency')
if not settings['fallbackModel'] or not isinstance(settings['rejectionRules'], list) or settings['failureAction'] not in ('pass', 'next'):
raise ValueError('Invalid Jev routing')
watch = settings['watchModels']
if not isinstance(watch, list) or len(watch) > 64 or not all(_text(x, 256) for x in watch) or len(set(watch)) != len(watch):
raise ValueError('Invalid watch models')
p = settings['prefilter']
if not (isinstance(p.get('enabled'), bool) and _text(p.get('question')) and _prob(p.get('threshold'))
and _text(p.get('target'), 256) and _int(p.get('timeoutSeconds'), 1, 60)):
raise ValueError('Invalid prefilter')
a = settings['autoRoute']
if not (isinstance(a.get('enabled'), bool) and _text(a.get('model'), 256) and _int(a.get('timeoutSeconds'), 1, 60)
and all(_text((a.get('questions') or {}).get(k)) and _prob((a.get('thresholds') or {}).get(k)) for k in ('zor', 'arastirma'))
and all(_text((a.get('targets') or {}).get(k), 256) for k in ('basit', 'zor', 'arastirma'))):
raise ValueError('Invalid automatic routing')
if a['model'] in a['targets'].values():
raise ValueError('Automatic routing cannot target itself')
f = settings['finishCheck']
if not (isinstance(f.get('enabled'), bool) and _text(f.get('question')) and _prob(f.get('threshold'))
and _text(f.get('note'), 4000) and _int(f.get('timeoutSeconds'), 1, 60)):
raise ValueError('Invalid finish check')
v = settings['verifierCheck']
if not (isinstance(v.get('enabled'), bool) and _text(v.get('question')) and _prob(v.get('threshold'))):
raise ValueError('Invalid verifier check')
pol = settings['policy']
if not (pol.get('source') in ('local', 'dify') and _https(pol.get('endpoint')) and _int(pol.get('refreshMinutes'), 1, 1440)):
raise ValueError('Invalid policy source')
return settings
def _policy(settings):
"""Last good Dify rules on top of the local settings; anything invalid keeps the local settings."""
if settings['policy']['source'] != 'dify':
return settings
try:
cache = json.loads((home() / POLICY_FILE).read_text(encoding='utf-8'))
overrides = cache.get('overrides')
if not cache.get('ok') or not isinstance(overrides, dict):
return settings
merged = copy.deepcopy(settings)
for key in POLICY_KEYS:
if isinstance(overrides.get(key), dict):
merged[key] = _fill(overrides[key], settings[key])
return validate(merged)
except (OSError, ValueError, KeyError, TypeError, AttributeError):
return settings
def read(overlay=True):
"""(settings, protectedKey). overlay=False: the local settings without the Dify rules."""
try:
data = json.loads((home() / 'jev-settings.json').read_text(encoding='utf-8'))
except FileNotFoundError:
return copy.deepcopy(DEFAULTS), None
settings = data['settings']
for section in SECTIONS:
settings[section] = _fill(settings[section], DEFAULTS[section]) if section in settings else copy.deepcopy(DEFAULTS[section])
validate(settings)
return (_policy(settings) if overlay else settings), data.get('protectedKey')
class _Blob(ctypes.Structure):
_fields_ = [('cbData', ctypes.c_uint32), ('pbData', ctypes.POINTER(ctypes.c_ubyte))]
def _unprotect(encrypted):
if not encrypted:
return ''
if os.name != 'nt':
raise RuntimeError('Windows credential protection required')
raw = base64.b64decode(encrypted, validate=True)
buf = (ctypes.c_ubyte * len(raw)).from_buffer_copy(raw)
source, result = _Blob(len(raw), buf), _Blob()
decrypt = ctypes.windll.crypt32.CryptUnprotectData
decrypt.argtypes = [ctypes.POINTER(_Blob), ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.POINTER(_Blob)]
decrypt.restype = ctypes.c_int
if not decrypt(ctypes.byref(source), None, None, None, None, 0, ctypes.byref(result)):
raise ctypes.WinError()
try:
return ctypes.string_at(result.pbData, result.cbData).decode('utf-8')
finally:
ctypes.windll.kernel32.LocalFree.argtypes = [ctypes.c_void_p]
ctypes.windll.kernel32.LocalFree(result.pbData)
def key():
_, encrypted = read()
return _unprotect(encrypted)
def policy_key():
"""Dify app key for the rules source (protectedPolicyKey, Windows DPAPI). Empty when not set."""
try:
data = json.loads((home() / 'jev-settings.json').read_text(encoding='utf-8'))
except (OSError, ValueError):
return ''
return _unprotect(data.get('protectedPolicyKey'))
def write_policy(result):
"""Atomically store the outcome of a Dify fetch. A failed fetch keeps the last good overrides."""
path = home() / POLICY_FILE
previous = {}
try:
previous = json.loads(path.read_text(encoding='utf-8'))
except (OSError, ValueError):
pass
record = {'fetchedAt': time.strftime('%Y-%m-%dT%H:%M:%S%z'), 'ok': bool(result.get('ok')), 'error': str(result.get('error') or '')[:500]}
record['overrides'] = result.get('overrides') if result.get('ok') else previous.get('overrides')
if not result.get('ok') and previous.get('ok'):
record['ok'] = True # the last good rules stay in force; the error is reported beside them
record['lastGoodAt'] = previous.get('lastGoodAt') or previous.get('fetchedAt')
elif result.get('ok'):
record['lastGoodAt'] = record['fetchedAt']
tmp = path.with_name(path.name + f'.{os.getpid()}.tmp')
tmp.write_text(json.dumps(record, ensure_ascii=False, indent=2), encoding='utf-8')
os.replace(tmp, path)
return record
def acquire(settings):
global _active
deadline = time.monotonic() + settings['queueWaitSeconds']
with _condition:
while settings['maxConcurrent'] and _active >= settings['maxConcurrent']:
remaining = deadline - time.monotonic()
if remaining <= 0:
return False
_condition.wait(remaining)
_active += 1
return True
def release():
global _active
with _condition:
_active -= 1
_condition.notify_all()