Wire authenticated voice watch notices through an owned native launcher

This commit is contained in:
yilsem committed 2026-10-12 19:24:33 +03:00
1 parent eeee2756c7
commit b66d9164ad
23 files changed
+1981 -10

No files matched your search

+9
View File
@@ -32,6 +32,8 @@ jobs:
"$RUNNER_TEMP/ci-venv/bin/python" -m pytest tests/tools/test_protected_instruction_unattended.py tests/tools/test_file_write_safety.py tests/tools/test_computer_use_approval_isolation.py tests/cli/test_cli_approval_ui.py tests/cli/test_single_query_session_finalize.py -q -p no:cacheprovider -rf
- name: Codex bridge regression tests
run: '"$RUNNER_TEMP/ci-venv/bin/python" -m unittest discover -s apps/desktop/assets/codex-bridge'
- name: Native watch and speech mux pytest regressions
run: '"$RUNNER_TEMP/ci-venv/bin/python" -m pytest apps/desktop/assets/codex-bridge/test_bekci_watch.py apps/desktop/assets/codex-bridge/test_native_speech_mux.py -q -p no:cacheprovider -rf'
- name: Mobile and desktop startup regression tests
run: |
npm install --prefix "$RUNNER_TEMP/mobile-tests" --no-save --ignore-scripts --no-audit --no-fund --legacy-peer-deps vitest@4.1.10 ws@8.21.1
@@ -54,6 +56,13 @@ jobs:
github-server-url: https://gitea.turkservis.online
- name: Install locked source dependencies
run: npm ci --no-audit --no-fund
- name: Install Windows launcher cross compiler and framework reference
run: |
set -eu
apt-get update
apt-get install -y --no-install-recommends mono-mcs=6.8.0.105+dfsg-3.2 libmono-system-web-extensions4.0-cil=6.8.0.105+dfsg-3.2
- name: Compile Windows launcher from source and verify failure gate
run: node --test apps/desktop/scripts/build-native-watch-launcher.test.mjs
- name: Native dependency staging regression tests
run: npx vitest run --root apps/desktop scripts/stage-native-deps.test.mjs --maxWorkers=2
- name: Build and package Windows x64
@@ -0,0 +1,2 @@
# Generated by beforePack from NativeWatchBootstrap.cs; never commit opaque output.
/native-watch.exe
@@ -0,0 +1,136 @@
# Native watch launcher — reviewable Windows candidate
This is an owned startup hook for the original Codex engine, not another model
engine. Do not activate it until root has built a new packaged release and
reviewed the evidence. No environment variable is set by build/provision tools.
## Build and provision
Normal Windows `beforePack` invokes `scripts/build-native-watch-launcher.mjs`.
The resulting `native-watch.exe` is included in the existing codex-bridge
extraResources directory along with Python modules. Windows hosts use the .NET Framework C# compiler. The existing Linux/Wine
Windows-package job installs mono-mcs and libmono-system-web-extensions4.0-cil
6.8.0.105+dfsg-3.2 from its Ubuntu 22.04 apt repository and compiles with mcs.
Both hosts rebuild the same C# source; missing compilers or invalid source fail
packaging and remove stale output. The generated EXE is ignored by git.
This is a Windows artifact cross-build, not a Linux runtime launcher. The executable is a managed console bootstrap; use
an absolute managed **python.exe**, not pythonw.exe. It checks pinned hashes of
original binary, interpreter and launcher script. Original is checked again in
Python immediately before use. Parent directory ACLs and the selected official
binary hash are provisioning authority; this does not claim Authenticode trust.
After installing the new package, root provisions an owner-local candidate:
```powershell
py -3 prepare_native_watch.py --source <packaged-codex-bridge> --destination <owned-candidate-directory> --original <official-absolute-codex.exe> --expected-sha256 <independently-verified-official-sha256> --python <managed-python.exe> --bridge-home <bridge-data-directory> --hermes-home <existing-agent-home>
```
`prepare_native_watch.py` copies only the relevant resources and writes the
adjacent native-watch.json (paths/hashes only, no keys). It never reads
CODEX_CLI_PATH to locate the original and rejects its own executable as original.
Root alone may later point Desktop's CODEX_CLI_PATH at this EXE. No Python argv
are placed inside CODEX_CLI_PATH. No installed app.asar/binary modification.
Rollback is root removing its override to restore original app startup.
Installed app read-only inspection (OpenAI.Codex_26.1007.2314.0):
`.vite/build/application-network-startup-CfAQmSYg.js` tc()/Ks() constructs
`-c` pairs before/after `app-server`, `--analytics-default-enabled`. Recognizer
accepts that grammar plus explicit stdio forms, rejects daemon/websocket/unknown
commands and passes those unchanged to the original. Approvals flow untouched.
One original engine only; no extra version process at startup. Original cwd and
args preserved except one per-child `-c openai_base_url` selecting the in-process
bridge's ephemeral loopback `/backend-api/codex`. Normal original models and
public member catalog reuse kopru.H. Native environment is restored after bridge
import. Bridge logging uses stderr so stdout remains native JSONL only.
## Receipt, delivery and lifecycle
Before routing/rewrite, all three actual request values must agree:
session-id, thread-id and prompt_cache_key. The active mux connection ID and
realtime generation are captured then. Neither current tab nor notice data can
create that mapping. thread/unsubscribe, realtime stop/start/closed/error and EOF
invalidate an epoch. UI-only switches not observable in native protocol cannot
be detected here; root must verify Desktop emits stop/unsubscribe for its switch.
Concurrent active native threads have independent request mappings.
SSE `event: bekci.watch` and nonstream `_bekci_watch` are removed before native
model text, history observers, response rewriting and response preview logging.
Only `/v1/bekci/events` and the fixed `https://ai.turkservis.online` origin are
accepted. The same primary route public member key authenticates POST; redirects
are not followed. No backend keys, raw server tenant/session/history identifiers,
or reconstructed history exist in this client.
Only authenticated SSE `ready` binds the receipt. Binding's legacy tenant_id,
bekci_session_id, history_revision fields contain **local opaque scopes**, not
literal server claims: member-key fingerprint, key+public-subscription digest,
public-receipt revision digest. Server remains authoritative for hidden history.
TLS plus same-key proxy ready is the receipt validity attestation. SQLite also
locks receipt scope to its original connection/thread/generation across restarts.
SQLite WAL/FULL ledger commits `uncertain` before enqueue. Replays or a crash
between commit and append are suppressed, including possible lost delivery.
Cursor is persisted after durable admission/disposition; it is a local replay
ack, not a server consumption API or exactly-once guarantee. Native RPC acceptance
does not prove audio output. Replay IDs never cause automatic uncertain resend.
Keepalives, bounded reconnect attempts, receipt expiry, lifecycle abort and
simultaneous threads are handled. Receipt expiry invalidates its local binding.
No raw notice text or credentials are persisted in the ledger.
## Evidence and exact remaining operations
- `.claude/sesli-client-all-tests.log`: 20 files, 203 passed, 0 failed.
- `.claude/sesli-client-tests-red.log`: compiled Windows bootstrap lost stdout/
stderr before fix; interactive flush subsequently exposed by real native probe
and covered by `test_executable_flushes_interactive_stdin_before_eof`.
- `.claude/native-launcher-proof.json`: compiled candidate, pinned official native
initialize accepted, stdout JSON, EOF exit 0. No native voice call performed.
- `.claude/native-launcher-candidate/native-watch.exe`: concrete provisioned
candidate under worktree, not activated or installed.
Source now stamps desktop beta.30 and bridge 10. Root still must run
normal full package CI/build/signing policy, inspect packaged resources, provision the
new packaged candidate with trusted official SHA/managed runtime, activate only
authorized Desktop startup, verify thread-switch notifications and exercise a
real Sesli-Hizli-Zeki call through deployed server to audible appendSpeech. The bootstrap assigns itself to a non-inheritable KILL_ON_JOB_CLOSE Windows
Job Object before Python starts. Its descendants inherit membership; abrupt
bootstrap termination closes the handle and kills only that owned tree. Owned
fake and pinned original initialize/kill tests passed for both Windows-built and
Linux-built EXEs running on Windows. Actual Desktop UI termination remains a
manual acceptance step. Atomic generation enforcement inside native appendSpeech
is still unavailable (the native RPC has no expected-generation field).
No commit, push, deployment, installed runtime change or AITURK delegation.
## 2026-10-12 source verification update
- `.claude/native-watch-linux-build.log`: exact CI image digest
`sha256:b226b27b1a7e95ad25eb3d6414ea90c56c04eb6d8329967f4d721dce50b989ff`,
real apt dependencies, two compiler regressions passed, Windows PE produced.
- `.claude/native-watch-windows-build.log`: same two tests passed with Framework csc.
- `.claude/native-watch-linux-artifact-windows-tests.log`: 25 tests passed with
the cross-built artifact, including pinned original initialize and abrupt kill.
- `.claude/sesli-client-release-all-tests.log`: 220 passed, 21 files, zero failures.
- `.claude/electron-beta30-tests.log`: 47 passed including Python/Electron version alignment.
Receipt validation accepts up to 24 hours plus 60 seconds clock skew. Actual
expiry remains authoritative; expired, boolean, nonnumeric, NaN, infinite and
over-bound receipts are rejected. Long research has no local 15-minute limit.
Provision the exact rebuilt candidate with the explicit original SHA-256
`d13914ced6c7af174d8d638db938284231227312104b06a25e9608c632a7daec` only
when that is the selected independently verified official original. SHA-1 is
not the executable integrity pin; a Git source identity and an executable
SHA-256 have different roles. Fixed-origin TLS plus the same public member key
and authenticated ready event attest the receipt; none claims Authenticode.
No full installer build, signing, activation, or audible acceptance is claimed.
CI verify explicitly runs test_bekci_watch.py and test_native_speech_mux.py using
ci-venv pytest in addition to the unchanged unittest suite. Both focused files
pass on Linux without skips. Tests that execute native-watch.exe reside in the
explicitly Windows-only test_native_watch_cleanup.py; their owned-process proof
is Windows evidence, not Linux CI runtime coverage.
Sideband JSON extraction and SSE filtering apply only to the resolved exact
Sesli-Hizli-Zeki target, including when no watch owner is active. Other models
retain their original Jev/response pipeline without the sideband frame buffer.
After the final scope correction, rebuild/provision from final source; the older
local candidate hash proof does not include this last kopru.py change.
@@ -0,0 +1,89 @@
// Thin Windows CLI_PATH executable. Runtime and original are owner-provisioned,
// absolute, SHA-pinned; never consult PATH or CODEX_CLI_PATH.
using System;
using System.IO;
using System.Text;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Collections.Generic;
using System.Threading;
using System.Runtime.InteropServices;
using System.Web.Script.Serialization;
class NativeWatchBootstrap {
// Assign this bootstrap BEFORE creating Python. Descendants inherit job
// membership atomically; the non-inheritable handle belongs only to us.
// Windows closes it even on TerminateProcess, killing Python and native.
static IntPtr ownedJob;
[StructLayout(LayoutKind.Sequential)] struct BasicLimits {
public long ProcessTime, JobTime;
public uint Flags;
public UIntPtr MinWorkingSet, MaxWorkingSet;
public uint ActiveProcesses;
public UIntPtr Affinity;
public uint Priority, Scheduling;
}
[StructLayout(LayoutKind.Sequential)] struct IoCounters {
public ulong ReadOps, WriteOps, OtherOps, ReadBytes, WriteBytes, OtherBytes;
}
[StructLayout(LayoutKind.Sequential)] struct ExtendedLimits {
public BasicLimits Basic;
public IoCounters Io;
public UIntPtr ProcessMemory, JobMemory, PeakProcessMemory, PeakJobMemory;
}
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
static extern IntPtr CreateJobObject(IntPtr attributes, string name);
[DllImport("kernel32.dll", SetLastError=true)]
static extern bool SetInformationJobObject(IntPtr job, int kind, ref ExtendedLimits limits, uint size);
[DllImport("kernel32.dll", SetLastError=true)]
static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
static void OwnProcessTree() {
ownedJob=CreateJobObject(IntPtr.Zero,null);
var limits=new ExtendedLimits(); limits.Basic.Flags=0x2000; // KILL_ON_JOB_CLOSE
if(ownedJob==IntPtr.Zero || !SetInformationJobObject(ownedJob,9,ref limits,(uint)Marshal.SizeOf(typeof(ExtendedLimits)))
|| !AssignProcessToJobObject(ownedJob,Process.GetCurrentProcess().Handle)) throw new Exception();
// Do not close here or in finally: the bootstrap is itself a member.
// The OS closes this handle at exit, preserving our native exit code.
}
static string Quote(string s) {
var b=new StringBuilder("\""); int slashes=0;
foreach(char c in s) {
if(c=='\\') {slashes++; continue;}
if(c=='"') { b.Append('\\',slashes*2+1); b.Append(c); }
else {b.Append('\\',slashes); b.Append(c);}
slashes=0;
}
b.Append('\\',slashes*2); b.Append('"'); return b.ToString();
}
static string Pin(Dictionary<string,object> c,string field) {
string p=(string)c[field], expected=(string)c[field+"_sha256"];
if(!Path.IsPathRooted(p) || String.Equals(Path.GetFullPath(p),Process.GetCurrentProcess().MainModule.FileName,StringComparison.OrdinalIgnoreCase)) throw new Exception();
using(var f=File.OpenRead(p)) using(var sha=SHA256.Create()) {
string actual=BitConverter.ToString(sha.ComputeHash(f)).Replace("-","").ToLowerInvariant();
if(actual!=expected.ToLowerInvariant()) throw new Exception();
}
return p;
}
static void Pump(Stream source, Stream target) {
byte[] buffer=new byte[8192]; int count;
while((count=source.Read(buffer,0,buffer.Length))>0) { target.Write(buffer,0,count); target.Flush(); }
}
static int Main(string[] args) {
try {
string manifest=Path.ChangeExtension(Process.GetCurrentProcess().MainModule.FileName,".json");
var c=new JavaScriptSerializer().Deserialize<Dictionary<string,object>>(File.ReadAllText(manifest));
string original=Pin(c,"original");
string python=Pin(c,"python");
string script=Pin(c,"script");
OwnProcessTree();
var argv=new List<string>{"-u",script,manifest}; argv.AddRange(args);
var quoted=new List<string>(); foreach(var a in argv) quoted.Add(Quote(a));
var p=Process.Start(new ProcessStartInfo(python,String.Join(" ",quoted.ToArray())) { UseShellExecute=false, CreateNoWindow=true, RedirectStandardInput=true, RedirectStandardOutput=true, RedirectStandardError=true });
var input=new Thread(()=> { try { Pump(Console.OpenStandardInput(),p.StandardInput.BaseStream); p.StandardInput.Close(); } catch(IOException) {} });
input.IsBackground=true; input.Start();
var output=new Thread(()=>Pump(p.StandardOutput.BaseStream,Console.OpenStandardOutput()));
var error=new Thread(()=>Pump(p.StandardError.BaseStream,Console.OpenStandardError()));
output.Start(); error.Start();
p.WaitForExit(); output.Join(); error.Join(); return p.ExitCode;
} catch { Console.Error.WriteLine("Owned native launcher integrity/configuration failure."); return 78; }
}
}
@@ -0,0 +1,44 @@
# Codex sesli entegrasyon incelemesi — 12 Ekim 2026
## Uygulanan değişiklik ve doğrulama
Çözülmüş hedef tam `Sesli-Hizli-Zeki` ise nihai metin Jev'e gönderilmez. Büyük/küçük harf, boşluk, `ide/` öneki veya sonek normalizasyonu yapılmaz. `ide/` kullanıcı slug'ı önce mevcut mapping ile hedefe çözülür. Taşıma hatası, boş yanıt ve kopan akışın katman kurtarması korunur; yedek katmana geçince muafiyet taşınmaz. Araç/commentary hazırlık bypass'ı korunur. İstisna yanıt tamponlamasını değiştirmez.
`test_jev_denetim.py` exact hedefi, benzer isimleri ve fallback katmanının tekrar denetlenmesini çalıştırır. `test_denetim_ek.py` gerçek `_responses` metodu üzerinden başarılı yanıt, bağlantı kopması ve HTTP 400 yollarında çözülmüş hedef aktarımını, `instructions` ve developer mesajının korunmasını doğrular. Bunlar mevcut handler test düzeneğini kullanır; canlı sağlayıcı testi değildir.
Paket README'sindeki komutla `py -3 -m unittest discover`: **185 test, 25.305 saniye, OK**. Mevcut testlerde ResourceWarning ve beklenen yerel bağlantı reddi çıktısı vardı. İlk yeni test eski imzada hata verdi. Ayrıca muafiyet fonksiyonu geçici mock ile False yapıldığında yeni test gerçek davranış farkıyla başarısız oldu: özgün cevap yerine Katman-1 cevabı geldi (`MUTATION_DETECTED=True`).
## Gerçek Codex ses yolu
İncelenen istemci: WindowsApps `OpenAI.Codex_26.1007.2314.0_x64__2p2nqsd0c76g0`, `app/resources/app.asar`, salt okunur. CLI: `codex-cli 0.155.1`. `codex app-server generate-ts --experimental --out <geçici-dizin>` ile yerel binary protokolü üretildi.
Protokolün `ClientRequest.ts` dosyasında gerçek yöntemler:
- `thread/realtime/appendText`: `{threadId, text, role}`; role = user/developer/assistant. Girdi ekler; tek başına ses üretimini garanti ettiği iddia edilmez.
- `thread/realtime/appendSpeech`: `{threadId, text}`; açıklaması “append speakable text to thread realtime”.
- `thread/realtime/start`: thread kapsamlı oturum; websocket, webrtc veya existingCall taşıması. `clientManagedHandoffs` varsayılan false; automatic Codex response handoff seçenekleri var. Başka oturumun existingCall bağlantısı kullanılmadı.
- `turn/start`, `turn/steer`, `thread/queue/add`, `thread/queue/start` var. `turn/steer` aktif `expectedTurnId` ister; bitmiş tura uygulanacak genel uyandırma yolu değildir. `codex queue --thread ... --message ...` mevcut CLI yüzeyidir; kuyruğa koymanın ses çıkışıyla aynı şey olduğu doğrulanmadı.
İstemcinin `webview/assets/app-initial-b8e1c8014bb4.js` dosyasında `cra` ve `lra`, sırasıyla appendText/appendSpeech RPC'sini `manager.sendRequest` ile çağırıyor. Realtime runtime aynı yöntemleri dışarı sunuyor; durdurulmuş durumda no-op. **Somut arka plan tüketicisi:** kullanım limiti yaklaşma bildirimi geldiğinde istemci `appendSpeech` çağırıp uyarıyı sesli söylüyor (`[Composer] failed to speak realtime usage warning` hata kolu). Bu, kullanıcı yeni mesaj yazmadan konuşma istemenin mevcut istemci mekanizması olduğunu kanıtlar. Bekçi düzeltmesinin canlı ses çıkışı test edilmiş değildir.
`CompilerIntrinsics-87ea46a69911.js` içinde WebRTC data channel üzerinden `conversation.item.create` gönderimi de var. Aynı aramada `response.created` olay işleyicisi görüldü; bunu `response.create` gönderim kanıtı saymıyoruz. Ham Realtime protokolü icat etmek yerine mevcut thread realtime RPC'si entegrasyon adayıdır.
## Köprüdeki somut eksik
`bridge.py:H._isle` yalnız `/responses` ve model listesini alt köprüye yollar; diğer yollar, `/realtime/calls` dahil, doğrudan ChatGPT'ye gider. WebSocket upgrade 426 ile reddedilir. Dolayısıyla `kopru.py:H._passthrough` içindeki isteğe bağlı `voice_prep.baslat(Location, headers, ...)` kancası standart ayırıcı topolojisinde çağrı oluşturmayı göremez.
`voice_prep.py` ve `ses_tuneli.py` kaynak worktree ve kurulu AITURK resources/codex-bridge içinde yok. Yerel aramada `Temp/oldb/voice_prep.py` yedek kopyası bulundu ve yalnız okundu: `Hazirlayici` aynı call'a `wss://api.openai.com/v1/live/` sideband açıyor; `session.context.append` ile bağlam ekliyor. Bu eski dosyada response.create/conversation.item.create gönderimi bulunmadı. Yedek dosya kurulu/aktif bileşen kanıtı değildir; çalıştırılmadı veya kopyalanmadı. `ses_tuneli` bulunamadı.
## Bekçi entegrasyonunda kalanlar
Diğer ajanın auth + session + cursor notices SSE sözleşmesi henüz bu çalışmaya ulaşmadı. Sözleşme sonrası bildirimin doğrulanmış session kimliğini doğru Codex host/thread ve canlı realtime ömrüne bağlamak gerekiyor. SSE yeniden bağlanma/cursor tekrarlarında aynı düzeltmeyi iki kez seslendirmeme, bitmiş ses oturumuna göndermeme ve yeni kullanıcı konuşmasıyla çakışmayı ele alma davranışları kabul testine dahil edilmeli. Bunlar uygulanmış sözleşme değildir.
Yalnız bize ait deney oturumunda, ilk ses cevabı bittikten sonra bir notice üretilip mevcut app-server yetkili bağlantısından `thread/realtime/appendSpeech` ile düzeltme konuşmasının başlaması; başka thread'in etkilenmemesi; yeni kullanıcı mesajı gerekmemesi gözlenmeli. Bu canlı test yapılmadan “entegrasyon tamamlandı” denemez. Hermes masaüstü TTS/voice hook'ları bu Codex kanalının yerine kullanılmadı. İmzalı istemciye API eklenmedi.
## Kokla / Codex alt ajan yolu
Mevcut yerel `C:/Users/maste/.codex/skills/codex-kokla-ajan/SKILL.md` açıkça `ide/GPT-6-Astra` CLI slug'ını, `GPT-6-Astra` OmniRoute combo kimliğini belirtiyor. `gpt-6-astra` tek modelini combo yerine kullanmayı yasaklıyor. Burada katalog yeniden sorgulanmadı; bilgi skill'in kaydıdır.
Mevcut CLI yolu: `codex exec --model 'ide/GPT-6-Astra' --color never -C '<ayrı-worktree>' -o '<sonuç>' '<görev>'`. `--json` süreç olaylarını, `exec resume` düzeltme devamını destekliyor. Skill ana ajana süreç kimliği, hedef, kalanlar, sonuç dosyası kaydı; çıktı takibi; bağımsız diff/test kontrolü; sıkıştırma sonrası kayıt ve kanıt okuma sorumluluğu veriyor. Bu görevde yeni bir CLI alt ajanı başlatılmadı. Runtime'ın sunulan `spawn_agent` aracı çağrısı `unsupported call: spawn_agent` döndü; bu nedenle kullanılabilir canlı alt ajan oluşturduğu iddia edilmez.
`C:/Users/maste/.codex/hooks/codex_continuity.py` save işlemi session/cwd kapsamlı; PreCompact yedekliyor, SessionStart/UserPromptSubmit geri yüklüyor, Stop aktif kayıtta devam uyarısı veriyor. Tamamlanma için kalan iş olmaması ve kanıt dosyası şartı var. Bu kayıt bağımsız test yerine geçmez. Runtime plugin CLI add/list/remove sağlar; istemcinin özel JS fonksiyonlarına genel plugin erişimi kanıtlanmadı. AITURK IDE devri yapılmadı.
@@ -0,0 +1,188 @@
"""Public Omni receipts. All scopes here are LOCAL opaque identities, never backend IDs."""
import hashlib
import http.client
import json
import socket
import sqlite3
import ssl
import threading
import time
from urllib.parse import urlsplit
from native_speech_mux import Binding, WatchReceiptAdapter
EVENTS_PATH = '/v1/bekci/events'
TRUSTED_ORIGIN = 'https://ai.turkservis.online'
# Public protocol lifetime, with bounded server/client clock skew. The server's
# expiry remains the watch deadline; there is no shorter research timeout.
MAX_RECEIPT_TTL = 24 * 60 * 60
RECEIPT_CLOCK_SKEW = 60
def opaque(*parts):
return hashlib.sha256('\0'.join(parts).encode()).hexdigest()
class Frames:
def __init__(self):
self.buf = b''
def feed(self, chunk):
self.buf += chunk
if len(self.buf) > 262144:
raise ValueError('SSE frame too large')
while True:
# Normalize only complete CRLF, including a CR split across reads.
self.buf = self.buf.replace(b'\r\n', b'\n')
if b'\n\n' not in self.buf:
return
frame, self.buf = self.buf.split(b'\n\n', 1)
event, data, cursor = '', [], None
for line in frame.decode('utf-8').split('\n'):
field, _, value = line.partition(':')
value = value.removeprefix(' ')
if field == 'event': event = value
elif field == 'data': data.append(value)
elif field == 'id': cursor = value
yield event, '\n'.join(data), cursor, frame + b'\n\n'
class MetadataFilter:
"""Strip sideband before rewrites, logging or transcript observers."""
def __init__(self, accept):
self.frames, self.accept = Frames(), accept
def feed(self, chunk):
out = []
for event, data, _, raw in self.frames.feed(chunk):
if event == 'bekci.watch':
try: self.accept(json.loads(data))
except (ValueError, TypeError): pass
else: out.append(raw)
return b''.join(out)
def flush(self):
# An incomplete sideband frame must never escape into model text.
raw, self.frames.buf = self.frames.buf, b''
return b'' if b'bekci.watch' in raw else raw
class Ledger:
"""Commit uncertain BEFORE enqueue. Crash ambiguity sacrifices delivery, never resends."""
def __init__(self, path):
self.db = sqlite3.connect(path, check_same_thread=False)
self.lock = threading.Lock()
self.db.execute('PRAGMA journal_mode=WAL')
self.db.execute('PRAGMA synchronous=FULL')
self.db.executescript('CREATE TABLE IF NOT EXISTS delivery(scope TEXT,id TEXT,status TEXT,PRIMARY KEY(scope,id)); CREATE TABLE IF NOT EXISTS cursors(scope TEXT PRIMARY KEY,cursor TEXT); CREATE TABLE IF NOT EXISTS bindings(scope TEXT PRIMARY KEY,epoch TEXT);')
def bind_epoch(self, scope, correlation):
epoch = opaque(correlation.connection_id,correlation.thread_id,str(correlation.generation))
with self.lock, self.db:
self.db.execute('INSERT OR IGNORE INTO bindings VALUES(?,?)',(scope,epoch))
return self.db.execute('SELECT epoch FROM bindings WHERE scope=?',(scope,)).fetchone()[0] == epoch
def claim(self, scope, nid):
with self.lock, self.db:
return self.db.execute('INSERT OR IGNORE INTO delivery VALUES(?,?,?)', (scope,nid,'uncertain')).rowcount == 1
def ack(self, scope, nid, cursor, status):
with self.lock, self.db:
self.db.execute('UPDATE delivery SET status=? WHERE scope=? AND id=?', (status,scope,nid))
self.db.execute('INSERT OR REPLACE INTO cursors VALUES(?,?)', (scope,cursor))
def cursor(self, scope):
with self.lock:
row = self.db.execute('SELECT cursor FROM cursors WHERE scope=?',(scope,)).fetchone()
return row[0] if row else None
class WatchOwner:
def __init__(self, mux, ledger, *, origin=TRUSTED_ORIGIN, test_loopback=False):
u = urlsplit(origin)
if origin != TRUSTED_ORIGIN and not (test_loopback and u.scheme == 'http' and u.hostname == '127.0.0.1'):
raise ValueError('untrusted Omni origin')
self.mux, self.ledger, self.origin = mux, ledger, u
self.adapter = WatchReceiptAdapter(mux)
self.lock, self.watches = threading.Lock(), {}
def capture(self, headers, body):
return self.adapter.capture(headers, body)
def live(self, c):
s = self.mux.snapshot(); t = s['threads'].get(c.thread_id, {})
return (not s['closed'] and not s['injection_disabled'] and s['connection_id'] == c.connection_id
and t.get('active') and t.get('generation') == c.generation)
def accept(self, payload, correlation, key, route_url):
if not correlation or not self.live(correlation): return 'stale_generation'
route = urlsplit(route_url)
if (route.scheme,route.netloc) != (self.origin.scheme,self.origin.netloc) or not key: return 'untrusted_route'
if (not isinstance(payload,dict) or set(payload) != {'subscription','events_url','expires_at'}
or payload['events_url'] != EVENTS_PATH or not isinstance(payload['subscription'],str)
or not 20 <= len(payload['subscription']) <= 128 or type(payload['expires_at']) not in (int,float)
or not time.time()*1000 < payload['expires_at'] <= (time.time()+MAX_RECEIPT_TTL+RECEIPT_CLOCK_SKEW)*1000): return 'invalid_receipt'
scope = opaque('public-watch',key,payload['subscription'])
if not self.ledger.bind_epoch(scope,correlation): return 'receipt_replay_mismatch'
with self.lock:
if scope in self.watches: return 'already_watching'
if len(self.watches) >= 256: return 'capacity_exhausted'
stop = threading.Event()
self.watches[scope] = stop
threading.Thread(target=self._watch,args=(dict(payload),correlation,key,scope,stop),daemon=True).start()
return 'subscribing'
def close(self):
with self.lock:
for stop in self.watches.values(): stop.set()
def _watch(self, p, c, key, scope, stop):
# These are local labels only. TLS+same-key authenticated ready attests
# receipt validity; server owns literal tenant/session/history mapping.
binding = Binding(opaque('member',key),scope,opaque('history-receipt',p['subscription']),c.connection_id,c.thread_id,c.generation)
deadline = p['expires_at']/1000
try:
for attempt in range(8):
if stop.is_set() or time.time() >= deadline or not self.live(c): break
conn = (http.client.HTTPSConnection(self.origin.hostname,self.origin.port,context=ssl.create_default_context(),timeout=10)
if self.origin.scheme == 'https' else http.client.HTTPConnection(self.origin.hostname,self.origin.port,timeout=10))
finished = threading.Event()
def guard():
while not finished.wait(.05):
if stop.is_set() or time.time() >= deadline or not self.live(c):
stop.set()
if conn.sock:
try: conn.sock.shutdown(socket.SHUT_RDWR)
except OSError: pass
conn.close()
return
guard_thread = threading.Thread(target=guard,daemon=True)
guard_thread.start()
try:
body = {'subscription':p['subscription']}
cursor = self.ledger.cursor(scope)
if cursor: body['cursor'] = cursor
conn.request('POST', EVENTS_PATH, json.dumps(body), {'Authorization':'Bearer '+key,'Content-Type':'application/json'})
resp = conn.getresponse()
if resp.status != 200 or 'text/event-stream' not in resp.getheader('Content-Type',''): break
parser, ready = Frames(), False
while self.live(c) and not stop.is_set() and time.time() < deadline:
chunk = resp.read1(4096)
if not chunk: break
for event,data,event_id,_ in parser.feed(chunk):
if not data: continue # keepalive
value = json.loads(data)
if event == 'ready':
if not isinstance(value,dict) or not isinstance(value.get('cursor'),str): raise ValueError('invalid ready')
ready = self.live(c) and self.mux.bind(binding)
if not ready: return
elif event == 'notice':
if not ready: raise ValueError('notice before ready')
self._notice(binding,scope,value,event_id,deadline,c)
except (OSError,ValueError,http.client.HTTPException):
pass # no credential-bearing exceptions in logs
finally:
finished.set(); conn.close(); guard_thread.join(timeout=1)
if stop.wait(min(2**attempt,15)): break
finally:
with self.mux._lock:
k = (binding.tenant_id,binding.bekci_session_id)
if self.mux._bindings.get(k) == binding: self.mux._bindings.pop(k,None)
with self.lock: self.watches.pop(scope,None)
def _notice(self,b,scope,value,event_id,deadline,c):
if not self.live(c) or not isinstance(value,dict): return
n, cursor = value.get('notice'), value.get('cursor')
if (not isinstance(n,dict) or not isinstance(cursor,str) or not 0 < len(cursor) <= 512
or event_id != cursor or not isinstance(n.get('id'),str) or not 0 < len(n['id']) <= 256
or n.get('type') not in ('duzeltme','arastirma') or not isinstance(n.get('metin'),str)
or not 0 < len(n['metin']) <= 8000): return
if not self.ledger.claim(scope,n['id']):
self.ledger.ack(scope,n['id'],cursor,'replay_suppressed'); return
now = time.time()
notice = dict(notice_id=n['id'],tenant_id=b.tenant_id,bekci_session_id=b.bekci_session_id,
history_revision=b.history_revision,connection_id=b.connection_id,thread_id=b.thread_id,
expected_generation=b.generation,issued_at=now,expires_at=min(now+60,deadline),text=n['metin'])
result = self.mux.submit(notice)
self.ledger.ack(scope,n['id'],cursor,result['status'])
+1 -1
View File
@@ -32,7 +32,7 @@ import urllib.error
import urllib.request
HERE = os.path.dirname(os.path.abspath(__file__))
SURUM = "9" # AITURK IDE'nin bekledigi kopru surumu (codex-link-config.ts CODEX_BRIDGE_VERSION)
SURUM = "10" # AITURK IDE'nin bekledigi kopru surumu (codex-link-config.ts CODEX_BRIDGE_VERSION)
try:
with open(os.path.abspath(__file__), "rb") as _f:
KOD = hashlib.sha256(_f.read()).hexdigest()[:8]
+44 -1
View File
@@ -32,6 +32,11 @@ import is_hatti
import internet
import jev_denetim
from bekci_watch import MetadataFilter
# Installed only by owned native launcher, never an HTTP control endpoint.
WATCH_OWNER = None
SURUM = "8"
HERE = os.path.dirname(os.path.abspath(__file__))
try:
@@ -122,7 +127,7 @@ seq = [0]
def log(msg):
line = time.strftime("%Y-%m-%d %H:%M:%S ") + msg
print(line, flush=True)
print(line, file=sys.stderr, flush=True)
with lock:
with open(os.path.join(LOGDIR, "bridge.log"), "a", encoding="utf-8") as f:
f.write(line + "\n")
@@ -1071,6 +1076,8 @@ class H(http.server.BaseHTTPRequestHandler):
body = self._read()
path = self.path
if path.startswith("/aiturk/codex-baglama/"):
if WATCH_OWNER is not None:
return self._send_json(404, {"error": "not available on native owner"})
return self._control(path)
if DEBUG:
log(f"[{n} giris {self.command} {path[:70]}] cl={self.headers.get('Content-Length')} enc={self.headers.get('Content-Encoding')} govde={len(body)}")
@@ -1391,6 +1398,7 @@ class H(http.server.BaseHTTPRequestHandler):
except Exception as e:
log(f"[{n} responses] govde cozulemedi: {e}")
return self._send_json(400, {"error": {"message": f"bridge: govde cozulemedi: {e}"}})
self.watch_correlation = WATCH_OWNER.capture(dict(self.headers), req) if WATCH_OWNER else None
slug = req.get("model", "")
gelen = slug # Codex'in bu sohbet icin gonderdigi ad (listeden secilen model)
if not ek_model_mu(gelen):
@@ -1669,6 +1677,23 @@ class H(http.server.BaseHTTPRequestHandler):
break
log(f"{tag} deneme {attempt}: {e}")
time.sleep(min(2 * attempt, 8)); continue
# Only the resolved exact voice target owns this sideband protocol.
# Other models retain their original response/Jev/stream pipeline.
if target == 'Sesli-Hizli-Zeki':
# Capture was taken before rewriting/request dispatch. Only this exact
# route credential may authenticate the returned public receipt.
correlation = self.watch_correlation
accept_watch = lambda metadata: WATCH_OWNER.accept(metadata, correlation, route['key'], route['url']) if WATCH_OWNER else None
if 'application/json' in str(resp.headers.get('Content-Type', '')):
try:
reply = json.loads(resp.read())
if isinstance(reply, dict):
metadata = reply.pop('_bekci_watch', None)
if metadata is not None: accept_watch(metadata)
return self._send_json(resp.status, reply)
finally:
resp.close()
resp = WatchResponse(resp, accept_watch)
# Headers go to the client from here on: a cut stream is never resent.
if ozet_tetik:
akis = ozet_kopru.OzetAkisi(rewriter, tohum=str(n), en_az=ozet_kopru.en_az_karakter(girdi_bayt, ozet_cfg))
@@ -1688,6 +1713,24 @@ class H(http.server.BaseHTTPRequestHandler):
do_DELETE = _handle
do_PATCH = _handle
class WatchResponse:
"""Remove metadata before any response rewriter, log or history observer."""
def __init__(self, response, accept):
self.response, self.filter = response, MetadataFilter(accept)
self.done = False
def __getattr__(self, name):
return getattr(self.response, name)
def read1(self, size):
while not self.done:
raw = self.response.read1(size)
if raw:
data = self.filter.feed(raw)
if data: return data
else:
self.done = True
return self.filter.flush()
return b''
class TS(http.server.ThreadingHTTPServer):
daemon_threads = True
# Windows'ta SO_REUSEADDR baska bir surecin ayni portu "calmasina" izin verir.
@@ -0,0 +1,573 @@
"""Owned native Codex stdio adapter; inert until explicitly launched by an owner.
Only a trusted in-process root adapter may install Binding objects. There is no
HTTP bind/RPC endpoint, tenant inference, or activated SSE subscription.
appendSpeech acceptance is not proof of audible speech or history persistence.
"""
from __future__ import annotations
import hmac
import hashlib
import json
import queue
import secrets
import subprocess
import sys
import threading
import time
from dataclasses import dataclass
from pathlib import Path
from http.server import BaseHTTPRequestHandler, HTTPServer
from typing import BinaryIO, TypedDict, Callable
@dataclass(frozen=True)
class WatchReceipt:
"""INTERNAL adapter contract, not a claim about backend wire field names.
Backend resolver authenticates an opaque handle using the member credential
against a trusted, configured Omni endpoint. Backend secret stays server-side.
The resolver must verify tenant, source request thread and history together.
No URL or identity supplied by a notice is trusted as subscription authority.
"""
opaque_handle: str
tenant_id: str
bekci_session_id: str
history_revision: str
thread_id: str
expires_at: float
@dataclass(frozen=True)
class RequestCorrelation:
"""Captured by the owned HTTP bridge from the actual incoming request.
Must be captured before metadata arrives, not reconstructed from its body.
generation is the mux epoch observed at request capture, never the latest tab.
"""
connection_id: str
thread_id: str
generation: int
class WatchReceiptAdapter:
"""One consumer; root chooses transport/decoder after backend agreement.
decode extracts only the opaque handle from bekci.watch SSE data or the
nonstream _bekci_watch value. resolve performs authenticated backend lookup.
Missing agreement is fail-closed, not fabricated ready metadata.
"""
def __init__(self, mux, *, decode: Callable[[object], str] | None = None,
resolve: Callable[[str], WatchReceipt] | None = None):
self.mux, self.decode, self.resolve = mux, decode, resolve
self._accepted = {}
def capture(self, headers: dict, body: dict) -> RequestCorrelation | None:
normalized = {k.lower(): v for k, v in headers.items()}
values = [normalized.get('session-id'), normalized.get('thread-id'), body.get('prompt_cache_key')]
if not all(isinstance(v, str) and v for v in values) or len(set(values)) != 1:
return None
snap = self.mux.snapshot()
state = snap['threads'].get(values[0], {})
if snap['closed'] or not state.get('active'):
return None
return RequestCorrelation(snap['connection_id'], values[0], state['generation'])
def accept(self, transport: str, payload: object, correlation: RequestCorrelation | None) -> str:
if transport not in ('bekci.watch', '_bekci_watch'):
return 'unsupported_metadata_transport'
if self.decode is None or self.resolve is None:
return 'backend_contract_not_established'
if correlation is None or correlation.connection_id != self.mux.connection_id:
return 'request_correlation_missing'
try:
handle = self.decode(payload)
if not isinstance(handle, str) or not 1 <= len(handle) <= 4096:
return 'invalid_receipt'
receipt = self.resolve(handle)
except Exception:
return 'receipt_verification_failed' # never expose credential-bearing exception text
if not isinstance(receipt, WatchReceipt) or receipt.opaque_handle != handle:
return 'invalid_receipt'
if (receipt.thread_id != correlation.thread_id or type(receipt.expires_at) not in (int, float)
or not time.time() < receipt.expires_at <= time.time() + 3600
or not all(isinstance(v, str) and 0 < len(v) <= 256 for v in
(receipt.tenant_id, receipt.bekci_session_id, receipt.history_revision, receipt.thread_id))):
return 'receipt_mismatch_or_expired'
binding = Binding(receipt.tenant_id, receipt.bekci_session_id, receipt.history_revision,
correlation.connection_id, receipt.thread_id, correlation.generation)
previous = self._accepted.get(handle)
if previous is not None and previous != (receipt, binding):
return 'receipt_replay_mismatch'
if previous is None and len(self._accepted) >= 4096:
return 'capacity_exhausted'
if not self.mux.bind(binding):
return 'stale_generation'
self._accepted[handle] = (receipt, binding)
return 'bound'
def notice(self, handle: str, notice: Notice) -> dict:
accepted = self._accepted.get(handle)
if accepted is None:
return {'status': 'binding_not_established'}
receipt, binding = accepted
if time.time() >= receipt.expires_at:
return {'status': 'receipt_expired'}
expected = dict(tenant_id=binding.tenant_id, bekci_session_id=binding.bekci_session_id,
history_revision=binding.history_revision, thread_id=binding.thread_id,
connection_id=binding.connection_id, expected_generation=binding.generation)
if not isinstance(notice, dict) or any(notice.get(k) != v for k, v in expected.items()):
return {'status': 'binding_mismatch'}
return self.mux.submit(notice)
def verified_original(path: str, expected_sha256: str) -> str:
"""Owner-pinned absolute original; no PATH/override discovery or extra engine."""
candidate = Path(path)
if not candidate.is_absolute() or not candidate.is_file() or candidate.resolve() == Path(__file__).resolve():
raise ValueError('original executable must be an explicitly pinned absolute file')
if len(expected_sha256) != 64:
raise ValueError('original executable hash required')
with candidate.open('rb') as stream:
actual = hashlib.file_digest(stream, 'sha256').hexdigest()
if not hmac.compare_digest(actual, expected_sha256.lower()):
raise ValueError('original executable hash mismatch')
return str(candidate.resolve())
def stdio_invocation(args: list[str]) -> bool:
"""Installed Desktop tc()/Ks() grammar, preserving every original argument."""
seen, i = False, 0
while i < len(args):
arg = args[i]
if arg in ('-c', '--config') and i + 1 < len(args):
i += 2
continue
if arg == 'app-server' and not seen:
seen = True
elif seen and arg in ('--stdio', '--analytics-default-enabled'):
pass
elif seen and arg == '--listen' and args[i+1:i+2] == ['stdio://']:
i += 1
else:
return False
i += 1
return seen
def launch_original(path: str, expected_sha256: str, args: list[str], *,
attach: Callable[[WatchReceiptAdapter], None] | None = None) -> int:
"""Source launcher entry for a future normal bridge module copy.
No activation/config mutation. Unknown invocation grammar passes native stdio
directly. Owner callback wires only this consumer after backend agreement.
No second engine, binary patch, token file or discovery of current UI tab.
"""
original = verified_original(path, expected_sha256)
observer = stdio_invocation(args)
mux = NativeSpeechMux([original, *args], observer=observer)
if observer and attach is not None:
try:
attach(WatchReceiptAdapter(mux))
except Exception:
# Optional adapter failure cannot take down native stdio. Invalidate
# any partial setup so no correction can escape a failed attachment.
mux._disabled = True
mux._bindings.clear()
return mux.run()
class Notice(TypedDict):
notice_id: str
tenant_id: str
bekci_session_id: str
history_revision: str
connection_id: str
thread_id: str
expected_generation: int
issued_at: float
expires_at: float
text: str
@dataclass(frozen=True)
class Binding:
"""Local scope labels (legacy field names), NOT literal server identity claims.
Trusted owner attests this opaque receipt scope belongs to this native epoch.
Install only from independently verified mapping, NEVER from notice fields.
Rebind explicitly for a new history revision or realtime generation.
"""
tenant_id: str
bekci_session_id: str
history_revision: str
connection_id: str
thread_id: str
generation: int
def _object(raw: bytes) -> dict:
try:
value = json.loads(raw)
return value if isinstance(value, dict) else {}
except (ValueError, UnicodeError):
return {}
def _id(value):
return (type(value), value) if type(value) in (str, int) else None
class NativeSpeechMux:
"""One original process, bounded FIFO, one writer in each direction.
argv is the original executable plus its original arguments. observer=True
must only be selected for a verified stdio app-server invocation. The CLI
deliberately recognizes only the exact ['app-server'] invocation; all other
invocations inherit the original stdio directly. No environment/cwd rewrite.
"""
def __init__(self, argv: list[str], *, observer: bool = False):
self.argv = list(argv)
self.observer = observer
self.connection_id = secrets.token_hex(24)
self._prefix = 'bekci-speech-' + secrets.token_hex(24) + ':'
self._token = secrets.token_urlsafe(32)
self._lock = threading.RLock()
self._queue = queue.Queue(maxsize=32)
self._ended = threading.Event()
self._eof = False
self._disabled = False
self._threads = {}
self._starts = set()
self._bindings = {}
self._receipts = {}
self._pending = {}
self._counter = 0
self._server = None
self._process = None
def snapshot(self) -> dict:
with self._lock:
return {'connection_id': self.connection_id,
'binding_status': 'established' if self._bindings else 'binding_not_established',
'threads': {k: dict(v) for k, v in self._threads.items()},
'closed': self._eof or self._ended.is_set(),
'injection_disabled': self._disabled}
def bind(self, binding: Binding) -> bool:
"""Trusted local API only. No untrusted endpoint can invoke this."""
with self._lock:
st = self._threads.get(binding.thread_id, {})
if (binding.connection_id != self.connection_id or not st.get('active')
or st.get('generation') != binding.generation or self._eof
or self._ended.is_set()):
return False
self._bindings[(binding.tenant_id, binding.bekci_session_id)] = binding
return True
def _invalidate(self, thread):
if not isinstance(thread, str):
return
if thread in self._threads:
self._threads[thread]['active'] = False
self._bindings = {k: b for k, b in self._bindings.items() if b.thread_id != thread}
def _observe_client(self, obj):
with self._lock:
rid = _id(obj.get('id'))
# Responses to server requests may legitimately reuse our ID.
if 'method' in obj and isinstance(obj.get('id'), str) and obj['id'].startswith(self._prefix):
self._disabled = True
self._pending.pop(obj['id'], None)
if obj.get('method') in ('thread/start', 'thread/resume', 'thread/fork') and rid:
if len(self._starts) < 4096:
self._starts.add(rid)
else:
self._disabled = True
if obj.get('method') in ('thread/realtime/stop', 'thread/realtime/start', 'thread/unsubscribe'):
params = obj.get('params')
if isinstance(params, dict):
self._invalidate(params.get('threadId'))
def _observe_server(self, obj) -> bool:
with self._lock:
rid = obj.get('id')
response = 'method' not in obj and (('result' in obj) != ('error' in obj))
if response and isinstance(rid, str) and rid in self._pending:
key = self._pending.pop(rid)
self._receipts[key]['status'] = 'rpc_error' if 'error' in obj else 'rpc_accepted'
return True
if response and _id(rid) in self._starts:
self._starts.discard(_id(rid))
result = obj.get('result')
thread = result.get('thread', {}) if isinstance(result, dict) else {}
tid = thread.get('id') if isinstance(thread, dict) else None
if isinstance(tid, str):
self._threads.setdefault(tid, {'generation': 0, 'active': False, 'realtime_session_id': None})
params = obj.get('params')
if not isinstance(params, dict) or 'id' in obj:
return False
tid = params.get('threadId')
if not isinstance(tid, str):
return False
method = obj.get('method')
if method == 'thread/realtime/started':
self._invalidate(tid)
previous = self._threads.get(tid, {}).get('generation', 0)
self._threads[tid] = {'generation': previous + 1, 'active': True,
'realtime_session_id': params.get('realtimeSessionId')}
elif method in ('thread/realtime/closed', 'thread/realtime/error'):
self._invalidate(tid)
return False
def _validate(self, notice):
if not isinstance(notice, dict) or set(notice) != set(Notice.__annotations__):
return 'invalid_schema'
for key in Notice.__annotations__:
if key not in ('expected_generation', 'issued_at', 'expires_at'):
if not isinstance(notice[key], str) or not notice[key].strip() or len(notice[key]) > (8000 if key == 'text' else 256):
return 'invalid_schema'
if type(notice['expected_generation']) is not int or notice['expected_generation'] < 1:
return 'invalid_schema'
if any(type(notice[k]) not in (int, float) for k in ('issued_at', 'expires_at')):
return 'invalid_schema'
now = time.time()
if not (now - 60 <= notice['issued_at'] <= now + 2 and now < notice['expires_at'] <= notice['issued_at'] + 60):
return 'expired_or_invalid_ttl'
if self._eof or self._ended.is_set():
return 'closed'
if self._disabled or not self.observer:
return 'injection_disabled'
binding = self._bindings.get((notice['tenant_id'], notice['bekci_session_id']))
if binding is None:
return 'binding_not_established'
if binding != Binding(notice['tenant_id'], notice['bekci_session_id'], notice['history_revision'],
notice['connection_id'], notice['thread_id'], notice['expected_generation']):
return 'binding_mismatch'
st = self._threads.get(binding.thread_id, {})
if not st.get('active') or st.get('generation') != binding.generation:
return 'stale_generation'
return None
def submit(self, notice: Notice) -> dict:
with self._lock:
error = self._validate(notice)
if error:
return {'status': error}
key = (notice['tenant_id'], notice['bekci_session_id'], notice['notice_id'])
if key in self._receipts:
return dict(self._receipts[key])
# Never evict dedupe during this process lifetime or retry uncertain sends.
if len(self._receipts) >= 4096:
return {'status': 'capacity_exhausted'}
receipt = {'status': 'queued'}
self._receipts[key] = receipt
try:
self._queue.put_nowait(('notice', (dict(notice), key)))
except queue.Full:
del self._receipts[key]
return {'status': 'busy'}
return dict(receipt)
def start_inlet(self) -> tuple[str, str]:
"""Return loopback URL and secret to trusted owner IN MEMORY; never log.
One bounded request at a time; 2s timeout applies only to inlet HTTP reads,
never the original engine. Owner must arrange private credential delivery.
"""
if not self.observer or self._server is not None:
raise ValueError('inlet requires an unused app-server observer')
mux = self
class Handler(BaseHTTPRequestHandler):
def log_message(self, *args):
pass
def setup(self):
super().setup()
self.connection.settimeout(2)
def do_POST(self):
try:
auth = self.headers.get('Authorization', '')
if not hmac.compare_digest(auth.encode(), ('Bearer ' + mux._token).encode()):
self.send_error(401)
return
if self.headers.get('Origin') or self.headers.get('Transfer-Encoding'):
self.send_error(400)
return
lengths = self.headers.get_all('Content-Length', [])
if len(lengths) != 1 or not lengths[0].isdigit() or not 0 < int(lengths[0]) <= 65536:
self.send_error(400)
return
raw = self.rfile.read(int(lengths[0]))
if len(raw) != int(lengths[0]):
self.send_error(400)
return
if self.path == '/notice':
result = mux.submit(_object(raw))
elif self.path == '/status' and _object(raw) == {}:
result = mux.snapshot()
else:
self.send_error(404)
return
body = json.dumps(result).encode()
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
except (OSError, ValueError):
return
self._server = HTTPServer(('127.0.0.1', 0), Handler)
threading.Thread(target=self._server.serve_forever, daemon=True).start()
return f'http://127.0.0.1:{self._server.server_port}', self._token
def _put(self, item):
while not self._ended.is_set():
try:
self._queue.put(item, timeout=.1)
return True
except queue.Full:
pass
return False
def run(self, source: BinaryIO | None = None, target: BinaryIO | None = None) -> int:
if not self.observer:
return subprocess.call(self.argv) # exact inherited stdin/stdout/stderr/env/cwd
source = source if source is not None else sys.stdin.buffer
target = target if target is not None else sys.stdout.buffer
process = self._process = subprocess.Popen(self.argv, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
def read_client():
fragment = False
while not self._ended.is_set():
raw = source.readline(65536)
if not raw:
with self._lock:
self._eof = True
self._put(('eof', None))
return
complete = raw.endswith(b'\n')
if not fragment and complete:
self._observe_client(_object(raw))
elif not complete:
# Oversized records still forward byte-for-byte, but cannot be
# safely observed. Fail closed for injection for this connection.
with self._lock:
self._disabled = True
fragment = not complete
if not self._put(('raw', raw)):
return
def send_notice(value):
notice, key = value
with self._lock:
error = self._validate(notice)
if error:
self._receipts[key]['status'] = error
return
self._counter += 1
rid = self._prefix + str(self._counter)
self._pending[rid] = key
self._receipts[key]['status'] = 'sent_unknown'
raw = json.dumps({'id': rid, 'method': 'thread/realtime/appendSpeech',
'params': {'threadId': notice['thread_id'], 'text': notice['text']}},
ensure_ascii=True).encode() + b'\n'
# No lock during blocking pipe I/O: observer must drain server output.
# Protocol has no server-side generation precondition; this race remains.
process.stdin.write(raw)
process.stdin.flush()
def write_server():
partial = False
try:
while not self._ended.is_set():
try:
kind, value = self._queue.get(timeout=.1)
except queue.Empty:
continue
if kind == 'eof':
return
if kind == 'notice':
if partial:
# An oversized frame has disabled injection; reject now.
with self._lock:
self._receipts[value[1]]['status'] = 'injection_disabled'
else:
send_notice(value)
else:
# Re-apply lifecycle invalidation at the actual write boundary
# too: a prior server start may have arrived while queued.
if not partial and value.endswith(b'\n'):
obj = _object(value)
if obj.get('method') in ('thread/realtime/start', 'thread/realtime/stop'):
params = obj.get('params')
if isinstance(params, dict):
with self._lock:
self._invalidate(params.get('threadId'))
process.stdin.write(value)
process.stdin.flush()
partial = not value.endswith(b'\n')
except (OSError, ValueError):
pass
finally:
try:
process.stdin.close()
except OSError:
pass
reader = threading.Thread(target=read_client, daemon=True)
writer = threading.Thread(target=write_server, daemon=True)
reader.start()
writer.start()
fragment = False
try:
while True:
raw = process.stdout.readline(65536)
if not raw:
break
complete = raw.endswith(b'\n')
consumed = not fragment and complete and self._observe_server(_object(raw))
if not complete:
with self._lock:
self._disabled = True
fragment = not complete
if not consumed:
target.write(raw)
target.flush()
finally:
self._ended.set()
process.stdout.close()
with self._lock:
self._bindings.clear()
for st in self._threads.values():
st['active'] = False
for receipt in self._receipts.values():
if receipt['status'] == 'queued':
receipt['status'] = 'closed'
if self._server:
self._server.shutdown()
self._server.server_close()
return process.wait() # no deadline on legitimate native work
def main() -> int:
# Future launcher must pin/verify the original executable, never resolve the
# override recursively. No CODEX_CLI_PATH configuration is performed here.
if len(sys.argv) >= 5 and sys.argv[1] == '--pinned-original' and sys.argv[4] == '--':
try:
return launch_original(sys.argv[2], sys.argv[3], sys.argv[5:])
except ValueError:
print('original executable verification failed', file=sys.stderr)
return 2
if len(sys.argv) < 3 or sys.argv[2] != '--':
print('usage: native_speech_mux.py --pinned-original ORIGINAL_EXECUTABLE SHA256 -- ORIGINAL_ARGS...', file=sys.stderr)
return 2
argv = [sys.argv[1], *sys.argv[3:]]
return NativeSpeechMux(argv, observer=argv[1:] == ['app-server']).run()
if __name__ == '__main__':
raise SystemExit(main())
@@ -0,0 +1,42 @@
"""Owned startup entry. No user environment/config changes or override discovery."""
import json
import os
from pathlib import Path
import subprocess
import sys
import threading
from native_speech_mux import NativeSpeechMux, stdio_invocation, verified_original
from bekci_watch import Ledger, WatchOwner
def main(manifest, args):
cfg = json.loads(Path(manifest).read_text(encoding='utf-8-sig'))
original = verified_original(cfg['original'], cfg['original_sha256'])
if not stdio_invocation(args):
return subprocess.call([original,*args])
# Existing public catalog and original model passthrough handler is reused.
# A per-child CLI setting selects the ephemeral owned bridge; no global file edit.
saved_env = dict(os.environ)
os.environ['AITURK_CODEX_BRIDGE_HOME'] = cfg['bridge_home']
os.environ['HERMES_HOME'] = cfg['hermes_home']
try:
import kopru
finally:
os.environ.clear(); os.environ.update(saved_env)
from http.server import ThreadingHTTPServer
server = ThreadingHTTPServer(('127.0.0.1',0), kopru.H)
mux = NativeSpeechMux([original,*args,'-c',f'openai_base_url="http://127.0.0.1:{server.server_port}/backend-api/codex"'],observer=True)
Path(cfg['bridge_home']).mkdir(parents=True,exist_ok=True)
owner = WatchOwner(mux,Ledger(str(Path(cfg['bridge_home'])/'native-watch-ledger.sqlite3')))
kopru.WATCH_OWNER = owner
threading.Thread(target=server.serve_forever,daemon=True).start()
try:
return mux.run()
finally:
owner.close(); server.shutdown(); server.server_close()
if __name__ == '__main__':
try:
raise SystemExit(main(sys.argv[1],sys.argv[2:]))
except (OSError,ValueError,KeyError):
sys.stderr.write('Owned native launcher configuration/integrity failure.\n')
raise SystemExit(78)
@@ -0,0 +1,28 @@
"""Provision a reviewable launcher candidate. Never sets CODEX_CLI_PATH or restarts apps."""
import argparse
import hashlib
import json
from pathlib import Path
import shutil
def prepare(source, destination, original, expected_sha256, python, bridge_home, hermes_home):
source, destination = Path(source).resolve(), Path(destination).resolve()
original, python = Path(original).resolve(), Path(python).resolve()
def digest(p):
with p.open('rb') as f: return hashlib.file_digest(f,'sha256').hexdigest()
if digest(original) != expected_sha256.lower(): raise ValueError('chosen official original hash mismatch')
if original == destination/'native-watch.exe': raise ValueError('recursive original')
if source == destination: raise ValueError('candidate must use separate destination')
destination.mkdir(parents=True,exist_ok=True)
for p in source.rglob('*'):
if p.is_file() and (p.suffix in ('.py','.pyd') or p.name in ('native-watch.exe','LICENSE')) and not p.name.startswith('test_'):
target=destination/p.relative_to(source);target.parent.mkdir(parents=True,exist_ok=True);shutil.copy2(p,target)
script=destination/'native_watch_launcher.py'
manifest=dict(original=str(original),original_sha256=expected_sha256.lower(),python=str(python),python_sha256=digest(python),script=str(script),script_sha256=digest(script),bridge_home=str(Path(bridge_home).resolve()),hermes_home=str(Path(hermes_home).resolve()))
(destination/'native-watch.json').write_text(json.dumps(manifest,indent=2),encoding='utf-8')
return destination/'native-watch.exe'
if __name__=='__main__':
p=argparse.ArgumentParser(description=__doc__)
for name in ('source','destination','original','expected-sha256','python','bridge-home','hermes-home'): p.add_argument('--'+name,required=True)
args=vars(p.parse_args());print(prepare(**args))
@@ -23,7 +23,7 @@ class AccessPrograms(unittest.TestCase):
def setUp(self):
self.upstream = Mock(return_value=Mock(status=200))
self.env = {
"json": json,
"WATCH_OWNER": None, "WatchResponse": lambda response, accept: response, "json": json,
"urllib": Mock(request=Mock(Request=urllib.request.Request, urlopen=self.upstream)),
"decompress": lambda body, encoding: body,
"MODEL_MAP": {},
@@ -0,0 +1,258 @@
import concurrent.futures
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
from pathlib import Path
import subprocess
import sys
import threading
import time
import pytest
from bekci_watch import Frames, Ledger, MetadataFilter, WatchOwner, opaque
from native_speech_mux import NativeSpeechMux, stdio_invocation
from test_native_speech_mux import Harness
def wait(predicate):
until=time.time()+5
while time.time()<until:
if predicate(): return
time.sleep(.01)
assert predicate()
class Server:
def __init__(self, frames, status=200):
self.requests=[];self.frames=frames;self.status=status;self.closed=threading.Event()
owner=self
class H(BaseHTTPRequestHandler):
def log_message(self,*args): pass
def do_POST(self):
owner.requests.append((self.path,self.headers.get('Authorization'),json.loads(self.rfile.read(int(self.headers['Content-Length'])))))
self.send_response(owner.status);self.send_header('Content-Type','text/event-stream');self.end_headers()
try:
for frame in owner.frames:
self.wfile.write(frame);self.wfile.flush()
while not owner.closed.wait(.05):
self.wfile.write(b': keepalive\n\n');self.wfile.flush()
except OSError: pass
self.server=ThreadingHTTPServer(('127.0.0.1',0),H);self.server.daemon_threads=True
self.origin='http://127.0.0.1:'+str(self.server.server_port)
threading.Thread(target=self.server.serve_forever,daemon=True).start()
def close(self): self.closed.set();self.server.shutdown();self.server.server_close()
def frame(event,value,cursor=None):
return ((f'id: {cursor}\n' if cursor else '')+f'event: {event}\ndata: '+json.dumps(value)+'\n\n').encode()
def started(mux,tid):
mux._observe_server({'method':'thread/realtime/started','params':{'threadId':tid,'realtimeSessionId':'rt'}})
def meta(): return dict(subscription='s'*40,events_url='/v1/bekci/events',expires_at=(time.time()+60)*1000)
def capture(owner,tid): return owner.capture({'session-id':tid,'thread-id':tid},{'prompt_cache_key':tid})
def notice(i='one'): return dict(notice=dict(id=i,type='arastirma',metin='correction',at=time.time()),cursor='cursor-'+i)
def test_stream_metadata_isolation_split_crlf_and_json():
seen=[];f=MetadataFilter(seen.append);wire=frame('response.output_text.delta',{'delta':'answer'})+frame('bekci.watch',meta())+b'data: [DONE]\n\n'
wire=wire.replace(b'\n',b'\r\n');out=b''
for byte in wire: out+=f.feed(bytes([byte]))
out+=f.flush()
assert len(seen)==1 and b'subscription' not in out and b'answer' in out and b'[DONE]' in out
def test_ready_auth_durable_replay_and_epoch_abort(tmp_path):
n=notice();server=Server([frame('ready',{'cursor':'start'}),frame('notice',n,n['cursor']),frame('notice',n,n['cursor'])])
mux=NativeSpeechMux(['unused'],observer=True);started(mux,'A')
ledger=Ledger(tmp_path/'ledger.db');owner=WatchOwner(mux,ledger,origin=server.origin,test_loopback=True)
c=capture(owner,'A');p=meta()
try:
assert owner.accept(p,c,'member',server.origin+'/v1/responses')=='subscribing'
wait(lambda:mux._queue.qsize()==1)
scope=opaque('public-watch','member',p['subscription'])
wait(lambda:ledger.cursor(scope)==n['cursor'])
assert server.requests[0]==('/v1/bekci/events','Bearer member',{'subscription':p['subscription']})
reopened=Ledger(tmp_path/'ledger.db');assert not reopened.claim(scope,'one')
mux._observe_client({'method':'thread/realtime/stop','params':{'threadId':'A'}})
wait(lambda:not owner.watches)
assert owner.accept(p,c,'member',server.origin+'/v1/responses')=='stale_generation'
finally: owner.close();server.close()
def test_no_ready_wrong_key_untrusted_url_and_two_threads(tmp_path):
n=notice();server=Server([frame('notice',n,n['cursor'])]);mux=NativeSpeechMux(['unused'],observer=True)
for t in ('A','B'): started(mux,t)
owner=WatchOwner(mux,Ledger(tmp_path/'db'),origin=server.origin,test_loopback=True)
try:
c=capture(owner,'A')
assert owner.accept(meta(),c,'member','https://evil.invalid/responses')=='untrusted_route'
assert owner.accept(meta(),None,'member',server.origin)=='stale_generation'
p=meta();p['events_url']='https://evil.invalid';assert owner.accept(p,c,'member',server.origin)=='invalid_receipt'
owner.accept(meta(),c,'member',server.origin);wait(lambda:len(server.requests)>0)
time.sleep(.1);assert mux._queue.empty() and not mux._bindings
finally:owner.close();server.close()
server=Server([frame('ready',{'cursor':'start'}),frame('notice',n,n['cursor'])]);owner=WatchOwner(mux,Ledger(tmp_path/'db2'),origin=server.origin,test_loopback=True)
try:
for t in ('A','B'):
p=meta();p['subscription']=t*40;owner.accept(p,capture(owner,t),'member',server.origin)
wait(lambda:mux._queue.qsize()==2)
assert {mux._queue.get()[1][0]['thread_id'] for _ in range(2)}=={'A','B'}
finally:owner.close();server.close()
def test_launcher_stdio_grammar(tmp_path):
assert stdio_invocation(['-c','x=true','app-server','--analytics-default-enabled','-c','y="z"'])
assert not stdio_invocation(['exec','app-server'])
assert not stdio_invocation(['app-server','--listen','ws://127.0.0.1'])
assert not stdio_invocation(['app-server','daemon','start'])
@pytest.mark.parametrize('active_owner', [True, False])
def test_http_receipt_through_real_bridge_handler_to_real_pipe(tmp_path, active_owner):
"""Real imported H, real HTTP primary/events, real subprocess append/approval."""
import os
import http.client
import urllib.request
bridge=tmp_path/'bridge';bridge.mkdir()
(bridge/'bridge.json').write_text(json.dumps({'aiturk_hat':{'enabled':False},'sesle_model':False,'nokta_karsila':False,'yarim_cevap':False,'ozet_kopru':{'enabled':False},'tasma_bekcisi':False}),encoding='utf-8')
agent=tmp_path/'agent';agent.mkdir()
old=dict(os.environ);os.environ['AITURK_CODEX_BRIDGE_HOME']=str(bridge);os.environ['HERMES_HOME']=str(agent)
try:
import kopru
finally: os.environ.clear();os.environ.update(old)
h=Harness(tmp_path);h.start()
p=meta();n=notice();requests=[]
class Primary(BaseHTTPRequestHandler):
def log_message(self,*args): pass
def do_POST(self):
body=json.loads(self.rfile.read(int(self.headers['Content-Length'])))
requests.append((self.path,self.headers.get('Authorization'),body))
if self.path=='/v1/bekci/events':
wire=frame('ready',{'cursor':'start'})+frame('notice',n,n['cursor'])
kind='text/event-stream'
elif body.get('stream',True):
wire=frame('response.output_text.delta',{'type':'response.output_text.delta','delta':'answer'})+frame('bekci.watch',p)+b'data: [DONE]\n\n';kind='text/event-stream'
else:
wire=json.dumps({'output':[],'_bekci_watch':p}).encode();kind='application/json'
self.send_response(200);self.send_header('Content-Type',kind);self.send_header('Content-Length',str(len(wire)));self.end_headers();self.wfile.write(wire)
upstream=ThreadingHTTPServer(('127.0.0.1',0),Primary);upstream.daemon_threads=True
threading.Thread(target=upstream.serve_forever,daemon=True).start();origin='http://127.0.0.1:'+str(upstream.server_port)
owner=WatchOwner(h.mux,Ledger(tmp_path/'http-ledger'),origin=origin,test_loopback=True)
kopru.WATCH_OWNER=owner if active_owner else None;kopru.MODEL_MAP['ide/test-watch']=('test-route','Sesli-Hizli-Zeki',False)
kopru.ROUTES['test-route']={'url':origin+'/v1/responses','key':'public-member','flatten_tools':False}
server=kopru.TS(('127.0.0.1',0),kopru.H);threading.Thread(target=server.serve_forever,daemon=True).start()
try:
for stream in (True,False):
conn=http.client.HTTPConnection('127.0.0.1',server.server_port,timeout=5)
conn.request('POST','/v1/responses',json.dumps({'model':'ide/test-watch','stream':stream,'prompt_cache_key':'thread-A','input':[]}),{'Content-Type':'application/json','session-id':'thread-A','thread-id':'thread-A'})
resp=conn.getresponse();data=resp.read();conn.close()
assert resp.status==200 and b'subscription' not in data and b'_bekci_watch' not in data
if active_owner:
approval=json.loads(h.recv());speech=json.loads(h.recv())
assert approval['method']=='item/commandExecution/requestApproval'
assert speech['method']=='seen-speech' and speech['params']=={'threadId':'thread-A','text':'correction'}
assert any(path=='/v1/bekci/events' and auth=='Bearer public-member' for path,auth,_ in requests)
else:
assert not any(path=='/v1/bekci/events' for path,_,_ in requests)
finally:
owner.close();server.shutdown();server.server_close();upstream.shutdown();upstream.server_close();kopru.WATCH_OWNER=None;h.close()
def test_http_403_expiry_and_crash_claim_never_resend(tmp_path):
server=Server([],status=403);mux=NativeSpeechMux(['unused'],observer=True);started(mux,'A')
owner=WatchOwner(mux,Ledger(tmp_path/'db'),origin=server.origin,test_loopback=True)
try:
owner.accept(meta(),capture(owner,'A'),'wrong-key',server.origin)
wait(lambda:len(server.requests)==1);wait(lambda:not owner.watches)
assert not mux._bindings and mux._queue.empty()
finally:owner.close();server.close()
n=notice();server=Server([frame('ready',{'cursor':'start'}),frame('notice',n,n['cursor'])])
ledger=Ledger(tmp_path/'crash');p=meta();scope=opaque('public-watch','member',p['subscription']);assert ledger.claim(scope,'one')
owner=WatchOwner(mux,Ledger(tmp_path/'crash'),origin=server.origin,test_loopback=True)
try:
p['expires_at']=(time.time()+.4)*1000
owner.accept(p,capture(owner,'A'),'member',server.origin)
wait(lambda:owner.ledger.cursor(scope)==n['cursor']);wait(lambda:not owner.watches)
assert mux._queue.empty()
finally:owner.close();server.close()
def test_receipt_cannot_move_to_other_thread_or_generation(tmp_path):
server=Server([frame('ready',{'cursor':'start'})]);mux=NativeSpeechMux(['unused'],observer=True)
started(mux,'A');started(mux,'B');owner=WatchOwner(mux,Ledger(tmp_path/'db'),origin=server.origin,test_loopback=True);p=meta()
try:
assert owner.accept(p,capture(owner,'A'),'member',server.origin)=='subscribing'
assert owner.accept(p,capture(owner,'B'),'member',server.origin)=='receipt_replay_mismatch'
mux._observe_client({'method':'thread/unsubscribe','params':{'threadId':'A'}})
wait(lambda:not owner.watches);started(mux,'A')
assert owner.accept(p,capture(owner,'A'),'member',server.origin)=='receipt_replay_mismatch'
finally:owner.close();server.close()
@pytest.mark.parametrize('seconds', [16*60, 60*60, 24*60*60, 24*60*60+30])
def test_protocol_lifetime_receipt_admitted(tmp_path, seconds):
server=Server([frame('ready', {'cursor':'start'})])
mux=NativeSpeechMux(['unused'],observer=True);started(mux,'A')
owner=WatchOwner(mux,Ledger(tmp_path/'db'),origin=server.origin,test_loopback=True)
try:
p=meta();p['expires_at']=(time.time()+seconds)*1000
assert owner.accept(p,capture(owner,'A'),'member',server.origin)=='subscribing'
wait(lambda:len(server.requests)==1)
wait(lambda:bool(mux._bindings))
assert owner.watches
finally: owner.close();server.close()
@pytest.mark.parametrize('expiry', [0, -1, float('inf'), float('-inf'), float('nan'), True, False, 'infinite', None, 10**100])
def test_invalid_or_unbounded_expiry_rejected(tmp_path, expiry):
mux=NativeSpeechMux(['unused'],observer=True);started(mux,'A')
owner=WatchOwner(mux,Ledger(tmp_path/'db'))
p=meta();p['expires_at']=expiry
assert owner.accept(p,capture(owner,'A'),'member','https://ai.turkservis.online/v1/responses')=='invalid_receipt'
assert not owner.watches
def test_receipt_beyond_protocol_and_skew_rejected(tmp_path):
mux=NativeSpeechMux(['unused'],observer=True);started(mux,'A')
owner=WatchOwner(mux,Ledger(tmp_path/'db'))
p=meta();p['expires_at']=(time.time()+24*60*60+120)*1000
assert owner.accept(p,capture(owner,'A'),'member','https://ai.turkservis.online/v1/responses')=='invalid_receipt'
@pytest.mark.parametrize('target', ['Other-Model', 'sesli-hizli-zeki', 'Sesli-Hizli-Zeki-extra'])
@pytest.mark.parametrize('json_response', [False, True])
@pytest.mark.parametrize('jev', [False, True])
def test_actual_handler_nonvoice_preserves_response_pipeline(tmp_path, monkeypatch, target, json_response, jev):
import http.client
import kopru
from unittest.mock import Mock
# Exceed the sideband frame cap: other models must never enter its buffer.
wire=(json.dumps({'output':[], 'payload':'x'*270000}).encode() if json_response
else frame('response.output_text.delta', {'delta':'x'*270000}))
class Primary(BaseHTTPRequestHandler):
def log_message(self,*args): pass
def do_POST(self):
self.rfile.read(int(self.headers['Content-Length']))
self.send_response(200)
self.send_header('Content-Type','application/json' if json_response else 'text/event-stream')
self.send_header('Content-Length',str(len(wire)));self.end_headers();self.wfile.write(wire)
upstream=ThreadingHTTPServer(('127.0.0.1',0),Primary)
threading.Thread(target=upstream.serve_forever,daemon=True).start()
owner=Mock();monkeypatch.setattr(kopru,'WATCH_OWNER',owner)
monkeypatch.setitem(kopru.MODEL_MAP,'ide/scope-proof',('aiturk-ide',target,None))
monkeypatch.setitem(kopru.ROUTES,'aiturk-ide',{'url':f'http://127.0.0.1:{upstream.server_port}/v1/responses','key':'scope-member','flatten_tools':jev})
monkeypatch.setattr(kopru.jev_denetim,'anahtar',lambda:'test-key' if jev else '')
wrapped=Mock(side_effect=AssertionError('nonvoice entered watch wrapper'))
monkeypatch.setattr(kopru,'WatchResponse',wrapped)
shortcut=Mock(side_effect=AssertionError('nonvoice entered JSON shortcut'))
monkeypatch.setattr(kopru.H,'_send_json',shortcut)
calls=[]
original_stream=kopru.H._stream
def stream(self,resp,*args,**kwargs):
calls.append('stream')
return original_stream(self,resp,*args,**kwargs)
def denetim(self,resp,tag,rewriter,izle,istek,sira,ac,*,hedef):
# Verify original upstream object/body reaches Jev, not a watch wrapper.
calls.append('jev');assert hedef==target and sira
data=resp.read();resp.close();assert data==wire
self.send_response(200);self.send_header('Content-Length',str(len(data)))
self.end_headers();self.wfile.write(data)
monkeypatch.setattr(kopru.H,'_stream',stream)
monkeypatch.setattr(kopru.H,'_denetimli_akis',denetim)
server=kopru.TS(('127.0.0.1',0),kopru.H)
threading.Thread(target=server.serve_forever,daemon=True).start()
try:
conn=http.client.HTTPConnection('127.0.0.1',server.server_port,timeout=5)
conn.request('POST','/v1/responses',json.dumps({'model':'ide/scope-proof','stream':not json_response,'input':[]}),{'Content-Type':'application/json'})
response=conn.getresponse();data=response.read();conn.close()
assert response.status==200 and data==wire
assert calls==(['jev'] if jev else ['stream'])
wrapped.assert_not_called();shortcut.assert_not_called();owner.accept.assert_not_called()
finally:
server.shutdown();server.server_close();upstream.shutdown();upstream.server_close()
@@ -27,7 +27,7 @@ class ComboNotResent(unittest.TestCase):
def setUp(self):
self.upstream = Mock()
self.env = {
"json": json, "re": __import__("re"), "os": __import__("os"), "http": __import__("http.client"),
"WATCH_OWNER": None, "WatchResponse": lambda response, accept: response, "json": json, "re": __import__("re"), "os": __import__("os"), "http": __import__("http.client"),
"time": types.SimpleNamespace(sleep=lambda s: None),
"urllib": types.SimpleNamespace(
request=types.SimpleNamespace(Request=urllib.request.Request, urlopen=self.upstream),
@@ -164,7 +164,7 @@ class Ozet(unittest.TestCase):
class Akis(unittest.TestCase):
env = build(top("fix_tool_ids", "SseRewriter", "phase_damgala", "asistan_mesaji_mi", "ID_PREFIX", "ARAC_OGELERI"),
{"json": json, "re": __import__("re"), "time": time, "log": lambda m: None, "pencere": pencere})
{"WATCH_OWNER": None, "WatchResponse": lambda response, accept: response, "json": json, "re": __import__("re"), "time": time, "log": lambda m: None, "pencere": pencere})
def events(self, raw):
return [json.loads(line[6:]) for line in raw.decode().splitlines() if line.startswith("data: ")]
@@ -256,7 +256,7 @@ class Zincir(unittest.TestCase):
"claude_sonu_duzelt", "ek_model_mu", "yonlendirilen")
+ [method("_responses"), method("_model_komutu"), method("_ozgun")], {
"gecit_modelleri": lambda: [],
"json": json, "re": __import__("re"), "os": __import__("os"), "copy": copy, "http": __import__("http.client"),
"WATCH_OWNER": None, "WatchResponse": lambda response, accept: response, "json": json, "re": __import__("re"), "os": __import__("os"), "copy": copy, "http": __import__("http.client"),
"time": types.SimpleNamespace(sleep=lambda s: None, time=time.time),
"urllib": types.SimpleNamespace(request=types.SimpleNamespace(Request=urllib.request.Request, urlopen=Mock()),
error=urllib.error),
@@ -389,6 +389,22 @@ class Zincir(unittest.TestCase):
self.assertEqual(sent["instructions"], req["instructions"])
self.assertEqual(sent["input"][0], req["input"][0])
def test_nonvoice_json_still_reaches_original_jev_with_upstream_identity(self):
self.env["ROUTES"]["aiturk-ide"] = {
"url": "https://example.invalid/v1/responses", "key": "test-only", "flatten_tools": True}
self.env["MODEL_MAP"]["ide/scope"] = ("aiturk-ide", "Other-Model", None)
response = Mock(status=200, headers={"Content-Type": "application/json"})
self.env["urllib"].request.urlopen.return_value = response
watch = self.env["WatchResponse"] = Mock(side_effect=AssertionError("watch scope escaped"))
fake = Sahte(); fake._denetimli_akis = Mock()
with patch.object(self.env["jev_denetim"], "anahtar", return_value="test-only"):
fake._responses(1, "/responses", json.dumps({"model":"ide/scope", "input":[user("task")]}).encode())
fake._denetimli_akis.assert_called_once()
self.assertIs(fake._denetimli_akis.call_args.args[0], response)
self.assertEqual(fake._denetimli_akis.call_args.kwargs, {"hedef":"Other-Model"})
response.read.assert_not_called()
watch.assert_not_called(); fake._send_json.assert_not_called(); fake._stream.assert_not_called()
def test_combo_dropped_after_sending_is_not_resent_but_a_single_model_is(self):
import http.client
self.external(http.client.RemoteDisconnected("closed"))
@@ -438,7 +454,7 @@ class Govde(unittest.TestCase):
class Liste(unittest.TestCase):
def env(self, **extra):
env = {"json": json, "copy": copy, "hashlib": hashlib, "time": time, "log": lambda m: None, "CFG": {},
env = {"WATCH_OWNER": None, "WatchResponse": lambda response, accept: response, "json": json, "copy": copy, "hashlib": hashlib, "time": time, "log": lambda m: None, "CFG": {},
"decompress": lambda raw, enc: raw, "EXTRA_MODELS": [], "IDE_PREFIX": "ide/", "SLIM_CLONES": False,
"SLIM_FIELDS": [], "ide_catalog": ide_catalog, "LAST_MODELS": {}, "save_last_models": Mock(),
"RESP_HOP": {"content-length", "content-encoding"}, "REQ_HOP": {"host"}, "CHATGPT": "https://example.invalid",
@@ -0,0 +1,358 @@
"""Real pipes/processes and real loopback HTTP; no native runtime activation."""
import concurrent.futures
import http.client
import json
import os
from pathlib import Path
import queue
import subprocess
import sys
import threading
import time
import native_speech_mux as muxmod
# Fake engine is a separate process speaking complete JSONL, not mocked Popen.
ENGINE = r'''
import json, os, sys, time
if '--probe' in sys.argv:
print(json.dumps({'argv':sys.argv[1:], 'cwd':os.getcwd(), 'marker':os.getenv('MUX_TEST_MARKER')}))
sys.stderr.write('original-stderr\n')
sys.exit(23)
for raw in sys.stdin.buffer:
try: m=json.loads(raw)
except ValueError:
sys.stdout.buffer.write(raw); sys.stdout.buffer.flush(); continue
method=m.get('method')
def emit(v):
sys.stdout.buffer.write(json.dumps(v).encode()+b'\n'); sys.stdout.buffer.flush()
if method=='thread/start':
emit({'id':m['id'],'result':{'thread':{'id':'thread-A'}}})
elif method=='thread/realtime/start':
emit({'method':'thread/realtime/started','params':{'threadId':'thread-A','realtimeSessionId':'rt'}})
elif method=='thread/realtime/stop':
emit({'method':'thread/realtime/closed','params':{'threadId':'thread-A','reason':None}})
elif method=='error-event':
emit({'method':'thread/realtime/error','params':{'threadId':'thread-A','message':'failed'}})
elif method=='thread/realtime/appendSpeech':
emit({'method':'item/commandExecution/requestApproval','id':m['id'],'params':{'command':'test'}})
emit({'method':'seen-speech','params':m['params']})
emit({'id':m['id'], 'error':{'code':-1,'message':'no'}} if m['params']['text']=='rpc-error' else {'id':m['id'],'result':{}})
elif method=='delay':
time.sleep(.25)
sys.stdout.buffer.write(raw); sys.stdout.buffer.flush()
elif method=='pause':
emit({'method':'paused'})
while not os.path.exists(m['params']['release']): time.sleep(.01)
emit({'method':'released'})
elif method=='exit':
sys.exit(17)
else:
sys.stdout.buffer.write(raw); sys.stdout.buffer.flush()
sys.stdout.buffer.write(b'{"method":"saw-eof"}\n'); sys.stdout.buffer.flush()
sys.stderr.write('engine-eof\n')
sys.exit(19)
'''
class Harness:
def __init__(self, tmp_path):
engine = tmp_path / 'engine.py'
engine.write_text(ENGINE)
self.mux = muxmod.NativeSpeechMux([sys.executable, '-u', str(engine), 'app-server'], observer=True)
ir, iw = os.pipe()
outr, outw = os.pipe()
self.source = os.fdopen(ir, 'rb', buffering=0)
self.input = os.fdopen(iw, 'wb', buffering=0)
self.target = os.fdopen(outw, 'wb', buffering=0)
self.output = os.fdopen(outr, 'rb')
self.lines = queue.Queue()
self.code = None
self.error = None
self.url, self.token = self.mux.start_inlet()
def run():
try:
self.code = self.mux.run(self.source, self.target)
except BaseException as e:
self.error = e
finally:
self.target.close()
self.thread = threading.Thread(target=run, daemon=True)
self.thread.start()
def collect():
for raw in self.output:
self.lines.put(raw)
self.collector = threading.Thread(target=collect, daemon=True)
self.collector.start()
def send(self, value):
raw = value if isinstance(value, bytes) else json.dumps(value).encode()+b'\n'
self.input.write(raw)
def recv(self):
return self.lines.get(timeout=8)
def http(self, path, body, token=None):
conn = http.client.HTTPConnection('127.0.0.1', int(self.url.rsplit(':', 1)[1]), timeout=5)
conn.request('POST', path, json.dumps(body), {'Authorization':'Bearer '+(token if token is not None else self.token)})
response = conn.getresponse()
data = response.read()
conn.close()
return response.status, json.loads(data) if response.status == 200 else data
def start(self):
self.send({'id':1, 'method':'thread/start','params':{}})
assert json.loads(self.recv())['result']['thread']['id'] == 'thread-A'
assert self.mux.snapshot()['threads']['thread-A']['active'] is False
self.send({'id':2,'method':'thread/realtime/start','params':{'threadId':'thread-A'}})
assert json.loads(self.recv())['method'] == 'thread/realtime/started'
return self.bind()
def bind(self):
generation = self.mux.snapshot()['threads']['thread-A']['generation']
b = muxmod.Binding('tenant','bekci','rev',self.mux.connection_id,'thread-A',generation)
assert self.mux.bind(b)
return self.notice(generation)
def notice(self, generation=1, **changes):
now = time.time()
return dict(notice_id='notice-1',tenant_id='tenant',bekci_session_id='bekci',history_revision='rev',
connection_id=self.mux.connection_id,thread_id='thread-A',expected_generation=generation,
issued_at=now,expires_at=now+30,text='düzeltme', **changes)
def close(self):
self.input.close()
self.thread.join(10)
if self.thread.is_alive():
self.mux._process.kill()
self.thread.join(5)
raise AssertionError('mux failed to exit after EOF')
self.collector.join(3)
self.source.close()
self.output.close()
assert self.error is None
def test_native_notice_rpc_and_approval_are_separate(tmp_path):
h = Harness(tmp_path)
try:
notice = h.start()
assert h.http('/notice', notice, 'wrong')[0] == 401
assert h.http('/notice', notice)[1]['status'] == 'queued'
approval = json.loads(h.recv())
assert approval['method'] == 'item/commandExecution/requestApproval'
assert approval['id'].startswith('bekci-speech-')
assert json.loads(h.recv()) == {'method':'seen-speech','params':{'threadId':'thread-A','text':'düzeltme'}}
answer = (json.dumps({'id':approval['id'],'result':{'decision':'decline'}}, separators=(', ', ': '))+'\r\n').encode()
h.send(answer)
assert h.recv() == answer
assert h.http('/notice', notice)[1]['status'] == 'rpc_accepted'
assert h.lines.empty() # native own-result never leaked to desktop
assert h.http('/rpc', {'method':'turn/start'})[0] == 404
assert h.http('/notice', {**notice,'notice_id':'err','text':'rpc-error'})[1]['status'] == 'queued'
h.recv(); h.recv()
h.send(b'{"method":"barrier"}\n')
assert h.recv() == b'{"method":"barrier"}\n'
assert h.http('/notice', {**notice,'notice_id':'err','text':'rpc-error'})[1]['status'] == 'rpc_error'
finally:
h.close()
assert h.code == 19
assert json.loads(h.recv())['method'] == 'saw-eof'
def test_unbound_schema_ttl_tenant_generation_and_restart(tmp_path):
h = Harness(tmp_path)
try:
n = h.notice()
assert h.http('/notice', n)[1]['status'] == 'binding_not_established'
n = h.start()
for delta, status in [({'method':'turn/start'},'invalid_schema'),
({'expected_generation':True},'invalid_schema'),
({'expires_at':time.time()-1},'expired_or_invalid_ttl'),
({'issued_at':float('nan')},'expired_or_invalid_ttl'),
({'tenant_id':'other'},'binding_not_established'),
({'history_revision':'other'},'binding_mismatch'),
({'expected_generation':2},'binding_mismatch'),
({'connection_id':'other'},'binding_mismatch')]:
assert h.http('/notice', {**n, **delta})[1]['status'] == status
h.send({'method':'thread/realtime/stop','params':{'threadId':'thread-A'}})
assert json.loads(h.recv())['method'] == 'thread/realtime/closed'
assert h.http('/notice', n)[1]['status'] == 'binding_not_established'
h.send({'method':'thread/realtime/start','params':{'threadId':'thread-A'}})
h.recv()
fresh = h.bind()
assert fresh['expected_generation'] == n['expected_generation']+1
assert h.http('/notice', n)[1]['status'] == 'binding_mismatch'
h.send({'method':'error-event'})
h.recv()
assert h.http('/notice', fresh)[1]['status'] == 'binding_not_established'
finally:
h.close()
def test_concurrent_notices_and_raw_backpressure_order(tmp_path):
h = Harness(tmp_path)
try:
n = h.start()
h.send({'method':'delay'})
records = [(json.dumps({'method':'echo','i':i,'padding':'x'*16000})+'\r\n').encode() for i in range(90)]
producer = threading.Thread(target=lambda: [h.send(r) for r in records], daemon=True)
producer.start()
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
results = list(pool.map(lambda i: h.http('/notice',{**n,'notice_id':str(i)})[1]['status'], range(12)))
assert set(results) <= {'queued','busy'}
accepted = results.count('queued')
echoes = []
speech = 0
approvals = 0
for _ in range(1 + len(records) + accepted*2):
raw = h.recv()
method = json.loads(raw).get('method')
if method == 'echo': echoes.append(raw)
elif method == 'seen-speech': speech += 1
elif method == 'item/commandExecution/requestApproval': approvals += 1
producer.join(5)
assert not producer.is_alive()
assert echoes == records
assert speech == approvals == accepted
assert h.mux._queue.maxsize == 32
finally:
h.close()
def test_fragmented_oversized_jsonl_and_partial_eof_preserved(tmp_path):
h = Harness(tmp_path)
try:
h.start()
raw = b'{ "method":"huge", "value":"' + b'x'*200000 + b'"}\r\n'
producer = threading.Thread(target=lambda: h.send(raw),daemon=True)
producer.start()
assert h.recv() == raw
producer.join(5)
assert h.mux.snapshot()['injection_disabled']
h.send(b'{"partial":true}')
finally:
h.close()
# Engine echoes an unterminated record, then its EOF notification: byte exact.
assert h.recv() == b'{"partial":true}{"method":"saw-eof"}\n'
assert h.code == 19
def test_non_appserver_passthrough_argv_env_cwd_stderr_exit(tmp_path, monkeypatch):
engine = tmp_path / 'engine.py'
engine.write_text(ENGINE)
module = Path(muxmod.__file__).resolve()
monkeypatch.setenv('MUX_TEST_MARKER','inherited')
args = [sys.executable,str(module),sys.executable,'--','-u',str(engine),'--probe','space arg','app-server']
p = subprocess.run(args,cwd=tmp_path,capture_output=True,timeout=10)
assert p.returncode == 23
result = json.loads(p.stdout)
assert result == {'argv':['--probe','space arg','app-server'],'cwd':str(tmp_path),'marker':'inherited'}
direct = subprocess.run([sys.executable, '-u', str(engine), '--probe', 'space arg', 'app-server'],
cwd=tmp_path, capture_output=True, timeout=10)
assert (p.stdout, p.stderr, p.returncode) == (direct.stdout, direct.stderr, direct.returncode)
def test_engine_exit_without_desktop_eof(tmp_path):
h = Harness(tmp_path)
try:
h.send({'method':'exit'})
h.thread.join(5)
assert not h.thread.is_alive()
assert h.code == 17
assert h.mux.snapshot()['closed']
finally:
h.close()
def test_owned_namespace_collision_disables_injection_but_forwards(tmp_path):
h = Harness(tmp_path)
try:
n = h.start()
raw = json.dumps({'id':h.mux._prefix+'collision','method':'echo'}).encode()+b'\n'
h.send(raw)
assert h.recv() == raw
assert h.http('/notice', n)[1]['status'] == 'injection_disabled'
finally:
h.close()
def test_queued_notice_revalidated_after_observed_stop(tmp_path):
h = Harness(tmp_path)
release = tmp_path / 'release'
try:
n = h.start()
h.send({'method':'pause','params':{'release':str(release)}})
assert json.loads(h.recv())['method'] == 'paused'
# Fill the OS pipe while the real engine is paused; leave bounded queue
# capacity for the notice and stop record behind it.
for i in range(8):
h.send({'method':'echo','i':i,'padding':'x'*32000})
assert h.http('/notice', n)[1]['status'] == 'queued'
h.send({'method':'thread/realtime/stop','params':{'threadId':'thread-A'}})
deadline = time.monotonic()+5
while h.mux.snapshot()['threads']['thread-A']['active'] and time.monotonic() < deadline:
time.sleep(.01)
assert not h.mux.snapshot()['threads']['thread-A']['active']
release.touch()
methods = [json.loads(h.recv())['method'] for _ in range(10)]
assert methods == ['released'] + ['echo']*8 + ['thread/realtime/closed']
assert h.mux._receipts[('tenant','bekci','notice-1')]['status'] == 'binding_not_established'
assert h.lines.empty()
finally:
release.touch()
h.close()
def test_real_request_correlation_and_opaque_receipt_contract(tmp_path):
h = Harness(tmp_path)
try:
n = h.start()
adapter = muxmod.WatchReceiptAdapter(h.mux)
headers = {'Session-Id':'thread-A','Thread-Id':'thread-A'}
correlation = adapter.capture(headers, {'prompt_cache_key':'thread-A'})
assert correlation.thread_id == 'thread-A'
assert adapter.capture({**headers,'Thread-Id':'other'}, {'prompt_cache_key':'thread-A'}) is None
assert adapter.capture(headers, {}) is None
assert adapter.accept('bekci.watch', {}, correlation) == 'backend_contract_not_established'
# Internal contract fixture, explicitly NOT a claim about backend wire format.
receipt = muxmod.WatchReceipt('opaque','tenant','bekci','rev','thread-A',time.time()+60)
adapter.decode = lambda payload: payload
adapter.resolve = lambda handle: receipt
assert adapter.accept('bekci.watch','opaque',correlation) == 'bound'
assert adapter.accept('_bekci_watch','opaque',correlation) == 'bound'
assert adapter.accept('other','opaque',correlation) == 'unsupported_metadata_transport'
assert adapter.notice('unbound',n)['status'] == 'binding_not_established'
assert adapter.notice('opaque',{**n,'tenant_id':'other'})['status'] == 'binding_mismatch'
assert adapter.notice('opaque',n)['status'] == 'queued'
assert json.loads(h.recv())['method'] == 'item/commandExecution/requestApproval'
assert json.loads(h.recv())['method'] == 'seen-speech'
h.send({'method':'thread/realtime/stop','params':{'threadId':'thread-A'}}); h.recv()
h.send({'method':'thread/realtime/start','params':{'threadId':'thread-A'}}); h.recv()
assert adapter.accept('bekci.watch','opaque',correlation) == 'stale_generation'
fresh = adapter.capture(headers, {'prompt_cache_key':'thread-A'})
adapter.resolve = lambda handle: muxmod.WatchReceipt('opaque','tenant','bekci','rev','wrong',time.time()+60)
assert adapter.accept('bekci.watch','opaque',fresh) == 'receipt_mismatch_or_expired'
adapter.resolve = lambda handle: muxmod.WatchReceipt('opaque','tenant','bekci','rev','thread-A',time.time()-1)
assert adapter.accept('bekci.watch','opaque',fresh) == 'receipt_mismatch_or_expired'
finally:
h.close()
def test_verified_original_launcher_fallback_actual_process(tmp_path):
import hashlib
executable = str(Path(sys.executable).resolve())
with open(executable,'rb') as f:
digest = hashlib.file_digest(f,'sha256').hexdigest()
assert muxmod.verified_original(executable,digest) == executable
import pytest
with pytest.raises(ValueError): muxmod.verified_original(executable,'0'*64)
with pytest.raises(ValueError): muxmod.verified_original('python.exe',digest)
# A value that happens to say app-server must not trigger observer/attachment.
marker = tmp_path/'original-ran'
code = 'from pathlib import Path; import sys; Path(sys.argv[1]).write_text("ok"); sys.exit(21)'
def forbidden(adapter):
raise AssertionError('not an app-server invocation')
assert muxmod.launch_original(executable,digest,['-c',code,str(marker),'app-server'],attach=forbidden) == 21
assert marker.read_text() == 'ok'
@@ -0,0 +1,116 @@
"""Windows owned-process lifetime tests; no installed process is terminated."""
import ctypes
from ctypes import wintypes as w
import hashlib
import json
import os
from pathlib import Path
import queue
import subprocess
import sys
import threading
import time
import pytest
from prepare_native_watch import prepare
pytestmark = pytest.mark.skipif(sys.platform != 'win32', reason='Windows executable lifecycle')
def descendants(parent):
class Entry(ctypes.Structure):
_fields_=[('size',w.DWORD),('usage',w.DWORD),('pid',w.DWORD),('heap',ctypes.c_size_t),('module',w.DWORD),('threads',w.DWORD),('parent',w.DWORD),('priority',w.LONG),('flags',w.DWORD),('exe',w.WCHAR*260)]
k=ctypes.WinDLL('kernel32',use_last_error=True)
k.CreateToolhelp32Snapshot.restype=w.HANDLE
k.Process32FirstW.argtypes=[w.HANDLE,ctypes.POINTER(Entry)]
k.Process32NextW.argtypes=[w.HANDLE,ctypes.POINTER(Entry)]
k.CloseHandle.argtypes=[w.HANDLE]
snap=k.CreateToolhelp32Snapshot(2,0);entry=Entry();entry.size=ctypes.sizeof(entry);rows=[]
try:
more=k.Process32FirstW(snap,ctypes.byref(entry))
while more:
rows.append((entry.pid,entry.parent));more=k.Process32NextW(snap,ctypes.byref(entry))
finally:k.CloseHandle(snap)
owned={parent}
while True:
children={pid for pid,ppid in rows if ppid in owned}
if children <= owned: return owned-{parent}
owned |= children
def assert_abrupt_cleanup(p, expected_min):
k=ctypes.WinDLL('kernel32',use_last_error=True)
k.OpenProcess.argtypes=[w.DWORD,w.BOOL,w.DWORD];k.OpenProcess.restype=w.HANDLE
k.WaitForSingleObject.argtypes=[w.HANDLE,w.DWORD]
k.CloseHandle.argtypes=[w.HANDLE]
ids=descendants(p.pid)
assert len(ids)>=expected_min, ids
handles=[k.OpenProcess(0x100000,False,pid) for pid in ids]
assert all(handles)
try:
p.kill();p.wait(timeout=5)
assert all(k.WaitForSingleObject(h,5000)==0 for h in handles), ids
finally:
for h in handles:k.CloseHandle(h)
return len(ids)
def line(p):
q=queue.Queue();threading.Thread(target=lambda:q.put(p.stdout.readline()),daemon=True).start()
return q.get(timeout=20)
def candidate(tmp_path, original, digest):
return prepare(Path(__file__).parent,tmp_path/'candidate',original,digest,Path(sys.executable),tmp_path/'bridge',tmp_path/'agent')
def test_abrupt_bootstrap_exit_kills_owned_python_and_grandchild(tmp_path):
exe=Path(sys.executable)
launcher=candidate(tmp_path,exe,hashlib.sha256(exe.read_bytes()).hexdigest())
script='import subprocess,sys,time; p=subprocess.Popen([sys.executable,"-c","import time; time.sleep(120)"]); print(p.pid,flush=True); time.sleep(120)'
p=subprocess.Popen([str(launcher),'-u','-c',script],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
try:
assert int(line(p))>0
assert_abrupt_cleanup(p,3)
finally:
if p.poll() is None:p.kill();p.wait(timeout=5)
p.stdin.close();p.stdout.close();p.stderr.close()
def test_pinned_original_initialize_and_abrupt_cleanup(tmp_path):
# Explicit provisioned evidence, never discovers or kills an existing app.
manifest_path=os.environ.get('NATIVE_WATCH_TEST_MANIFEST')
if not manifest_path: pytest.skip('requires explicit pinned original candidate')
manifest=json.loads(Path(manifest_path).read_text(encoding='utf-8-sig'))
launcher=candidate(tmp_path,Path(manifest['original']),manifest['original_sha256'])
p=subprocess.Popen([str(launcher),'-c','model_provider="openai"','app-server','--analytics-default-enabled'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
try:
p.stdin.write(json.dumps({'id':1,'method':'initialize','params':{'clientInfo':{'name':'owned-cleanup-proof','version':'1.0'},'capabilities':{'experimentalApi':True}}}).encode()+b'\n');p.stdin.flush()
response=json.loads(line(p));assert response.get('id')==1 and 'result' in response,response
assert_abrupt_cleanup(p,2)
finally:
if p.poll() is None:p.kill();p.wait(timeout=5)
p.stdin.close();p.stdout.close();p.stderr.close()
def test_compiled_launcher_fallback(tmp_path):
source=Path(__file__).parent
import hashlib
exe=Path(sys.executable);digest=hashlib.sha256(exe.read_bytes()).hexdigest()
launcher=prepare(source,tmp_path/'candidate',exe,digest,exe,tmp_path/'bridge',tmp_path/'agent')
args=['-c','import json,sys,os; print(json.dumps([sys.argv[1:],os.getcwd()])); sys.stderr.write("stderr\\n"); sys.exit(23)','space arg','quote"arg','trailing\\']
direct=subprocess.run([str(exe),*args],cwd=tmp_path,capture_output=True)
wrapped=subprocess.run([str(launcher),*args],cwd=tmp_path,capture_output=True)
assert (wrapped.returncode,wrapped.stdout,wrapped.stderr)==(direct.returncode,direct.stdout,direct.stderr)
manifest=launcher.with_suffix('.json');data=json.loads(manifest.read_text(encoding='utf-8'));data['original_sha256']='0'*64;manifest.write_text(json.dumps(data),encoding='utf-8')
assert subprocess.run([str(launcher),'--version'],capture_output=True).returncode==78
def test_executable_flushes_interactive_stdin_before_eof(tmp_path):
import hashlib,queue
exe=Path(sys.executable);digest=hashlib.sha256(exe.read_bytes()).hexdigest()
launcher=prepare(Path(__file__).parent,tmp_path/'candidate',exe,digest,exe,tmp_path/'bridge',tmp_path/'agent')
p=subprocess.Popen([str(launcher),'-u','-c','import sys; print(sys.stdin.readline().strip(),flush=True); sys.stdin.read()'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
q=queue.Queue();threading.Thread(target=lambda:q.put(p.stdout.readline()),daemon=True).start()
try:
p.stdin.write(b'live-before-eof\n');p.stdin.flush()
assert q.get(timeout=5).strip()==b'live-before-eof'
finally:
p.stdin.close();p.wait(timeout=5)
+1 -1
View File
@@ -27,7 +27,7 @@ export const CODEX_LINK_REFRESH_URL = `${CODEX_LINK_ORIGIN}/aiturk/codex-baglama
export const CODEX_LINK_MARKER = '# AITURK IDE Codex Baglama'
// Must equal SURUM in assets/codex-bridge/bridge.py; a running bridge that reports
// another version is restarted so an updated IDE never keeps an old bridge alive.
export const CODEX_BRIDGE_VERSION = '9'
export const CODEX_BRIDGE_VERSION = '10'
export const CODEX_LINK_LINE = `openai_base_url = "${CODEX_LINK_ORIGIN}/backend-api/codex" ${CODEX_LINK_MARKER}`
export const CODEX_LINK_RUN_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'
+2 -1
View File
@@ -2,7 +2,7 @@
"name": "aiturk-ide",
"productName": "AITURK IDE",
"private": true,
"version": "1.0.0-beta.29",
"version": "1.0.0-beta.30",
"description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.",
"author": "AITURK / TurkServis",
"repository": {
@@ -306,6 +306,7 @@
"filter": [
"**/*.py",
"**/*.pyd",
"native-watch.exe",
"**/LICENSE",
"!test_*.py"
]
+2
View File
@@ -57,6 +57,7 @@
* - electronPlatformName: 'win32' | 'darwin' | 'linux'
* - arch: Arch enum (0=ia32, 1=x64, 2=armv7l, 3=arm64, 4=universal)
*/
import { buildNativeWatchLauncher } from './build-native-watch-launcher.mjs'
import { existsSync, rmSync, renameSync } from 'node:fs'
import path from 'node:path'
import { Arch } from 'electron-builder'
@@ -111,6 +112,7 @@ export function preserveRollbackBackup(appOutDir, productExeName = 'Hermes.exe')
}
export default async function beforePack(context) {
if (context?.electronPlatformName === 'win32') buildNativeWatchLauncher()
const appOutDir = context && context.appOutDir
const platformName = context && context.electronPlatformName
try {
@@ -0,0 +1,30 @@
/** Build the Windows artifact from source on Windows or the Linux/Wine CI host. */
import { execFileSync } from 'node:child_process'
import { rmSync, readFileSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const sourceHome = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../assets/codex-bridge')
export function compilerCommand({ platform = process.platform, env = process.env, source, output }) {
const args = ['/nologo', '/target:exe', '/platform:anycpu', '/reference:System.Web.Extensions.dll', `/out:${output}`, source]
if (platform === 'win32') {
return { command: path.win32.join(env.SystemRoot || 'C:/Windows', 'Microsoft.NET/Framework64/v4.0.30319/csc.exe'), args }
}
if (platform === 'linux') return { command: 'mcs', args }
throw new Error(`Windows launcher compilation is not configured on ${platform}`)
}
export function buildNativeWatchLauncher({ home = sourceHome, ...options } = {}) {
const output = path.join(home, 'native-watch.exe')
const invocation = compilerCommand({ ...options, source: path.join(home, 'NativeWatchBootstrap.cs'), output })
// A missing compiler or broken source must fail packaging, never reuse an old EXE.
rmSync(output, { force: true })
execFileSync(invocation.command, invocation.args, { windowsHide: true, stdio: 'inherit' })
const bytes = readFileSync(output)
if (bytes.length < 64 || bytes.toString('ascii', 0, 2) !== 'MZ' || bytes.toString('ascii', bytes.readUInt32LE(60), bytes.readUInt32LE(60) + 4) !== 'PE\0\0') {
rmSync(output, { force: true })
throw new Error('Native watch compiler did not produce a Windows PE executable')
}
return output
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) buildNativeWatchLauncher()
@@ -0,0 +1,36 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { mkdtempSync, copyFileSync, readFileSync, writeFileSync, existsSync, rmSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { compilerCommand, buildNativeWatchLauncher } from './build-native-watch-launcher.mjs'
const desktop = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
test('Windows and Linux hosts build the same Windows source with the required assembly', () => {
const input = { source: 'source with spaces.cs', output: 'output with spaces.exe' }
const win = compilerCommand({ ...input, platform: 'win32', env: { SystemRoot: 'D:/Windows' } })
const linux = compilerCommand({ ...input, platform: 'linux' })
assert.equal(win.command, 'D:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\csc.exe')
assert.equal(linux.command, 'mcs')
assert.deepEqual(win.args, linux.args)
assert.ok(linux.args.includes('/reference:System.Web.Extensions.dll'))
assert.equal(linux.args.at(-1), input.source)
assert.throws(() => compilerCommand({ ...input, platform: 'darwin' }), /not configured/)
})
test('real host compiler builds PE from source, and broken source fails without stale artifact', () => {
// Inside the source workspace, including on the mounted Linux CI fixture.
const home = mkdtempSync(path.join(desktop, '.native-watch-test-'))
try {
copyFileSync(path.join(desktop, 'assets/codex-bridge/NativeWatchBootstrap.cs'), path.join(home, 'NativeWatchBootstrap.cs'))
const exe = buildNativeWatchLauncher({ home })
const first = readFileSync(exe)
assert.equal(first.toString('ascii', 0, 2), 'MZ')
writeFileSync(exe, 'stale artifact')
buildNativeWatchLauncher({ home })
assert.equal(readFileSync(exe).toString('ascii', 0, 2), 'MZ')
writeFileSync(path.join(home, 'NativeWatchBootstrap.cs'), 'invalid C# source')
assert.throws(() => buildNativeWatchLauncher({ home }))
assert.equal(existsSync(exe), false)
} finally { rmSync(home, { recursive: true, force: true }) }
})
+1 -1
View File
@@ -65,7 +65,7 @@
},
"apps/desktop": {
"name": "aiturk-ide",
"version": "1.0.0-beta.29",
"version": "1.0.0-beta.30",
"license": "MIT",
"dependencies": {
"@assistant-ui/core": "0.2.23",