- One decision per real turn: a turn starts with a user message after the previous final answer; tool steps, messages typed mid-turn, a lone "." and compaction keep the combo. Works without prompt_cache_key too. - The virtual Otomatik name never reaches the gateway (stage off or error -> last choice or targets.zor); prefilter.target may not be it. - finishCheck: at most once per turn; a broken, empty or refused retry delivers the already approved first answer. - Layer window filter: only trusted windows, never the last layer. - Correction channel only on always-pass targets while checks are on; Sesli-Hizli-Zeki stays always-pass by default; a >256 KB SSE frame passes unfiltered instead of breaking the stream. - Turn-start questions wait in the local queue no longer than their timeout; decision-turn memory under a lock with refreshed order. - Dify fetch refuses redirects (the app key never moves host); the rules file write retries a Windows sharing violation. - An unreadable settings file no longer breaks every request. - TypeScript accepts only https:// URLs the bridge accepts; finishCheck.timeoutSeconds (unused) removed. Tests: 310 bridge tests, 36 electron tests; side-bridge live run for the turn, prefilter (incl. decision timeout) and finish-check paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
103 lines
4.5 KiB
Python
103 lines
4.5 KiB
Python
"""Decision rules from Dify (14 Oct 2026).
|
|
|
|
Owner request, 14 Oct 2026: "Dify'de hazirlansa oradan gelse". The model a Codex step talks to stays an
|
|
OmniRoute combo, because a Dify app would not pass Codex's own tools through. What Dify supplies is the rules:
|
|
the decision questions, thresholds and which combo each kind of request goes to. The bridge fetches them in
|
|
the background (policy.refreshMinutes), validates them against the local settings and stores the last good
|
|
copy in jev-policy.json. A turn never waits for Dify; a failed fetch keeps the last good rules.
|
|
|
|
The Dify workflow returns one output named ``kurallar``: a JSON object (or JSON text) whose keys are any of
|
|
jev_settings.POLICY_KEYS, each holding the same fields as the local settings section, for example
|
|
{"autoRoute": {"enabled": true, "targets": {"zor": "Ajan"}}}.
|
|
"""
|
|
import copy
|
|
import json
|
|
import time
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
import jev_settings
|
|
|
|
UA = "AITURK-IDE-codex-bridge/karar-politikasi"
|
|
|
|
|
|
class _YonlendirmeYok(urllib.request.HTTPRedirectHandler):
|
|
"""A redirect would carry the Authorization header to another host; it is an error instead."""
|
|
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
raise urllib.error.HTTPError(req.full_url, code, f"redirect refused: {newurl}", headers, fp)
|
|
|
|
|
|
_ACICI = urllib.request.build_opener(_YonlendirmeYok)
|
|
MAX_BYTES = 64 * 1024
|
|
_son = {"zaman": 0.0}
|
|
|
|
|
|
def ayikla(cikti, yerel):
|
|
"""Dify output -> validated overrides. Raises ValueError with a short reason when unusable."""
|
|
if isinstance(cikti, str):
|
|
if len(cikti.encode("utf-8")) > MAX_BYTES:
|
|
raise ValueError("kurallar too large")
|
|
cikti = json.loads(cikti)
|
|
if not isinstance(cikti, dict):
|
|
raise ValueError("kurallar is not an object")
|
|
bilinmeyen = sorted(set(cikti) - set(jev_settings.POLICY_KEYS))
|
|
if bilinmeyen:
|
|
raise ValueError("unknown sections: " + ", ".join(bilinmeyen))
|
|
overrides = {k: v for k, v in cikti.items() if isinstance(v, dict)}
|
|
if len(overrides) != len(cikti):
|
|
raise ValueError("every section must be an object")
|
|
birlesik = copy.deepcopy(yerel)
|
|
for k, v in overrides.items():
|
|
birlesik[k] = jev_settings._fill(v, yerel[k])
|
|
jev_settings.validate(birlesik)
|
|
return overrides
|
|
|
|
|
|
def cek(settings, key, urlopen=_ACICI.open):
|
|
"""One fetch. Returns {"ok": bool, "overrides": {...}, "error": str}; never raises."""
|
|
if not key:
|
|
return {"ok": False, "error": "Dify key not configured"}
|
|
url = settings['policy']['endpoint'].rstrip('/') + '/workflows/run'
|
|
govde = json.dumps({"inputs": {}, "response_mode": "blocking", "user": "aiturk-codex-bridge"}).encode("utf-8")
|
|
istek = urllib.request.Request(url, data=govde, method="POST", headers={
|
|
"Authorization": "Bearer " + key, "Content-Type": "application/json", "User-Agent": UA})
|
|
try:
|
|
with urlopen(istek, timeout=30) as yanit:
|
|
ham = yanit.read(MAX_BYTES * 4 + 1)
|
|
if len(ham) > MAX_BYTES * 4:
|
|
return {"ok": False, "error": "Dify response too large"}
|
|
veri = json.loads(ham)
|
|
cikti = ((veri.get("data") or {}).get("outputs") or {}).get("kurallar")
|
|
if cikti is None:
|
|
return {"ok": False, "error": "Dify output 'kurallar' missing"}
|
|
return {"ok": True, "overrides": ayikla(cikti, settings)}
|
|
except Exception as e:
|
|
return {"ok": False, "error": f"{type(e).__name__}: {e}"[:300]}
|
|
|
|
|
|
def zamani_mi(settings, simdi=None):
|
|
simdi = time.time() if simdi is None else simdi
|
|
return settings['policy']['source'] == 'dify' and simdi - _son["zaman"] >= settings['policy']['refreshMinutes'] * 60
|
|
|
|
|
|
def yenile(log=lambda _m: None, urlopen=_ACICI.open, zorla=False):
|
|
"""Fetch when due; store the outcome. Local settings are read without the Dify overlay."""
|
|
try:
|
|
yerel, _ = _yerel_ayar()
|
|
except Exception as e:
|
|
log(f"karar kurallari: ayar okunamadi ({type(e).__name__})")
|
|
return None
|
|
if not zorla and not zamani_mi(yerel):
|
|
return None
|
|
_son["zaman"] = time.time()
|
|
sonuc = cek(yerel, jev_settings.policy_key(), urlopen=urlopen)
|
|
kayit = jev_settings.write_policy(sonuc)
|
|
log("karar kurallari Dify'dan alindi" if sonuc.get("ok") else f"karar kurallari alinamadi: {sonuc.get('error')}"
|
|
+ (" (son gecerli kurallar kullaniliyor)" if kayit.get("ok") else ""))
|
|
return kayit
|
|
|
|
|
|
def _yerel_ayar():
|
|
"""Settings as written locally (no Dify overlay), so a bad overlay can never validate itself."""
|
|
return jev_settings.read(overlay=False)
|