From 1445e204daf8f8f84297883527d00c9e77876ebd Mon Sep 17 00:00:00 2001 From: yilsem Date: Sun, 11 Oct 2026 01:05:00 +0300 Subject: [PATCH] feat(codex-bridge): handoff instruction for Codex Desktop only (off by default) User decision: the "larger work goes to AITURK" instruction lives only in what Codex IDE sees; the CLI must not know it exists. The bridge adds it in flight by the originator header (measured: Codex Desktop, codex-tui, codex_exec) and only when a send command is configured. Co-Authored-By: Claude Opus 5.5 --- apps/desktop/assets/codex-bridge/README.md | 7 +++ apps/desktop/assets/codex-bridge/bridge.py | 12 +++++ apps/desktop/assets/codex-bridge/is_hatti.py | 52 +++++++++++++++++++ .../codex-bridge/test_access_programs.py | 2 +- .../assets/codex-bridge/test_combo_tekrar.py | 2 +- .../assets/codex-bridge/test_is_hatti.py | 35 +++++++++++++ 6 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 apps/desktop/assets/codex-bridge/is_hatti.py create mode 100644 apps/desktop/assets/codex-bridge/test_is_hatti.py diff --git a/apps/desktop/assets/codex-bridge/README.md b/apps/desktop/assets/codex-bridge/README.md index 936c761..c19b350 100644 --- a/apps/desktop/assets/codex-bridge/README.md +++ b/apps/desktop/assets/codex-bridge/README.md @@ -45,6 +45,13 @@ for the chat, i.e. until the user picks another one in the list. Setting: `sesle_model` (default on). +### Work handoff instruction (prepared, off by default) + +`is_hatti.py`: when `aiturk_hat` is enabled and has a `komut`, the bridge prepends a developer message ("small work +yourself; larger work goes to AITURK via this command") to requests whose `originator` header is `Codex Desktop`. +The Codex CLI (`codex-tui`, `codex_exec`) never receives it and nothing is written to any file it reads. Each new +`originator` value is logged once (`istemci goruldu`). + ## Version 7 (IDE beta.26): external models behave like subscription models Measured on 10 Oct 2026 with the unmodified Codex binary (`codex exec`, same conversation, model switched per turn). diff --git a/apps/desktop/assets/codex-bridge/bridge.py b/apps/desktop/assets/codex-bridge/bridge.py index e4ea1ac..0d28542 100644 --- a/apps/desktop/assets/codex-bridge/bridge.py +++ b/apps/desktop/assets/codex-bridge/bridge.py @@ -28,6 +28,7 @@ import ozet_kopru import pencere import yarim import model_gecis +import is_hatti SURUM = "8" HERE = os.path.dirname(os.path.abspath(__file__)) @@ -666,6 +667,7 @@ def decompress(body, enc): OZET_FRENI = ozet_kopru.DonguFreni() YONLENDIRME = model_gecis.Yonlendirme(HOME) +GORULEN_ISTEMCI = set() # originator degerleri; her biri gunluge bir kez yazilir _BILINEN = {"kaynak": None, "liste": []} def bilinen_modeller(): @@ -1108,6 +1110,16 @@ class H(http.server.BaseHTTPRequestHandler): # istek sanip is yapmaz, yavas modelde asil soru beklemez. Secilen modelin adiyla aninda donulur. log(f"[{n} {slug}] nokta mesaji kopruden karsilandi (modele gonderilmedi)") return self._yerel_yanit(gelen, f"{model_adi(slug)} hazır. Sohbetin modeli artık {model_adi(slug)}.") + # Is hatti talimati yalniz masaustu uygulamasindan gelen istege eklenir; CLI hic gormez (is_hatti.py). + kaynak = self.headers.get("originator") or "" + if kaynak and kaynak not in GORULEN_ISTEMCI: + GORULEN_ISTEMCI.add(kaynak) + log(f"istemci goruldu: {kaynak}") + try: + if is_hatti.masaustu_mu(kaynak) and is_hatti.ekle(req, is_hatti.talimat(CFG.get("aiturk_hat"))): + govde_degisti = True + except Exception as e: + log(f"[{n} {slug}] is hatti talimati atlandi: {type(e).__name__}: {e}") # Ayni sohbetin onceki cevabi yarida kesildiyse (yarim.py) metni bu istege not olarak eklenir. yarim_eklenen = 0 if CFG.get("yarim_cevap", True): diff --git a/apps/desktop/assets/codex-bridge/is_hatti.py b/apps/desktop/assets/codex-bridge/is_hatti.py new file mode 100644 index 0000000..df38f36 --- /dev/null +++ b/apps/desktop/assets/codex-bridge/is_hatti.py @@ -0,0 +1,52 @@ +"""Codex IDE'ye "is kucuk degilse AITURK yapacak" talimati (11 Ekim 2026). + +Kullanici karari: talimat yalniz Codex IDE'nin (masaustu uygulamasi) gordugu sistem talimatinda durur; Codex CLI +talimati hic gormez, varligindan haberi olmaz. Karari secili model verir; kucuk modellerin yanilmasi kabul edilir, +ayri bir yazilimla onune gecilmez. + +Bu yuzden talimat hicbir dosyaya yazilmaz (AGENTS.md'yi CLI da okur): kopru, yalniz masaustu uygulamasindan gelen +isteklere, istek gecerken ekler. Istemci `originator` basligindan ayirt edilir (olcum 11 Ekim 2026: masaustu +"Codex Desktop", etkilesimli CLI "codex-tui", `codex exec` "codex_exec"). + +Ayar (bridge.json): "aiturk_hat": {"enabled": true, "komut": "", "talimat": ""}. +Komut tanimli degilse talimat eklenmez: gonderme yolu olmayan bir "AITURK'e gonder" talimati modeli yaniltir. +""" + +MASAUSTU = {"codex desktop"} +ISARET = "[AITURK iş hattı]" +VARSAYILAN = ( + ISARET + " Bu Codex, AITURK'e bağlı. Şu kurala göre çalış:\n" + "1. Basit soruyu, bilgi isteğini ya da tek küçük düzeltmeyi kendin yap.\n" + "2. Kullanıcı \"sen yap\" derse işi kendin yap.\n" + "3. İş küçük değilse (birden çok dosya, birkaç adım, yeni özellik, araştırma gerektiren iş) kendin yapma; " + "işi şu komutla AITURK'e gönder, bitmesini bekle ve sonucu kullanıcıya kısaca aktar:\n{komut}\n" + "Emin değilsen işi küçük say ve kendin yap." +) + + +def masaustu_mu(originator): + return str(originator or "").strip().lower() in MASAUSTU + + +def talimat(ayar): + """Eklenecek metin; hat kapaliysa ya da komut yoksa None.""" + if not isinstance(ayar, dict) or not ayar.get("enabled"): + return None + komut = str(ayar.get("komut") or "").strip() + if not komut: + return None + return str(ayar.get("talimat") or VARSAYILAN).replace("{komut}", komut) + + +def ekle(req, metin): + """Talimati istegin basina developer mesaji olarak koyar. True: eklendi (zaten varsa eklenmez).""" + girdi = req.get("input") + if not metin or not isinstance(girdi, list): + return False + for oge in girdi[:3]: + icerik = oge.get("content") if isinstance(oge, dict) else None + yazi = icerik if isinstance(icerik, str) else " ".join(str(p.get("text") or "") for p in icerik or [] if isinstance(p, dict)) + if ISARET in yazi: + return False + girdi.insert(0, {"type": "message", "role": "developer", "content": [{"type": "input_text", "text": metin}]}) + return True diff --git a/apps/desktop/assets/codex-bridge/test_access_programs.py b/apps/desktop/assets/codex-bridge/test_access_programs.py index 54a0bff..92d9901 100644 --- a/apps/desktop/assets/codex-bridge/test_access_programs.py +++ b/apps/desktop/assets/codex-bridge/test_access_programs.py @@ -31,7 +31,7 @@ class AccessPrograms(unittest.TestCase): "fix_tool_ids": lambda req: 0, "drop_foreign_items": lambda req: 0, "SseRewriter": Mock(), - "log": lambda message: None, "pencere": __import__("pencere"), "IDE_BILGI": {}, "yarim": __import__("yarim"), "nokta_mesaji_mi": lambda req: False, "EXTRA_MODELS": [], + "log": lambda message: None, "pencere": __import__("pencere"), "IDE_BILGI": {}, "yarim": __import__("yarim"), "is_hatti": __import__("is_hatti"), "GORULEN_ISTEMCI": set(), "nokta_mesaji_mi": lambda req: False, "EXTRA_MODELS": [], "DEBUG": False, "UA": "test", "RETRIES": 1, "UPSTREAM_IDLE_TIMEOUT": 3900, } exec(compile(ast.Module(body=[flatten, claude_sonu, responses], type_ignores=[]), "bridge.py", "exec"), self.env) diff --git a/apps/desktop/assets/codex-bridge/test_combo_tekrar.py b/apps/desktop/assets/codex-bridge/test_combo_tekrar.py index cba8b02..675a350 100644 --- a/apps/desktop/assets/codex-bridge/test_combo_tekrar.py +++ b/apps/desktop/assets/codex-bridge/test_combo_tekrar.py @@ -40,7 +40,7 @@ class ComboNotResent(unittest.TestCase): "fix_tool_ids": lambda req: 0, "drop_foreign_items": lambda req: 0, "SseRewriter": Mock(), "log": lambda message: None, "DEBUG": False, "UA": "test", "RETRIES": 6, "UPSTREAM_IDLE_TIMEOUT": 3600, - "NO_RETRY_PATTERNS": [], "pencere": __import__("pencere"), "IDE_BILGI": {}, "yarim": __import__("yarim"), "nokta_mesaji_mi": lambda req: False, "EXTRA_MODELS": [], + "NO_RETRY_PATTERNS": [], "pencere": __import__("pencere"), "IDE_BILGI": {}, "yarim": __import__("yarim"), "is_hatti": __import__("is_hatti"), "GORULEN_ISTEMCI": set(), "nokta_mesaji_mi": lambda req: False, "EXTRA_MODELS": [], } exec(compile(ast.Module(body=[pick("flatten_request"), pick("claude_sonu_duzelt"), pick("is_timeout"), responses], type_ignores=[]), "bridge.py", "exec"), self.env) diff --git a/apps/desktop/assets/codex-bridge/test_is_hatti.py b/apps/desktop/assets/codex-bridge/test_is_hatti.py new file mode 100644 index 0000000..6f3546d --- /dev/null +++ b/apps/desktop/assets/codex-bridge/test_is_hatti.py @@ -0,0 +1,35 @@ +"""The "send larger work to AITURK" instruction reaches only Codex Desktop, and only when a send command exists.""" +import unittest + +import is_hatti as h + + +class Hat(unittest.TestCase): + def test_only_the_desktop_app_is_recognised(self): + self.assertTrue(h.masaustu_mu("Codex Desktop")) + for other in ("codex-tui", "codex_exec", "codex_cli_rs", "", None, "Codex Desktop Helper"): + self.assertFalse(h.masaustu_mu(other), other) + + def test_no_instruction_without_the_switch_or_without_a_send_command(self): + self.assertIsNone(h.talimat(None)) + self.assertIsNone(h.talimat({"enabled": False, "komut": "aiturk gonder"})) + self.assertIsNone(h.talimat({"enabled": True})) + self.assertIsNone(h.talimat({"enabled": True, "komut": " "})) + text = h.talimat({"enabled": True, "komut": "aiturk gonder --dosya is.md"}) + self.assertIn("aiturk gonder --dosya is.md", text) + self.assertIn("sen yap", text) + self.assertEqual(h.talimat({"enabled": True, "komut": "x", "talimat": "ozel {komut}"}), "ozel x") + + def test_instruction_is_put_first_once_and_never_repeated(self): + req = {"input": [{"type": "message", "role": "user", "content": "selam"}]} + text = h.talimat({"enabled": True, "komut": "aiturk gonder"}) + self.assertTrue(h.ekle(req, text)) + self.assertEqual((req["input"][0]["role"], len(req["input"])), ("developer", 2)) + self.assertFalse(h.ekle(req, text)) + self.assertEqual(len(req["input"]), 2) + self.assertFalse(h.ekle({"input": "duz metin"}, text)) + self.assertFalse(h.ekle(req, None)) + + +if __name__ == "__main__": + unittest.main()