Compare commits

..
Author SHA1 Message Date
yilsem 7a479e46be merge: bring Windows IDE model transitions into aiturk/main
ci / approval-gate-windows (push) Waiting to run
Preserve main workflow, existing approval fixes, and both histories.
2026-10-20 20:37:13 +03:00
yilsem db46a470ea fix(codex-link): omit opaque execution state on provider switches
Server CI and Windows package / verify (push) Successful in 6m55s
Server CI and Windows package / windows-package (push) Waiting to run
Preserve visible history with omission notes and original encrypted state on native return. Ship IDE beta.45.
2026-10-20 20:24:47 +03:00
yilsem 21a8f3b8e9 ci: preserve IDE workflow line endings
Server CI and Windows package / verify (push) Successful in 7m18s
Server CI and Windows package / windows-package (push) Successful in 18m51s
2026-10-20 17:45:33 +03:00
yilsem fb62f31be3 ci: cancel superseded user-facing IDE workflow runs
Server CI and Windows package / verify (push) Canceled after 21s
Server CI and Windows package / windows-package (push) Canceled after 0s
2026-10-20 17:45:08 +03:00
yilsem 1bf80e06a3 ci: cancel superseded workflow runs with concurrency
ci / approval-gate-windows (push) Canceled after 0s
2026-10-20 17:06:05 +03:00
yilsem 1aa033a751 fix(codex-bridge): preserve plaintext peer tasks and fail invalid transfers without retries
Server CI and Windows package / verify (push) Successful in 6m11s
Server CI and Windows package / windows-package (push) Successful in 17m49s
2026-10-20 07:35:41 +03:00
yilsemandClaude Opus 4.8 212015e7c9 chore(desktop): bump AITURK IDE to 1.0.0-beta.43
Server CI and Windows package / verify (push) Successful in 5m58s
Server CI and Windows package / windows-package (push) Successful in 13m48s
Carries the codex-bridge ide/ prefix normalization fix (previous commit)
into a downloadable release.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-17 02:14:09 +03:00
yilsemandClaude Opus 4.8 65ce4be58b fix(codex-bridge): strip a stray ide/ prefix from stored model names
Server CI and Windows package / verify (push) Successful in 4m30s
Server CI and Windows package / windows-package (push) Successful in 16m59s
A hand- or agent-edited jev-settings.json could store a model name that
already carries the gateway's own "ide/" prefix. The bridge adds "ide/"
again when it routes, producing "ide/ide/<model>" — an unknown model whose
context window the bridge reads as the 32768 default and then silently
drops the request, answering "." with a wrong "model seçildi" instead of
the chosen model. jev_settings.read()/_policy() now normalize every stored
model name (fallbackModel, watchModels, exemptModels, prefilter.target,
autoRoute.model/targets) by stripping a leading "ide/". No real
provider/model id begins with "ide/", so valid settings are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-17 02:09:30 +03:00
yilsemandClaude Opus 5.5 69e2f98e18 ci(gitea): run the approval-gate tests on the Windows runner
This fork had no CI on Gitea (Actions was off; the upstream .github
workflows do not run here). Add one job that runs the protected
instruction-file gate tests and the file-write safety tests on the
Windows runner. Seven upstream tests that cannot pass on Windows (POSIX
paths, symlink privilege, POSIX stat) are deselected by name and listed
in the job log.

Kart: turkservis-20261007-1421-2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwSpve9HuXMy7dHf4RDMzX
2026-10-07 18:12:11 +03:00
yilsemandClaude Opus 5.5 0d8c521a3c fix(approval): fail closed at once when no human can see the approval prompt
A one-shot run (hermes chat --query-file ... -Q, stdin closed, no window)
still installs the prompt_toolkit approval callback but never starts the
prompt UI. The protected instruction-file gate (AGENTS.md / CLAUDE.md /
SOUL.md ...) took the registered callback for a reachable human and waited
out approvals.timeout on a panel painted nowhere. With the profile value of
300000 s the stage job hung for good.

- Gate: in a one-shot run, or when only an automatic approver
  (subagent_auto_approve) is attached, return BLOCKED / APPROVAL REQUIRED
  immediately instead of prompting. Human consent is still required every
  time; yolo and approvals.single_query_mode still do not open this gate.
- One-shot exit prints an "approval_required: {json}" line on stderr
  (session_id, tool_call_id, request_id, targets, reason, applied=false)
  so the launcher can tell "blocked, needs a human" from "still working".
- execute_code: an approval that arrives after the calling script stopped
  waiting (300 s reply timeout, script run timeout, retired kernel cell)
  is not applied.
- Same invisible wait removed from the computer_use and memory-write
  prompts in a one-shot run. The one-shot marker is no longer inherited by
  child processes.
- BLOCKED text no longer breaks on a target path containing braces.

Long-running tools are untouched: no general tool timeout was shortened.

Kart: turkservis-20261007-1421-2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwSpve9HuXMy7dHf4RDMzX
2026-10-07 17:16:43 +03:00
13 changed files with 333 additions and 120 deletions

