Files
aiturk-hermes-ide/apps/desktop/assets/codex-bridge/karar_politikasi.py
T
yilsemandClaude Opus 5.5 a7ba300448
Server CI and Windows package / verify (push) Successful in 3m23s
Server CI and Windows package / windows-package (push) Successful in 7m9s
fix(codex-bridge): close the gaps an independent review found
- 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>
2026-10-14 08:14:38 +03:00

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)