No files matched your search

+77 -83
View File
@@ -1,98 +1,92 @@
name: Server CI and Windows package
# AITURK IDE ajan CI (kart turkservis-20261007-1421-2).
#
# Bu depo bir yukari akis catalidir; .github/workflows altindaki GitHub is akislari Gitea'da kosmaz. .gitea/workflows
# klasoru var oldugu surece Gitea yalniz buradaki is akislarini okur.
#
# Ne kosar: korumali talimat dosyasi onay kapisinin testleri (insana sorulamayan kosuda aninda BLOCKED, gec onayin
# uygulanmamasi, yolo ile kapinin acilmamasi) ve dosya yazma guvenligi testleri. Kosucu Windows'tur (etiket
# windows-latest; 7 Ekim 2026'da cevrimici kosucu hermes-laptop-windows): urunun hedefi Windows x64'tur ve hata
# Windows asama kosusunda gorulmustur.
#
# Ne sayilmaz (durustluk notu): tests/tools/test_file_write_safety.py icindeki 7 test Windows'ta bu degisiklikten
# bagimsiz olarak gecemez; yukari akis bunlari POSIX icin yazmistir (/etc, /private/etc, /boot yollari; yonetici
# ayricaligi isteyen symlink; POSIX stat alanlari). Asagida adlariyla deselect edilir; degismemis 58dbb4523b
# commit'inde de ayni 7 test kaliyordu (7 Ekim 2026 olcumu). Tam yukari akis test takimi burada KOSMAZ.
name: ci
on:
push:
branches: [aiturk/main, fix/ide-windows-vcruntime]
branches:
- aiturk/main
- fix/ide-windows-vcruntime
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
verify:
runs-on: ubuntu-24.04
approval-gate-windows:
runs-on: windows-latest
timeout-minutes: 40
env:
NODE_OPTIONS: --max-old-space-size=2560
PYTHONDONTWRITEBYTECODE: '1'
steps:
- uses: actions/checkout@v4
- name: Kaynagi al
uses: actions/checkout@v4
with:
github-server-url: https://gitea.turkservis.online
- name: Exact source and Linux test environment
run: |
set -eu
test "$(git rev-parse HEAD)" = "$GITHUB_SHA"
apt-get update
apt-get install -y --no-install-recommends ripgrep
rg --version
python3 -m venv "$RUNNER_TEMP/bootstrap"
"$RUNNER_TEMP/bootstrap/bin/pip" install uv
"$RUNNER_TEMP/bootstrap/bin/uv" venv --python 3.11 "$RUNNER_TEMP/ci-venv"
"$RUNNER_TEMP/bootstrap/bin/uv" pip install --python "$RUNNER_TEMP/ci-venv/bin/python" -e . pytest zstandard
- name: Approval gate and file safety on the server
run: |
"$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: Managed Python update preservation
run: '"$RUNNER_TEMP/ci-venv/bin/python" -m pytest tests/test_install_ps1_reuse_healthy_venv.py tests/test_install_ps1_python_fallback_venv.py -q -p no:cacheprovider -rf'
- name: Project scoped and bounded search regressions
run: '"$RUNNER_TEMP/ci-venv/bin/python" -m pytest tests/tools/test_search_project_scope.py tests/tools/test_search_files_cpu_windows.py tests/tools/test_search_zero_match_and_multipath.py tests/tools/test_search_giant_line_containment.py tests/tools/test_search_budget_truncation.py -q -p no:cacheprovider -rf'
- name: Desktop browser control lane (AITURK Chrome)
run: '"$RUNNER_TEMP/ci-venv/bin/python" -m pytest tests/gateway/test_browser_control_local_lane.py tests/gateway/test_browser_control_broker.py tests/gateway/test_browser_control_broker_hardening.py tests/gateway/test_browser_control_cloud.py tests/tools/test_browser_extension_router.py tests/tools/test_browser_extension_router_wiring.py -q -p no:cacheprovider -rf'
- 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 apps/desktop/assets/codex-bridge/test_live_limits.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
ln -s "$RUNNER_TEMP/mobile-tests/node_modules" node_modules
node node_modules/vitest/vitest.mjs run --root apps/desktop --config vitest.mobile.config.ts --maxWorkers=2
github-server-url: https://gitea.twinpay.one
windows-package:
if: github.ref == 'refs/heads/fix/ide-windows-vcruntime'
needs: verify
runs-on: ubuntu-24.04
timeout-minutes: 60
# Windows packaging runs on Linux/Wine in the new server. No laptop runner.
container: electronuserland/builder@sha256:b226b27b1a7e95ad25eb3d6414ea90c56c04eb6d8329967f4d721dce50b989ff
env:
NODE_OPTIONS: --max-old-space-size=2560
CSC_IDENTITY_AUTO_DISCOVERY: 'false'
steps:
- uses: actions/checkout@v4
with:
github-server-url: https://gitea.turkservis.online
- name: Install locked source dependencies
run: npm ci --no-audit --no-fund
- name: Chrome extension, agent control and bridge tests
run: node --test apps/chrome/tests/agent.test.mjs apps/chrome/tests/bridge.test.mjs apps/chrome/tests/panel.test.mjs
- name: Install Windows launcher cross compiler and framework reference
- name: Ortami kur
shell: powershell
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: Codex connection settings regression tests
run: npx vitest run --root apps/desktop electron/codex-link.test.ts electron/codex-link-config.test.ts electron/staged-transition.test.ts electron/compression-settings.test.ts src/app/settings/codex-link-settings.test.tsx src/app/settings/staged-transition-settings.test.tsx src/app/settings/compression-settings.test.tsx --maxWorkers=2
- name: Build and package Windows x64
$ErrorActionPreference = 'Stop'
"kosucu: $env:COMPUTERNAME / $env:USERNAME"
"checkout SHA: $(git rev-parse HEAD) / beklenen: $env:GITHUB_SHA"
$py = $null
$launcher = Get-Command py -ErrorAction SilentlyContinue
if ($launcher) { $py = (& $launcher.Source -3 -c 'import sys; print(sys.executable)').Trim() }
if (-not $py) {
$cmd = Get-Command python -ErrorAction SilentlyContinue
if ($cmd -and $cmd.Source -notlike '*\WindowsApps\*') { $py = $cmd.Source }
}
if (-not $py) { throw 'Python bulunamadi' }
"onyukleme python: $py"
& $py -m pip install --quiet --disable-pip-version-check --target "$env:RUNNER_TEMP\uvboot" uv
if ($LASTEXITCODE -ne 0) { throw 'uv kurulamadi' }
$env:PYTHONPATH = "$env:RUNNER_TEMP\uvboot"
& $py -m uv venv --python 3.11 "$env:RUNNER_TEMP\ci-venv"
if ($LASTEXITCODE -ne 0) { throw 'venv olusturulamadi' }
& $py -m uv pip install --python "$env:RUNNER_TEMP\ci-venv\Scripts\python.exe" -e . pytest
if ($LASTEXITCODE -ne 0) { throw 'bagimliliklar kurulamadi' }
$env:PYTHONPATH = ''
& "$env:RUNNER_TEMP\ci-venv\Scripts\python.exe" --version
- name: Onay kapisi testleri
shell: powershell
run: |
set -eu
test "$(git rev-parse HEAD)" = "$GITHUB_SHA"
npm run build --workspace=apps/desktop
npm run builder --workspace=apps/desktop -- --win nsis zip --x64 --publish never
git diff --exit-code
cd apps/desktop/release
sha256sum *.exe *.zip > SHA256SUMS
- name: Preserve exact-source Windows package
uses: actions/upload-artifact@v3
with:
name: aiturk-ide-windows-${{ github.sha }}
if-no-files-found: error
path: |
apps/desktop/release/*.exe
apps/desktop/release/*.zip
apps/desktop/release/SHA256SUMS
$ErrorActionPreference = 'Continue'
$env:PYTHONDONTWRITEBYTECODE = '1'
$env:PYTHONUTF8 = '1'
$venv = "$env:RUNNER_TEMP\ci-venv\Scripts\python.exe"
$f = 'tests/tools/test_file_write_safety.py'
$sayilmayan = @(
"${f}::TestCheckSensitivePathMacOSBypass::test_etc_hosts_blocked",
"${f}::TestCheckSensitivePathMacOSBypass::test_private_etc_hosts_blocked",
"${f}::TestCheckSensitivePathMacOSBypass::test_private_etc_ssh_config_blocked",
"${f}::TestCheckSensitivePathMacOSBypass::test_private_var_blocked",
"${f}::TestCheckSensitivePathMacOSBypass::test_boot_still_blocked",
"${f}::TestAtomicWrite::test_patch_routes_through_atomic_write",
"${f}::TestProtectedInstructionFiles::test_symlink_to_protected_file_is_gated"
)
$deselect = @()
foreach ($t in $sayilmayan) { $deselect += '--deselect'; $deselect += $t }
"CI OZETI: Windows'ta sayilmayan (deselect) test sayisi: $($sayilmayan.Count)"
$sayilmayan | ForEach-Object { " sayilmadi: $_" }
& $venv -m pytest tests/tools/test_protected_instruction_unattended.py $f `
tests/tools/test_computer_use_approval_isolation.py tests/cli/test_cli_approval_ui.py `
tests/cli/test_single_query_session_finalize.py @deselect -q -p no:cacheprovider -rf
$kod = $LASTEXITCODE
"pytest cikis kodu: $kod"
exit $kod
@@ -27,6 +27,24 @@ Run: `python bridge.py --home <data dir> --hermes-home <agent home>`. The IDE wr
Tests: `python -m unittest discover` in this directory.
## Version 23: portable model switches (IDE beta.45)
Cross-provider requests replace encrypted reasoning and every typed encrypted
peer-message part with a short visible omission note. Ciphertext is never
interpreted, guessed from its alphabet, or forwarded to another provider. Visible
messages, peer text, tool calls/results and their order remain intact. Projection
is idempotent and changes only the outgoing copy. Returning to a subscription
model retains its original encrypted execution state, including large histories.
Actual conversation compaction is still rendered through its issuing endpoint;
unreadable conversation summaries retain the existing explicit error.
Other external-provider switches reuse the same transcript, with destination
context preparation only when necessary. This follows the Hermes runtime model
switch contract: `agent/agent_runtime_helpers.py::switch_model` updates the client,
provider, cache policy and compressor without resetting conversation history;
`website/docs/user-guide/configuring-models.md` documents in-session `/model`.
Native voice routing is unchanged.
## Version 19: decision stages (14 October 2026)
The decision model (Jev or another System One compatible service such as Celeris) can now be asked at
+3 -1
View File
@@ -35,7 +35,7 @@ import gecis_context
import terminal_stream
HERE = os.path.dirname(os.path.abspath(__file__))
SURUM = "21" # AITURK IDE'nin bekledigi kopru surumu (codex-link-config.ts CODEX_BRIDGE_VERSION)
SURUM = "23" # 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]
@@ -537,6 +537,8 @@ class H(http.server.BaseHTTPRequestHandler):
if external or mixed or gecis_context.foreign(req):
try:
normalized = gecis_context.normalize(req)
if external:
normalized = gecis_context.portable_reasoning(normalized)
except gecis_context.TransferError as e:
self._transfer_begin()
return self._transfer_fail(str(e))
@@ -7,7 +7,6 @@ must not enter the budget preparation path.
import copy
import hashlib
import json
import re
import threading
from collections import OrderedDict
@@ -17,6 +16,8 @@ import compression_settings
PREFIXES = {"custom_tool_call": "ctc", "custom_tool_call_output": "ctco",
"function_call": "fc", "function_call_output": "fco"}
RATIO = 0.70
PEER_OMITTED = "[Aktarım notu: Burada okunamayan şifreli bir alt ajan iletisi vardı; aktarılmadı.]"
REASONING_OMITTED = "[Aktarım notu: Şifreli model düşünme kaydı aktarılmadı.]"
class TransferError(ValueError):
@@ -56,11 +57,16 @@ def foreign(req):
def native_opaque(req):
"""Native encrypted state belongs to the subscription, even in mixed history."""
return any(isinstance(item, dict) and item.get("type") == "compaction" and item.get("encrypted_content")
and not str(item["encrypted_content"]).startswith(ozet_kopru.ISARET)
return any(isinstance(item, dict) and (
(item.get("type") in ("compaction", "reasoning") and item.get("encrypted_content")
and not str(item["encrypted_content"]).startswith(ozet_kopru.ISARET))
or (item.get("type") == "agent_message" and any(
isinstance(part, dict) and part.get("type") == "encrypted_content"
for part in item.get("content") or [])))
for item in req.get("input") or [])
def foreign_history(req):
"""Semantic migration evidence, excluding tool ID format mismatches."""
return any(isinstance(item, dict) and (
@@ -135,14 +141,10 @@ def portable_reasoning(req):
portable = []
for part in content:
if isinstance(part, dict) and part.get("type") == "encrypted_content":
# Codex peer delivery can put the literal task/result in
# this wrapper. It is distinct from encrypted reasoning.
# External translators otherwise silently drop the task.
text = part.get("encrypted_content")
if (not isinstance(text, str) or text.startswith("gAAAA")
or re.fullmatch(r"[A-Za-z0-9_+/=-]{128,}", text)):
raise TransferError("Alt ajan iletisi şifreli veya okunamıyor; görev eksik gönderilmedi.")
portable.append({"type": "input_text", "text": text})
# Treat the typed wrapper as opaque regardless of whether
# its bytes happen to look like plaintext. Never decrypt,
# guess, or forward it to a different provider.
portable.append({"type": "input_text", "text": PEER_OMITTED})
else:
portable.append(part)
result.append({"type": "message", "role": "user", "content": [
@@ -158,6 +160,8 @@ def portable_reasoning(req):
for part in item.get(field) or []:
if isinstance(part, dict) and isinstance(part.get("text"), str) and part["text"]:
visible.append(part["text"])
if item.get("encrypted_content"):
visible.append(REASONING_OMITTED)
if visible:
# Keep this on the assistant side: a synthetic user message would
# change split_history's count of protected user turns.
@@ -59,6 +59,42 @@ def _https(value):
return url.scheme == 'https' and bool(url.netloc) and not url.username and not url.password
IDE_PREFIX = 'ide/'
def _strip_ide(value):
"""A stored model name must never carry the gateway's own 'ide/' prefix; the bridge adds it when
it routes. A doubled 'ide/ide/...' makes the bridge treat the name as an unknown model, read its
context window as the 32768 default and silently drop the request. No real provider/model id
begins with 'ide/', so stripping a leading 'ide/' here can only undo an accidental prefix."""
if isinstance(value, str):
while value.startswith(IDE_PREFIX):
value = value[len(IDE_PREFIX):]
return value
def _normalize_models(settings):
"""Strip a stray 'ide/' prefix from every stored model name so routing can never double it."""
if 'fallbackModel' in settings:
settings['fallbackModel'] = _strip_ide(settings['fallbackModel'])
for key in ('watchModels', 'exemptModels'):
values = settings.get(key)
if isinstance(values, list):
settings[key] = [_strip_ide(x) for x in values]
prefilter = settings.get('prefilter')
if isinstance(prefilter, dict) and 'target' in prefilter:
prefilter['target'] = _strip_ide(prefilter['target'])
auto = settings.get('autoRoute')
if isinstance(auto, dict):
if 'model' in auto:
auto['model'] = _strip_ide(auto['model'])
targets = auto.get('targets')
if isinstance(targets, dict):
for key in list(targets):
targets[key] = _strip_ide(targets[key])
return settings
def validate(settings):
"""Raise ValueError when a value could route to the wrong place or wait without bound."""
# A corrupted file must not silently change routing or contact another host.
@@ -115,6 +151,7 @@ def _policy(settings):
for key in POLICY_KEYS:
if isinstance(overrides.get(key), dict):
merged[key] = _fill(overrides[key], settings[key])
_normalize_models(merged)
return validate(merged)
except (OSError, ValueError, KeyError, TypeError, AttributeError):
return settings
@@ -129,6 +166,7 @@ def read(overlay=True):
settings = data['settings']
for section in SECTIONS:
settings[section] = _fill(settings[section], DEFAULTS[section]) if section in settings else copy.deepcopy(DEFAULTS[section])
_normalize_models(settings)
validate(settings)
return (_policy(settings) if overlay else settings), data.get('protectedKey')
+20 -4
View File
@@ -42,7 +42,7 @@ from bekci_watch import MetadataFilter
# Installed only by owned native launcher, never an HTTP control endpoint.
WATCH_OWNER = None
SURUM = "20"
SURUM = "21"
HERE = os.path.dirname(os.path.abspath(__file__))
try:
with open(os.path.abspath(__file__), "rb") as _f:
@@ -883,11 +883,27 @@ class SseRewriter:
self.bekleyen = d
return once
return once + [d]
def _restore_tool_name(self, item):
def _restore_tool_name(self, item, plaintext=False):
if isinstance(item, dict) and item.get("type") in ("function_call", "custom_tool_call"):
identity = self.tool_names.get(item.get("name"))
if identity:
item["namespace"], item["name"] = identity
if (plaintext and item.get("type") == "function_call"
and item.get("namespace") == "collaboration"
and item.get("name") in ("spawn_agent", "send_message", "followup_task")
and "encrypted_function_args" not in item):
# Codex distinguishes missing encryption metadata (opaque) from
# an explicit empty list (plaintext). External providers return
# ordinary JSON strings; stamp only a complete readable message,
# after its final arguments arrive. Never override owner state.
try:
message = json.loads(item.get("arguments") or "{}").get("message")
except (ValueError, AttributeError, TypeError):
message = None
if (isinstance(message, str) and message.strip()
and not message.startswith("gAAAA")
and not re.fullmatch(r"[A-Za-z0-9_+/=-]{128,}", message)):
item["encrypted_function_args"] = []
return item
def _fix(self, d):
t = d.get("type", "")
@@ -923,10 +939,10 @@ class SseRewriter:
item = {"type": "custom_tool_call", "id": item.get("id"), "call_id": item.get("call_id"),
"name": item.get("name"), "input": inp, "status": "completed"}
fix_tool_ids({"input": [item]})
out.append(self._restore_tool_name(item))
out.append(self._restore_tool_name(item, plaintext=True))
d["response"]["output"] = out
fix_tool_ids({"input": out})
self._restore_tool_name(d.get("item"))
self._restore_tool_name(d.get("item"), plaintext=t == "response.output_item.done")
return d
def feed(self, chunk):
self.buf += chunk
@@ -1,9 +1,10 @@
"""Regression fixture matching actual Codex child delivery, with no user data."""
import copy
import json
import unittest
import gecis_context
import kopru
from test_model_switch import env as wire_bridge
class AgentPayloadTests(unittest.TestCase):
@@ -16,16 +17,17 @@ class AgentPayloadTests(unittest.TestCase):
],
}]}
def test_actual_peer_wrapper_survives_external_text_extraction(self):
def test_typed_encrypted_wrapper_is_omitted_even_if_bytes_look_like_text(self):
task = "Run exec_command: Write-Output (157 * 29); report the exit code."
request = self.request(task)
original = copy.deepcopy(request)
kopru.flatten_request(request)
wire_bridge["flatten_request"](request)
# An OpenAI-compatible translator consumes input_text, not Codex's
# encrypted_content part. Checking the JSON string alone misses loss.
text = "\n".join(part["text"] for item in request["input"]
for part in item["content"] if part["type"] == "input_text")
self.assertIn(task, text)
self.assertNotIn(task, text)
self.assertIn(gecis_context.PEER_OMITTED, text)
self.assertIn('/root/test', text)
self.assertEqual(request["input"][0]["role"], "user")
self.assertEqual(original["input"][0]["content"][1]["type"], "encrypted_content")
@@ -34,18 +36,69 @@ class AgentPayloadTests(unittest.TestCase):
request = self.request("Result: 4553; exit code: 0")
original = copy.deepcopy(request)
result = gecis_context.portable_reasoning(request)
self.assertEqual(result["input"][0]["content"][-1]["text"], "Result: 4553; exit code: 0")
self.assertEqual(result["input"][0]["content"][-1]["text"], gecis_context.PEER_OMITTED)
self.assertEqual(result["input"][0]["content"][1], original["input"][0]["content"][0])
self.assertEqual(request, original)
def test_opaque_peer_payload_fails_instead_of_sending_header_only(self):
def test_opaque_peer_payload_is_replaced_by_a_visible_note(self):
for opaque in ("gAAAA_native_ciphertext", "A" * 200, None):
with self.subTest(opaque=opaque):
with self.assertRaises(gecis_context.TransferError):
gecis_context.portable_reasoning(self.request(opaque))
result = gecis_context.portable_reasoning(self.request(opaque))
self.assertEqual(result["input"][0]["content"][-1]["text"], gecis_context.PEER_OMITTED)
self.assertNotIn("encrypted_content", json.dumps(result))
def test_reasoning_ciphertext_is_still_not_interpreted_as_a_peer_task(self):
request = {"input": [{"type": "reasoning", "encrypted_content": "Run a command now"}]}
self.assertEqual(gecis_context.portable_reasoning(request)["input"], [])
result = gecis_context.portable_reasoning(request)
self.assertNotIn("Run a command now", json.dumps(result))
self.assertEqual(result["input"][0]["content"][0]["text"], gecis_context.REASONING_OMITTED)
class PlaintextAgentWireTests(unittest.TestCase):
def item(self, message="Read the complete task and report its result.", **extra):
return dict(type="function_call", namespace="collaboration", name="spawn_agent",
id="fc_peer", call_id="call_peer", arguments=json.dumps({
"task_name": "peer", "message": message}), **extra)
def test_final_plaintext_peer_calls_are_explicit_without_argument_changes(self):
for name in ("spawn_agent", "send_message", "followup_task"):
original = self.item(); original["name"] = name
for stage in ("response.output_item.done", "response.completed"):
with self.subTest(name=name, stage=stage):
adapter = wire_bridge["SseRewriter"](set())
item = copy.deepcopy(original)
event = ({"type": stage, "item": item} if stage.endswith(".done") else
{"type": stage, "response": {"output": [item]}})
adapter._fix(event)
self.assertEqual(item.pop("encrypted_function_args"), [])
self.assertEqual(item, original)
def test_partial_empty_or_opaque_messages_never_gain_plaintext_metadata(self):
for arguments in ('', '{"message":', '{}', '[]', 'null',
json.dumps({"message": " "}), json.dumps({"message": None}),
json.dumps({"message": "gAAAA_ciphertext"}),
json.dumps({"message": "A" * 200})):
for stage in ("response.output_item.added", "response.output_item.done", "response.completed"):
with self.subTest(arguments=arguments, stage=stage):
item = self.item(); item["arguments"] = arguments
wire_bridge["SseRewriter"](set())._restore_tool_name(item, plaintext=stage != "response.output_item.added")
self.assertNotIn("encrypted_function_args", item)
item = self.item()
wire_bridge["SseRewriter"](set())._fix({"type": "response.output_item.added", "item": item})
self.assertNotIn("encrypted_function_args", item)
def test_existing_encryption_metadata_and_other_tools_are_untouched(self):
for marker in (None, [], ["message"]):
item = self.item(encrypted_function_args=marker)
original = copy.deepcopy(item)
wire_bridge["SseRewriter"](set())._fix({"type": "response.output_item.done", "item": item})
self.assertEqual(item, original)
for namespace, name in (("other", "spawn_agent"), ("collaboration", "wait_agent"),
(None, "spawn_agent")):
item = self.item(); item.update(namespace=namespace, name=name)
original = copy.deepcopy(item)
wire_bridge["SseRewriter"](set())._fix({"type": "response.output_item.done", "item": item})
self.assertEqual(item, original)
if __name__ == "__main__":
@@ -68,7 +68,7 @@ class History(unittest.TestCase):
self.assertNotIn("OPAQUE_", json.dumps(result))
self.assertEqual(req, before)
self.assertLessEqual(gc.tokens(result), gc.input_budget(req, 32768))
self.assertEqual(result["input"][-1], {"role": "user", "content": "Latest"})
self.assertIn({"role": "user", "content": "Latest"}, result["input"])
def test_legacy_summary_and_tool_ids_repaired_without_mutating_source(self):
req = {"input": [ozet_kopru.ozet_ogesi("KARAR-4827 korunacak"),
@@ -259,19 +259,18 @@ class PreparationService(unittest.TestCase):
self.assertNotIn("OPAQUE_REASONING", json.dumps(result))
self.assertEqual(req, before)
def test_large_native_return_summarizes_visible_history_without_ciphertext(self):
def test_large_native_return_keeps_original_encrypted_execution_state(self):
for opaque in ({"type": "reasoning", "encrypted_content": "OPAQUE_NATIVE"},
{"type": "agent_message", "content": [
{"type": "encrypted_content", "encrypted_content": "OPAQUE_PEER"}]}):
req = {"model": "gpt-6-astra", "input": [
{"role": "user", "content": "KARAR-4827 " + "x" * 190000},
{"type": "reasoning", "encrypted_content": "OPAQUE_NATIVE"},
{"role": "user", "content": "Latest"}]}
opaque, {"role": "user", "content": "Latest"}]}
before = copy.deepcopy(req)
result = self.service._prepare_transfer({"request": req, "source_model": "ide/test"})["request"]
self.assertEqual(result["model"], "gpt-6-astra")
self.assertIn("KARAR-4827", json.dumps(result))
self.assertLessEqual(gc.tokens(result), gc.input_budget(req, 128000))
self.assertEqual(result, before)
self.assertEqual(req, before)
self.assertTrue(self.server.gelen)
self.assertTrue(all(b"OPAQUE_NATIVE" not in call["govde"] for call in self.server.gelen))
self.assertEqual(self.server.gelen, [])
def test_external_compaction_uses_external_summary_not_subscription(self):
req = {"model": "ide/test", "input": [{"role": "assistant", "content": "KARAR-4827"}, {"role": "user", "content": "KARAR-4827"}, {"type": "compaction_trigger"}]}
@@ -281,6 +280,50 @@ class PreparationService(unittest.TestCase):
class SocketMigration(fixtures.Ayirici):
def test_opaque_peer_transfer_succeeds_without_ciphertext_or_transfer_error(self):
req = {"model": "ide/agy-gemini-3.8-flash-high", "prompt_cache_key": "opaque-peer",
"input": [{"role": "user", "content": "USER_DECISION"},
{"type": "agent_message", "author": "/root", "recipient": "/root/peer",
"content": [{"type": "input_text", "text": "VISIBLE_PEER_HEADER"},
{"type": "encrypted_content", "encrypted_content": "gAAAA_synthetic_opaque"}]},
{"role": "assistant", "content": "VISIBLE_ANSWER"},
{"type": "function_call", "id": "fc_1", "call_id": "c1", "name": "read", "arguments": "{}"},
{"type": "function_call_output", "id": "fco_1", "call_id": "c1", "output": "VISIBLE_RESULT"}]}
original = copy.deepcopy(req)
for model in (req["model"], "ide/another-provider"):
req["model"] = model
status, body, _ = self.iste("POST", "/backend-api/codex/responses", json.dumps(req).encode())
self.assertEqual(status, 200)
self.assertNotIn(b"aiturk_transfer_failed", body)
sent = json.loads(self.kopru.gelen[-1]["govde"])
wire = json.dumps(sent, ensure_ascii=False)
self.assertNotIn("gAAAA_synthetic_opaque", wire)
self.assertNotIn("encrypted_content", wire)
self.assertIn(gc.PEER_OMITTED, wire)
self.assertIn("VISIBLE_PEER_HEADER", wire)
for item in original["input"][:1] + original["input"][2:]:
self.assertIn(item, sent["input"])
self.assertEqual(req["input"], original["input"])
self.assertEqual(self.ozgun.gelen, [])
# Returning uses the client's original transcript, including opaque peer
# state. Projection never rewrites the persisted source or a native turn.
req["model"] = "gpt-6-astra"
raw = zstd(json.dumps(req).encode())
status, _, _ = self.iste("POST", "/backend-api/codex/responses", raw, {"Content-Encoding": "zstd"})
self.assertEqual(status, 200)
self.assertEqual(self.ozgun.gelen[-1]["govde"], raw)
def test_native_opaque_peer_history_remains_byte_identical_on_owner_route(self):
req = {"model": "gpt-6-astra", "input": [{"type": "agent_message", "author": "/root",
"recipient": "/root/peer", "content": [{"type": "encrypted_content",
"encrypted_content": "gAAAA_synthetic_native_owner"}]}]}
raw = zstd(json.dumps(req).encode())
status, _, _ = self.iste("POST", "/backend-api/codex/responses", raw,
{"Content-Encoding": "zstd"})
self.assertEqual(status, 200)
self.assertEqual(self.ozgun.gelen[-1]["govde"], raw)
self.assertEqual(self.kopru.gelen, [])
def test_completed_return_ends_preparation_for_following_native_turns(self):
self.ozgun.cevap = duz(200, b'data: {"type":"response.completed"}\n\n', 'text/event-stream')
req = {"model": "ide/test", "prompt_cache_key": "return-once", "input": []}
@@ -42,6 +42,28 @@ class Settings(unittest.TestCase):
self.save(rejectionRules=[{'from': 'A', 'to': 'B'}, {'from': 'B', 'to': 'A'}])
self.assertEqual(jev_denetim.sonrakiler('A'), ['B'])
def test_stored_ide_prefix_on_model_names_is_stripped(self):
# A hand- or agent-edited settings file may store a model name carrying the gateway's own
# 'ide/' prefix. The bridge adds 'ide/' again when it routes, so a stored prefix would make
# 'ide/ide/...', an unknown model whose context window the bridge mis-reads before silently
# dropping the request. read() must strip the stray prefix so routing stays correct.
self.save(
fallbackModel='ide/abliteration-abliterated-model-large-v2',
exemptModels=['ide/claude-claude-opus-5-5', 'agnes-agnes-1.5-flash'],
prefilter={**self.settings['prefilter'], 'target': 'ide/ide/abliteration-abliterated-model-large-v2'},
)
settings, _ = jev_settings.read()
self.assertEqual(settings['fallbackModel'], 'abliteration-abliterated-model-large-v2')
self.assertEqual(settings['prefilter']['target'], 'abliteration-abliterated-model-large-v2')
self.assertEqual(settings['exemptModels'], ['claude-claude-opus-5-5', 'agnes-agnes-1.5-flash'])
self.assertTrue(jev_denetim.muaf_mi('claude-claude-opus-5-5'))
def test_strip_ide_leaves_normal_names_and_removes_only_prefix(self):
self.assertEqual(jev_settings._strip_ide('claude-claude-opus-5-5'), 'claude-claude-opus-5-5')
self.assertEqual(jev_settings._strip_ide('ide/x'), 'x')
self.assertEqual(jev_settings._strip_ide('ide/ide/x'), 'x')
self.assertEqual(jev_settings._strip_ide('provider-ide/x'), 'provider-ide/x')
def test_prompts_model_criteria_and_thresholds_are_applied(self):
self.save(model='jev-custom', questions={'ret': 'question A', 'sart': 'question B'}, thresholds={'ret': .9, 'sart': .8}, criteria={'ret': {'true': 'bad', 'false': 'good'}, 'sart': {'true': '', 'false': ''}})
captured = []
@@ -45,6 +45,29 @@ class ToolNamespaces(unittest.TestCase):
self.assertEqual(restored["id"], "ctc_patch")
self.assertEqual(restored["input"], "patch text")
def test_plaintext_peer_marker_survives_namespace_restore_and_split_frames(self):
req = self.request(); names = {}; custom = env["flatten_request"](req, names)
alias = req["tools"][0]["name"]
arguments = json.dumps({"task_name": "child", "message": "Execute the complete delegated task."})
item = {"type": "function_call", "name": alias, "id": "fc_peer", "call_id": "call_peer", "arguments": arguments}
events = [
{"type": "response.output_item.added", "item": dict(item, arguments="")},
{"type": "response.function_call_arguments.delta", "item_id": "fc_peer", "delta": arguments[:17]},
{"type": "response.function_call_arguments.delta", "item_id": "fc_peer", "delta": arguments[17:]},
{"type": "response.output_item.done", "item": dict(item)},
{"type": "response.completed", "response": {"output": [dict(item)]}},
]
raw = b"".join(("data: " + json.dumps(event) + "\n\n").encode() for event in events)
adapter = env["SseRewriter"](custom, tool_names=names)
output = b"".join(adapter.feed(raw[i:i+13]) for i in range(0, len(raw), 13)) + adapter.flush()
restored = [json.loads(line[6:]) for line in output.decode().splitlines() if line.startswith("data: ")]
self.assertNotIn("encrypted_function_args", restored[0]["item"])
self.assertEqual(restored[1:3], events[1:3])
for final in (restored[3]["item"], restored[4]["response"]["output"][0]):
self.assertEqual(final["encrypted_function_args"], [])
self.assertEqual((final["namespace"], final["name"]), ("collaboration", "spawn_agent"))
self.assertEqual((final["id"], final["call_id"], final["arguments"]), ("fc_peer", "call_peer", arguments))
def test_plain_tools_stay_plain_and_history_not_mutated(self):
req = self.request(); original = copy.deepcopy(req); old_item = req["input"][0]
env["flatten_request"](req, {})
+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 = '21'
export const CODEX_BRIDGE_VERSION = '23'
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'
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "aiturk-ide",
"productName": "AITURK IDE",
"private": true,
"version": "1.0.0-beta.42",
"version": "1.0.0-beta.45",
"description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.",
"author": "AITURK / TurkServis",
"repository": {
+1 -1
View File
@@ -65,7 +65,7 @@
},
"apps/desktop": {
"name": "aiturk-ide",
"version": "1.0.0-beta.42",
"version": "1.0.0-beta.45",
"license": "MIT",
"dependencies": {
"@assistant-ui/core": "0.2.23",