Compare commits

...
Author SHA1 Message Date
yilsem 4037c65204 ci: install ripgrep for real search regressions
Server CI and Windows package / verify (push) Successful in 2m51s
Server CI and Windows package / windows-package (push) Successful in 6m14s
2026-10-13 16:35:24 +03:00
yilsem e7d3080e0a fix: preserve native model switches and bound project searches
Server CI and Windows package / verify (push) Failing after 1m40s
Server CI and Windows package / windows-package (push) Skipped
2026-10-13 16:32:50 +03:00
yilsem db5e876f40 fix(bridge): preserve plaintext peer task payloads for external models
Server CI and Windows package / verify (push) Successful in 3m16s
Server CI and Windows package / windows-package (push) Successful in 8m18s
2026-10-13 08:05:00 +03:00
yilsem ab7a721fbc Ship beta34 with external agent tasks, terminal SSE and live token limits
Server CI and Windows package / verify (push) Successful in 3m13s
Server CI and Windows package / windows-package (push) Successful in 7m56s
2026-10-13 07:32:57 +03:00
21 changed files with 528 additions and 50 deletions

No files matched your search

+6 -1
View File
@@ -23,6 +23,9 @@ jobs:
run: | run: |
set -eu set -eu
test "$(git rev-parse HEAD)" = "$GITHUB_SHA" 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" python3 -m venv "$RUNNER_TEMP/bootstrap"
"$RUNNER_TEMP/bootstrap/bin/pip" install uv "$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" venv --python 3.11 "$RUNNER_TEMP/ci-venv"
@@ -32,8 +35,10 @@ 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 "$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 - name: Codex bridge regression tests
run: '"$RUNNER_TEMP/ci-venv/bin/python" -m unittest discover -s apps/desktop/assets/codex-bridge' run: '"$RUNNER_TEMP/ci-venv/bin/python" -m unittest discover -s apps/desktop/assets/codex-bridge'
- 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: Native watch and speech mux pytest regressions - 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' 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 - name: Mobile and desktop startup regression tests
run: | 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 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
+5 -2
View File
@@ -25,6 +25,8 @@ ayrıntı seviyesi, korunacak mesajlaşmalar, yüzde/token bütçesi ve otomatik
seçimini sunar. Varsayılanlar: Gemini 3.8 Flash High, yedek Gemini 3 Flash, seçimini sunar. Varsayılanlar: Gemini 3.8 Flash High, yedek Gemini 3 Flash,
10 mesajlaşma, geçişte %70, otomatik tetiklemede %70 ve sonrasında %50. 10 mesajlaşma, geçişte %70, otomatik tetiklemede %70 ve sonrasında %50.
Kapasite bağlantıdan gelen model bilgisiyle hesaplanır; 272K genel tavan yoktur. Kapasite bağlantıdan gelen model bilgisiyle hesaplanır; 272K genel tavan yoktur.
`/models` içindeki doğrulanmış sayısal bağlam bilgisi, model adından çıkarılan
yerel varsayılandan önce gelir; gözlenmiş daha düşük bir gerçek sınır korunur.
Girdi hesabı talimatlar ve araçları da içeren temkinli bir token tahminidir. Girdi hesabı talimatlar ve araçları da içeren temkinli bir token tahminidir.
Abonelik modellerinin kendi aralarındaki istekler ve GPT-Live ses yolu özgün Abonelik modellerinin kendi aralarındaki istekler ve GPT-Live ses yolu özgün
@@ -58,8 +60,9 @@ npm run builder --workspace=apps/desktop -- --win nsis --x64 --publish never
Dağıtılacak derleme temiz, yayımlanmış Git commit'inden üretilmelidir. İlk açılış Dağıtılacak derleme temiz, yayımlanmış Git commit'inden üretilmelidir. İlk açılış
ajan kurulumunu paketteki `install-stamp.json` commit'ine sabitler. Windows ajan kurulumunu paketteki `install-stamp.json` commit'ine sabitler. Windows
imzası ve temiz kurulum doğrulanmadan bir paket doğrulanmış üretim sürümü diye paketleri imzasız EXE/ZIP olarak yayımlanır; SHA-256 ve gerçek Windows çalıştırma
sunulmamalıdır. macOS ve Linux paketleri bu beta kapsamında doğrulanmamıştır. kontrolü gereklidir. Kullanıcı açıkça istemeden imzalama yapılmaz. macOS ve Linux
paketleri bu beta kapsamında doğrulanmamıştır.
## Lisans ve kaynak ## Lisans ve kaynak
+60
View File
@@ -8,6 +8,66 @@ path; broad diagnostic probes should apply this policy to recursive walks.
from __future__ import annotations from __future__ import annotations
from pathlib import Path
SEARCH_RG_THREADS = 2
SEARCH_MAX_CONCURRENT = 2
_PROJECT_MARKERS = ('.git', '.hg', 'pyproject.toml', 'package.json',
'Cargo.toml', 'go.mod', 'CMakeLists.txt')
def _project_root(path):
"""Prefer the enclosing repository over nested monorepo packages."""
package = None
for parent in (path, *path.parents):
if (parent / '.git').exists() or (parent / '.hg').exists():
return parent
if package is None and any((parent / marker).exists() for marker in _PROJECT_MARKERS):
package = parent
return package
def local_search_scope(path, cwd, home):
"""Resolve broad local roots without starting a recursive search process.
Returns (root, notice, error). Only one level of candidate directories is
inspected; no repository-wide discovery is needed to reject an umbrella.
Real paths prevent symlink aliases from bypassing broad-root detection.
"""
root = Path(path)
if not root.is_absolute():
root = Path(cwd) / root
root = root.resolve()
if not root.is_dir():
return path, None, None
home = Path(home).resolve()
project = _project_root(Path(cwd).resolve())
root_project = _project_root(root)
if root_project is not None:
return path, None, None
candidates = []
try:
# Bound metadata work even in directories containing many entries.
from itertools import islice
for child in islice(root.iterdir(), 256):
if child.name in SEARCH_PRUNE_DIR_NAMES or not child.is_dir():
continue
if any((child / marker).exists() for marker in _PROJECT_MARKERS):
candidates.append(str(child))
except OSError:
pass
broad = (root == Path(root.anchor) or root == home or root in home.parents
or len(candidates) > 1 or (project is not None and root in project.parents))
if not broad:
return path, None, None
if project is not None and root in project.parents:
chosen = str(project)
return chosen, f'Search scoped to current project: {chosen}. Set allow_broad=true for the requested umbrella.', None
choices = ', '.join(sorted(candidates)[:12]) or 'choose an explicit project directory'
return path, None, ('Search scope needed: requested root spans projects or a home/drive. '
f'Candidates: {choices}. Set allow_broad=true only for an intentional broad search.')
# Keep this policy conservative and name-based so it works for local and remote # Keep this policy conservative and name-based so it works for local and remote
# shell backends alike. The same set is used by context discovery and search # shell backends alike. The same set is used by context discovery and search
+10 -4
View File
@@ -34,7 +34,7 @@ import urllib.request
import gecis_context import gecis_context
HERE = os.path.dirname(os.path.abspath(__file__)) HERE = os.path.dirname(os.path.abspath(__file__))
SURUM = "14" # AITURK IDE'nin bekledigi kopru surumu (codex-link-config.ts CODEX_BRIDGE_VERSION) SURUM = "17" # AITURK IDE'nin bekledigi kopru surumu (codex-link-config.ts CODEX_BRIDGE_VERSION)
try: try:
with open(os.path.abspath(__file__), "rb") as _f: with open(os.path.abspath(__file__), "rb") as _f:
KOD = hashlib.sha256(_f.read()).hexdigest()[:8] KOD = hashlib.sha256(_f.read()).hexdigest()[:8]
@@ -484,15 +484,21 @@ class H(http.server.BaseHTTPRequestHandler):
source = TRANSFERS.get(scope) if thread else None source = TRANSFERS.get(scope) if thread else None
# A completed return to the subscription ends migration. Historical # A completed return to the subscription ends migration. Historical
# foreign IDs must not keep later native turns in the preparer. # foreign IDs must not keep later native turns in the preparer.
mixed = ek_model_mu(source or "") or (source is None and gecis_context.foreign(req)) mixed = ek_model_mu(source or "") or (source is None and gecis_context.foreign_history(req))
if external or mixed: # Format repair is stateless: the saved transcript can still contain
# foreign wrappers after a successful native return (or a restart).
if external or mixed or gecis_context.foreign(req):
try: try:
normalized = gecis_context.normalize(req) normalized = gecis_context.normalize(req)
except gecis_context.TransferError as e: except gecis_context.TransferError as e:
self._transfer_begin() self._transfer_begin()
return self._transfer_fail(str(e)) return self._transfer_fail(str(e))
# Small returning requests need no summarizer or local worker. # Small returning requests need no summarizer or local worker.
if external or gecis_context.tokens(normalized) > int(32768 * 0.70): # Native opaque state must remain on its owner route. Its byte
# size is not a semantic token budget and cannot trigger a
# foreign summarizer, including when transfer state is cold.
if external or (mixed and not gecis_context.native_opaque(normalized)
and gecis_context.tokens(normalized) > int(32768 * 0.70)):
prepared, started = self._prepare(normalized, source) prepared, started = self._prepare(normalized, source)
if prepared is None: if prepared is None:
return return
@@ -180,9 +180,13 @@ def prepare(handler, module, payload, checkpoints):
slug = original.get('model', '') slug = original.get('model', '')
external = module.ek_model_mu(slug) external = module.ek_model_mu(slug)
source_model = payload.get('source_model') or '' source_model = payload.get('source_model') or ''
if not external and gc.native_opaque(original):
# Defense in depth for direct/older splitter callers. Native encrypted
# state never enters owner decoding or cross-provider summarization.
return {'request': gc.normalize(original)}
# Defense in depth: an accidental worker call cannot alter native-only history. # Defense in depth: an accidental worker call cannot alter native-only history.
if not external and not module.ek_model_mu(source_model) and not gc.foreign(original): if not external and not module.ek_model_mu(source_model) and not gc.foreign_history(original):
return {'request': copy.deepcopy(original), 'window': handler._transfer_window(slug)} return {'request': gc.normalize(original)}
settings = settings_store.read(module.HOME) settings = settings_store.read(module.HOME)
actual = (module.yonlendirilen(str(original.get('prompt_cache_key') or ''), slug) or slug) if external else slug actual = (module.yonlendirilen(str(original.get('prompt_cache_key') or ''), slug) or slug) if external else slug
window = handler._transfer_window(actual) window = handler._transfer_window(actual)
@@ -7,6 +7,7 @@ must not enter the budget preparation path.
import copy import copy
import hashlib import hashlib
import json import json
import re
import threading import threading
from collections import OrderedDict from collections import OrderedDict
@@ -53,6 +54,22 @@ def foreign(req):
return False return False
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)
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 (
"chatcmpl-" in str(item.get("id") or "")
or "astrabridge" in str(item.get("id") or "")
or str(item.get("encrypted_content") or "").startswith(ozet_kopru.ISARET))
for item in req.get("input") or [])
def normalize(req): def normalize(req):
"""Repair local wrappers/IDs, retaining real OpenAI encrypted compaction. """Repair local wrappers/IDs, retaining real OpenAI encrypted compaction.
@@ -81,6 +98,10 @@ def normalize(req):
if any(visible): if any(visible):
result.append(ozet_kopru._ozet_mesaji("\n".join(visible))) result.append(ozet_kopru._ozet_mesaji("\n".join(visible)))
continue continue
if ("chatcmpl-" in str(identity) or "astrabridge" in str(identity)) and (
item.get("type") == "message" or item.get("role")):
# Inline message content is portable; its foreign server ID is not.
item.pop("id", None)
prefix = PREFIXES.get(item.get("type")) prefix = PREFIXES.get(item.get("type"))
if prefix and isinstance(identity, str) and identity and not identity.startswith(prefix + "_"): if prefix and isinstance(identity, str) and identity and not identity.startswith(prefix + "_"):
item["id"] = prefix + "_" + identity.split("_", 1)[-1] item["id"] = prefix + "_" + identity.split("_", 1)[-1]
@@ -104,6 +125,31 @@ def portable_reasoning(req):
return out return out
result = [] result = []
for item in items: for item in items:
if isinstance(item, dict) and item.get("type") == "agent_message":
# Peer messages carry delegated tasks/results. External Responses
# translators do not understand this Codex-specific item type.
# Keep provenance as data, never as system/developer authority.
content = item.get("content") or []
if isinstance(content, str):
content = [{"type": "input_text", "text": content}]
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})
else:
portable.append(part)
result.append({"type": "message", "role": "user", "content": [
{"type": "input_text", "text": "Agent message: " + encoded({
"author": item.get("author"), "recipient": item.get("recipient")})}
] + portable})
continue
if not isinstance(item, dict) or item.get("type") != "reasoning": if not isinstance(item, dict) or item.get("type") != "reasoning":
result.append(item) result.append(item)
continue continue
@@ -348,11 +348,13 @@ def yukle(ua="AstraBridge/1.0 (codex)", timeout=30, home=None, ag=True):
if not girdiler: if not girdiler:
raise ValueError("Gecitte kullanilabilir OmniRoute sohbet modeli yok") raise ValueError("Gecitte kullanilabilir OmniRoute sohbet modeli yok")
kayitlar = {m["id"]: m for m in metadata if m.get("id")} kayitlar = {m["id"]: m for m in metadata if m.get("id")}
ilanlar = {m["id"]: m for m in data if m.get("id")}
nano = pencere.nano_yukle(home, ua, ag=ag) # ag=False: acilista yalniz onbellek nano = pencere.nano_yukle(home, ua, ag=ag) # ag=False: acilista yalniz onbellek
ogr = pencere.ogrenilen(home) ogr = pencere.ogrenilen(home)
aracsiz = pencere.aracsizlar(home) aracsiz = pencere.aracsizlar(home)
for g in girdiler: for g in girdiler:
bilgi = pencere.coz(g["id"], g["kind"], g.get("yol"), kayitlar.get(g["id"]), nano, ogr, aracsiz) bilgi = pencere.coz(g["id"], g["kind"], g.get("yol"), kayitlar.get(g["id"]), nano, ogr, aracsiz,
ilan=ilanlar.get(g["id"]))
g["pencere"], g["gorsel"], g["arac"], g["pencere_kaynak"] = (bilgi["pencere"], bilgi["gorsel"], g["pencere"], g["gorsel"], g["arac"], g["pencere_kaynak"] = (bilgi["pencere"], bilgi["gorsel"],
bilgi["arac"], bilgi["kaynak"]) bilgi["arac"], bilgi["kaynak"])
g["claude"] = pencere.claude_var(g["kind"], g.get("yol"), kayitlar.get(g["id"])) g["claude"] = pencere.claude_var(g["kind"], g.get("yol"), kayitlar.get(g["id"]))
+14 -10
View File
@@ -40,7 +40,7 @@ from bekci_watch import MetadataFilter
# Installed only by owned native launcher, never an HTTP control endpoint. # Installed only by owned native launcher, never an HTTP control endpoint.
WATCH_OWNER = None WATCH_OWNER = None
SURUM = "14" SURUM = "17"
HERE = os.path.dirname(os.path.abspath(__file__)) HERE = os.path.dirname(os.path.abspath(__file__))
try: try:
with open(os.path.abspath(__file__), "rb") as _f: with open(os.path.abspath(__file__), "rb") as _f:
@@ -365,6 +365,10 @@ def arac_semalarini_duzelt(req):
def flatten_request(req, tool_names=None): def flatten_request(req, tool_names=None):
"""additional_tools/namespace/custom -> duz function tools. Donus: custom arac adlari kumesi.""" """additional_tools/namespace/custom -> duz function tools. Donus: custom arac adlari kumesi."""
if isinstance(req.get("input"), list) and any(
isinstance(item, dict) and item.get("type") == "agent_message" for item in req["input"]):
import gecis_context
req["input"] = gecis_context.portable_reasoning(req)["input"]
custom = set() custom = set()
tools = [] tools = []
tool_names = tool_names if tool_names is not None else {} tool_names = tool_names if tool_names is not None else {}
@@ -1031,13 +1035,13 @@ class H(http.server.BaseHTTPRequestHandler):
if jev_denetim.muaf_mi(hedef): if jev_denetim.muaf_mi(hedef):
return self._stream(resp, resp.status, tag, rewriter, izle, basliksiz=True) return self._stream(resp, resp.status, tag, rewriter, izle, basliksiz=True)
if gecmedi: if gecmedi:
# Son katman ne dondurduyse o gider, hicbir sey eklenmez; dondurmediyse hicbir sey gitmez. # Headers already declare SSE. A raw upstream JSON/HTML body
if son_govde: # here leaves Codex waiting for a terminal event and retrying.
w(son_govde) event = {"type": "response.failed", "response": {
else: "id": "resp_astrabridge_%d" % time.time_ns(),
event = {"type": "response.failed", "response": {"status": "failed", "error": { "object": "response", "status": "failed", "output": [], "error": {
"code": "jev_no_fallback", "message": "Jev denetiminden gecen cevap alinamadi; gecis kurallarini kontrol edin."}}} "code": "jev_no_fallback", "message": "Jev denetiminden gecen cevap alinamadi; gecis kurallarini kontrol edin."}}}
w(b"event: response.failed\ndata: " + json.dumps(event).encode() + b"\n\n") w(b"event: response.failed\ndata: " + json.dumps(event).encode() + b"\n\n")
else: else:
for chunk in parcalar: for chunk in parcalar:
if izle is not None: if izle is not None:
@@ -1283,7 +1287,7 @@ class H(http.server.BaseHTTPRequestHandler):
for model in json.loads(cached["body"]).get("models", []): for model in json.loads(cached["body"]).get("models", []):
if model.get("slug") == slug and model.get("context_window"): if model.get("slug") == slug and model.get("context_window"):
return int(model["context_window"]) return int(model["context_window"])
return 32768 raise gecis_context.TransferError("Abonelik modelinin bağlam kapasitesi katalogda bulunamadı; geçmiş korundu.")
def _prepare_transfer(self, payload): def _prepare_transfer(self, payload):
@@ -1747,7 +1751,7 @@ class H(http.server.BaseHTTPRequestHandler):
yanit = urllib.request.urlopen(urllib.request.Request( yanit = urllib.request.urlopen(urllib.request.Request(
jev_rota["url"], data=json.dumps(katman_req, ensure_ascii=False).encode("utf-8"), headers=jev_hdrs, method="POST"), jev_rota["url"], data=json.dumps(katman_req, ensure_ascii=False).encode("utf-8"), headers=jev_hdrs, method="POST"),
timeout=UPSTREAM_IDLE_TIMEOUT) timeout=UPSTREAM_IDLE_TIMEOUT)
return (yanit, SseRewriter(custom, hedef=ad, tasma=self._tasma_ogren, aracsiz=self._aracsiz_ogren), return (yanit, SseRewriter(custom, hedef=ad, tasma=self._tasma_ogren, aracsiz=self._aracsiz_ogren, tool_names=tool_names),
f"[{n} {slug}=>{ad}]") f"[{n} {slug}=>{ad}]")
# A 9Router combo (Fusion-*, no provider prefix) runs a whole panel upstream. Once it may # A 9Router combo (Fusion-*, no provider prefix) runs a whole panel upstream. Once it may
# have started (timeout, 504, cut stream) resending it runs and bills it twice (28 Sep 2026); # have started (timeout, 504, cut stream) resending it runs and bills it twice (28 Sep 2026);
+8 -1
View File
@@ -221,7 +221,7 @@ def _yapraklar(dugum, derinlik=0):
return cikti return cikti
def coz(kimlik, tur, yol, kayit=None, nano=None, ogr=None, aracsiz=()): def coz(kimlik, tur, yol, kayit=None, nano=None, ogr=None, aracsiz=(), ilan=None):
"""Bir katalog girdisinin ilan edilecek penceresi ve yetenekleri. """Bir katalog girdisinin ilan edilecek penceresi ve yetenekleri.
kimlik: gecit kimligi; tur: "combo" | "model"; yol: gecidin aciklamasindaki "saglayici/model" yolu; kimlik: gecit kimligi; tur: "combo" | "model"; yol: gecidin aciklamasindaki "saglayici/model" yolu;
kayit: /router/catalog girdisi (combo topolojisi icin). Donus: {"pencere","gorsel","arac","kaynak"}.""" kayit: /router/catalog girdisi (combo topolojisi icin). Donus: {"pencere","gorsel","arac","kaynak"}."""
@@ -246,6 +246,13 @@ def coz(kimlik, tur, yol, kayit=None, nano=None, ogr=None, aracsiz=()):
m = re.match(r"no-think-(?:9router-)?([a-z0-9]+)-", kimlik or "") m = re.match(r"no-think-(?:9router-)?([a-z0-9]+)-", kimlik or "")
yol = (m.group(1) + "/" + yol[len("no-think/"):]) if m else yol yol = (m.group(1) + "/" + yol[len("no-think/"):]) if m else yol
sonuc = yol_bilgisi(yol, nano) or dict(bilinmeyen) sonuc = yol_bilgisi(yol, nano) or dict(bilinmeyen)
# Exact /models metadata is stronger evidence than name-based local
# fallbacks. Do not infer a combo's capacity from its display name.
ilan = ilan if isinstance(ilan, dict) else {}
canli_sinirlar = [ilan[k] for k in ("context_length", "max_input_tokens")
if type(ilan.get(k)) is int and TABAN <= ilan[k] <= 10_000_000]
if canli_sinirlar:
sonuc = {**sonuc, "pencere": min(canli_sinirlar), "kaynak": "saglayici-katalogu"}
if kimlik in aracsiz: if kimlik in aracsiz:
sonuc = {**sonuc, "arac": False} sonuc = {**sonuc, "arac": False}
sinir = (ogr or {}).get(kimlik) sinir = (ogr or {}).get(kimlik)
@@ -0,0 +1,52 @@
"""Regression fixture matching actual Codex child delivery, with no user data."""
import copy
import unittest
import gecis_context
import kopru
class AgentPayloadTests(unittest.TestCase):
def request(self, text):
return {"model": "ide/test", "input": [{
"type": "agent_message", "author": "/root", "recipient": "/root/test",
"content": [
{"type": "input_text", "text": "Message Type: NEW_TASK\nPayload:\n"},
{"type": "encrypted_content", "encrypted_content": text},
],
}]}
def test_actual_peer_wrapper_survives_external_text_extraction(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)
# 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.assertIn('/root/test', text)
self.assertEqual(request["input"][0]["role"], "user")
self.assertEqual(original["input"][0]["content"][1]["type"], "encrypted_content")
def test_peer_result_and_plain_parts_keep_order_without_source_mutation(self):
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(request, original)
def test_opaque_peer_payload_fails_instead_of_sending_header_only(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))
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"], [])
if __name__ == "__main__":
unittest.main()
@@ -22,6 +22,26 @@ from test_model_switch import env
class History(unittest.TestCase): class History(unittest.TestCase):
def test_agent_task_survives_external_projection_and_recent_history(self):
task = {"type": "agent_message", "id": "amsg_task", "author": "/root",
"recipient": "/root/child", "content": [
{"type": "input_text", "text": "Message Type: NEW_TASK\nRun CHILD_TOOL_410962"}]}
req = {"input": [{"role": "user", "content": "Old setup"}, task]}
before = copy.deepcopy(req)
self.assertEqual(gc.normalize(req), before)
projected = gc.portable_reasoning(req)
message = projected["input"][-1]
self.assertEqual(message["role"], "user")
self.assertEqual(message["content"][1:], task["content"])
self.assertIn('/root/child', message["content"][0]["text"])
self.assertNotIn("id", message)
self.assertEqual(gc.split_history(projected["input"], keep_users=1)[2], [message])
self.assertEqual(req, before)
self.assertEqual(gc.portable_reasoning(projected), projected)
direct = copy.deepcopy(req)
env["flatten_request"](direct)
self.assertEqual(direct["input"][-1], message)
def test_native_function_results_are_not_foreign_or_renamed(self): def test_native_function_results_are_not_foreign_or_renamed(self):
req = {"input": [{"type": "function_call_output", "id": "fco_native", req = {"input": [{"type": "function_call_output", "id": "fco_native",
"call_id": "call_native", "output": "kept"}]} "call_id": "call_native", "output": "kept"}]}
@@ -335,7 +335,7 @@ class Akis(unittest.TestCase):
self.assertNotIn(b"yapamam", self.h.wfile.getvalue()) self.assertNotIn(b"yapamam", self.h.wfile.getvalue())
self.assertIn(b'response.failed', self.sent()) self.assertIn(b'response.failed', self.sent())
def test_the_last_layers_own_error_body_is_delivered_untouched(self): def test_last_layer_http_error_terminates_sse_without_leaking_raw_body(self):
class Hata(Exception): class Hata(Exception):
def read(self): def read(self):
return b'{"error": {"message": "katman 4 hatasi"}}' return b'{"error": {"message": "katman 4 hatasi"}}'
@@ -344,7 +344,14 @@ class Akis(unittest.TestCase):
def ac(ad): def ac(ad):
raise Hata("HTTP 400") raise Hata("HTTP 400")
self.h._denetimli_akis(Yanit(sse("Bunu yapamam.")), "[1]", None, None, "yap", ["Katman-4"], ac) self.h._denetimli_akis(Yanit(sse("Bunu yapamam.")), "[1]", None, None, "yap", ["Katman-4"], ac)
self.assertEqual(self.sent(), b'{"error": {"message": "katman 4 hatasi"}}') raw = self.sent()
self.assertTrue(raw.startswith(b"event: response.failed\ndata: "))
event = json.loads(raw.split(b"data: ", 1)[1])
self.assertEqual(event["response"]["status"], "failed")
self.assertEqual(event["response"]["error"]["code"], "jev_no_fallback")
self.assertEqual(event["response"]["output"], [])
self.assertNotIn(b"katman 4 hatasi", raw)
self.assertNotIn(b"Bunu yapamam", raw)
def test_unreachable_jev_keeps_the_answer(self): def test_unreachable_jev_keeps_the_answer(self):
self.puanlar = [None] self.puanlar = [None]
@@ -0,0 +1,18 @@
import pencere
def test_exact_combo_limits_override_unknown_and_local_defaults():
row = {"context_length": 872000, "max_input_tokens": 872000, "max_output_tokens": 128000}
result = pencere.coz("GPT-6-Astra", "combo", None, ilan=row)
assert result["pencere"] == 872000
assert result["kaynak"] == "saglayici-katalogu"
result = pencere.coz("codex-gpt-6-astra", "model", "codex/gpt-6-astra", ilan=row)
assert result["pencere"] == 872000
def test_learned_actual_limit_still_wins_and_invalid_metadata_is_ignored():
result = pencere.coz("combo", "combo", None, ilan={"context_length": 872000}, ogr={"combo": 500000})
assert result["pencere"] == 500000 and result["kaynak"] == "ogrenilen"
for value in (True, -1, "872000", 999999999):
result = pencere.coz("combo", "combo", None, ilan={"context_length": value})
assert result["pencere"] == pencere.VARSAYILAN
@@ -0,0 +1,83 @@
"""Native switch regressions using the real HTTP splitter and offline peers."""
import copy
import gzip
import json
import unittest
import ast
from pathlib import Path
import bridge
import gecis_context as gc
import ozet_kopru
import test_ayirici as fixtures
class NativeSwitch(unittest.TestCase):
setUp = fixtures.Ayirici.setUp
tearDown = fixtures.Ayirici.tearDown
iste = fixtures.Ayirici.iste
def test_unknown_native_capacity_is_not_invented(self):
tree = ast.parse(Path(__file__).with_name('kopru.py').read_text(encoding='utf-8'))
method = next(node for node in ast.walk(tree)
if isinstance(node, ast.FunctionDef) and node.name == '_transfer_window')
ns = {'ek_model_mu': lambda _: False, 'LAST_MODELS': {}, 'gecis_context': gc, 'json': json}
exec(compile(ast.Module(body=[method], type_ignores=[]), 'window', 'exec'), ns)
from types import SimpleNamespace
with self.assertRaises(gc.TransferError):
ns['_transfer_window'](SimpleNamespace(headers={}), 'gpt-6-astra')
def test_opaque_native_switch_with_persisted_foreign_residue(self):
self.ozgun.cevap = fixtures.duz(200, b'data: {"type":"response.completed"}\n\n', 'text/event-stream')
compact = {'type': 'compaction', 'id': 'cmp_native',
'encrypted_content': 'gAAAAABp' + 'AqZ09_-jX' * 10000}
req = {'model': 'gpt-6.1-sol', 'prompt_cache_key': 'restart', 'input': [
ozet_kopru.ozet_ogesi('DECISION-4827 must survive'), compact,
{'type': 'custom_tool_call', 'id': 'fc_123', 'call_id': 'call_123',
'name': 'apply_patch', 'input': 'patch text'},
{'type': 'custom_tool_call_output', 'id': 'fco_123',
'call_id': 'call_123', 'output': 'complete'},
{'role': 'user', 'content': 'continue'}]}
before = copy.deepcopy(req)
for cold, model in ((True, 'gpt-6.1-sol'), (False, 'gpt-6-astra'), (True, 'gpt-6-astra')):
if cold:
bridge.TRANSFERS.clear()
req['model'] = model
body = gzip.compress(json.dumps(req).encode())
self.assertEqual(self.iste('POST', '/backend-api/codex/responses', body,
{'Content-Encoding': 'gzip'})[0], 200)
sent = json.loads(self.ozgun.gelen[-1]['govde'])
self.assertEqual(sent['input'][1], compact)
self.assertEqual(sent['input'][2]['id'], 'ctc_123')
self.assertEqual(sent['input'][3]['id'], 'ctco_123')
self.assertEqual(sent['input'][3]['call_id'], 'call_123')
self.assertIn('DECISION-4827', json.dumps(sent))
self.assertEqual(self.kopru.gelen, [])
self.assertEqual(req['input'], before['input'])
def test_clean_native_body_is_byte_identical_even_after_external_source(self):
req = {'model': 'ide/test', 'prompt_cache_key': 'return', 'input': []}
self.iste('POST', '/backend-api/codex/responses', json.dumps(req).encode())
self.kopru.gelen.clear()
req.update(model='gpt-6-astra', input=[{'type': 'compaction',
'encrypted_content': 'gAAAAABp' + 'X' * 100000}])
raw = gzip.compress(json.dumps(req, indent=3).encode())
self.iste('POST', '/backend-api/codex/responses', raw, {'Content-Encoding': 'gzip'})
self.assertEqual(self.ozgun.gelen[-1]['govde'], raw)
self.assertEqual(self.kopru.gelen, [])
def test_cold_native_tool_id_repair_does_not_require_semantic_budget(self):
req = {'model': 'gpt-6-astra', 'input': [
{'type': 'custom_tool_call', 'id': 'fc_old', 'call_id': 'call_old',
'name': 'apply_patch', 'input': 'x' * 100000},
{'type': 'custom_tool_call_output', 'id': 'fco_old',
'call_id': 'call_old', 'output': 'complete'}]}
self.iste('POST', '/backend-api/codex/responses', json.dumps(req).encode())
self.assertEqual(len(self.kopru.gelen), 0)
sent = json.loads(self.ozgun.gelen[-1]['govde'])
self.assertEqual(sent, gc.normalize(req))
self.assertEqual(req['input'][0]['id'], 'fc_old')
if __name__ == '__main__':
unittest.main()
+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' export const CODEX_LINK_MARKER = '# AITURK IDE Codex Baglama'
// Must equal SURUM in assets/codex-bridge/bridge.py; a running bridge that reports // 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. // another version is restarted so an updated IDE never keeps an old bridge alive.
export const CODEX_BRIDGE_VERSION = '14' export const CODEX_BRIDGE_VERSION = '17'
export const CODEX_LINK_LINE = `openai_base_url = "${CODEX_LINK_ORIGIN}/backend-api/codex" ${CODEX_LINK_MARKER}` 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' export const CODEX_LINK_RUN_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "aiturk-ide", "name": "aiturk-ide",
"productName": "AITURK IDE", "productName": "AITURK IDE",
"private": true, "private": true,
"version": "1.0.0-beta.33", "version": "1.0.0-beta.36",
"description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.", "description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.",
"author": "AITURK / TurkServis", "author": "AITURK / TurkServis",
"repository": { "repository": {
+1 -1
View File
@@ -65,7 +65,7 @@
}, },
"apps/desktop": { "apps/desktop": {
"name": "aiturk-ide", "name": "aiturk-ide",
"version": "1.0.0-beta.33", "version": "1.0.0-beta.36",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@assistant-ui/core": "0.2.23", "@assistant-ui/core": "0.2.23",
@@ -20,6 +20,7 @@ import subprocess
import pytest import pytest
from tools.file_operations import ShellFileOperations from tools.file_operations import ShellFileOperations
from tools.environments.local import LocalEnvironment
# Big enough to prove containment, small enough to keep the test fast. # Big enough to prove containment, small enough to keep the test fast.
GIANT = 5 * 1024 * 1024 # 5MB single line GIANT = 5 * 1024 * 1024 # 5MB single line
@@ -27,22 +28,20 @@ GIANT = 5 * 1024 * 1024 # 5MB single line
STDOUT_CEILING = 1 * 1024 * 1024 STDOUT_CEILING = 1 * 1024 * 1024
class RecordingEnv: class RecordingEnv(LocalEnvironment):
"""Local bash executor that records the largest stdout it returned.""" """Local bash executor that records the largest stdout it returned."""
def __init__(self, cwd): def __init__(self, cwd):
self.cwd = cwd super().__init__(cwd=cwd)
self.max_stdout = 0 self.max_stdout = 0
def execute(self, command, timeout=60, **kwargs): def execute(self, command, timeout=60, **kwargs):
proc = subprocess.run( # Use the application's shell selection/path handling. On Windows a
["bash", "-c", command], # bare 'bash' may select WSL, whose filesystem is not the native one.
capture_output=True, text=True, errors="replace", result = super().execute(command, timeout=timeout + 30, **kwargs)
timeout=timeout + 30, out = result.get("output", "")
)
out = proc.stdout + (proc.stderr or "")
self.max_stdout = max(self.max_stdout, len(out)) self.max_stdout = max(self.max_stdout, len(out))
return {"output": out, "returncode": proc.returncode} return result
@pytest.fixture() @pytest.fixture()
@@ -58,6 +57,8 @@ def _ops(giant_dir, engine):
env = RecordingEnv(str(giant_dir)) env = RecordingEnv(str(giant_dir))
ops = ShellFileOperations(env) ops = ShellFileOperations(env)
ops._has_command = lambda cmd: cmd == engine ops._has_command = lambda cmd: cmd == engine
resolve = ops._resolve_command
ops._resolve_command = lambda cmd: None if cmd == "rg" and engine != "rg" else resolve(cmd)
return ops, env return ops, env
@@ -96,5 +97,5 @@ def test_line_cap_skipped_for_path_and_count_modes(giant_dir, engine, output_mod
if output_mode == "files_only": if output_mode == "files_only":
assert {os.path.basename(f) for f in result.files} == {"trace.json", "small.py"} assert {os.path.basename(f) for f in result.files} == {"trace.json", "small.py"}
else: else:
assert {os.path.basename(k) for k in result.counts} == {"trace.json", "small.py"} assert {os.path.basename(k) for k, count in result.counts.items() if count} == {"trace.json", "small.py"}
assert env.max_stdout < STDOUT_CEILING assert env.max_stdout < STDOUT_CEILING
+112
View File
@@ -0,0 +1,112 @@
"""Project scope and resource limits: real local paths, no broad subprocesses."""
from unittest.mock import Mock
from tools.file_operations import ShellFileOperations, SearchResult, ExecuteResult
from tools.environments.local import LocalEnvironment
def projects(tmp_path):
for name in ('one', 'two'):
project = tmp_path / name
project.mkdir()
(project / '.git').mkdir()
(project / 'source.py').write_text('needle\n')
return tmp_path / 'one'
def test_ambiguous_scope_starts_no_process(tmp_path):
projects(tmp_path)
ops = ShellFileOperations(LocalEnvironment(str(tmp_path)))
ops._exec = Mock(side_effect=AssertionError('must not start process'))
result = ops.search('needle', str(tmp_path))
assert 'scope' in result.error.lower()
assert 'one' in result.error and 'two' in result.error
ops._exec.assert_not_called()
def test_umbrella_uses_cwd_project_and_reports_it(tmp_path):
project = projects(tmp_path)
ops = ShellFileOperations(LocalEnvironment(str(project)))
ops._exec = Mock(return_value=ExecuteResult(stdout='exists', exit_code=0))
ops._search_content = Mock(return_value=SearchResult())
result = ops.search('needle', str(tmp_path))
assert ops._search_content.call_args.args[1] == str(project)
assert str(project) in result.warning
def test_broad_opt_in_and_explicit_file(tmp_path):
project = projects(tmp_path)
ops = ShellFileOperations(LocalEnvironment(str(project)))
ops._exec = Mock(return_value=ExecuteResult(stdout='exists', exit_code=0))
ops._search_content = Mock(return_value=SearchResult())
for root, broad in ((tmp_path, True), (project / 'source.py', False)):
assert not ops.search('needle', str(root), allow_broad=broad).error
assert ops._search_content.call_args.args[1] == str(root)
def test_rg_commands_bound_threads_and_prune(tmp_path):
project = projects(tmp_path)
ops = ShellFileOperations(LocalEnvironment(str(project)))
ops._exec = Mock(return_value=ExecuteResult(stdout='', exit_code=1))
ops._search_with_rg('needle', str(project), '*.py', 50, 0, 'content', 0, rg_executable='rg')
command = ops._exec.call_args.args[0]
assert '--threads 2' in command
assert 'node_modules' in command and '.git' in command
assert '*.py' in command
ops._search_files_rg('*.py', str(project), 50, 0, rg_executable='rg')
command = ops._exec.call_args.args[0]
assert '--threads 2' in command and 'node_modules' in command
def test_concurrent_searches_have_two_slots_and_release_after_failure(tmp_path):
import threading
from concurrent.futures import ThreadPoolExecutor
project = projects(tmp_path)
lock = threading.Lock()
release = threading.Event()
entered = threading.Event()
active = peak = 0
def run_search(*args):
nonlocal active, peak
with lock:
active += 1
peak = max(peak, active)
if active == 2:
entered.set()
try:
assert release.wait(5)
return SearchResult()
finally:
with lock:
active -= 1
with ThreadPoolExecutor(max_workers=6) as pool:
pending = []
for _ in range(6):
ops = ShellFileOperations(LocalEnvironment(str(project)))
ops._search_scoped = run_search
pending.append(pool.submit(ops.search, 'needle'))
try:
assert entered.wait(3)
assert peak == 2
finally:
release.set()
assert all(not task.result().error for task in pending)
assert peak == 2
def test_real_rg_project_content_and_nested_glob(tmp_path):
project = projects(tmp_path)
(project / 'src').mkdir()
(project / 'src' / 'nested.py').write_text('needle\n')
(project / 'node_modules').mkdir()
(project / 'node_modules' / 'noise.py').write_text('needle\n')
ops = ShellFileOperations(LocalEnvironment(str(project)))
result = ops.search('needle', str(tmp_path), file_glob='*.py')
assert not result.error
assert len(result.matches) == 2
assert all('node_modules' not in item.path for item in result.matches)
result = ops.search('src/*.py', str(project), target='files')
assert not result.error
assert len(result.files) == 1 and 'nested.py' in result.files[0]
+56 -12
View File
@@ -50,7 +50,8 @@ from agent.file_safety import (
get_write_denied_error, get_write_denied_error,
is_write_denied as _shared_is_write_denied, is_write_denied as _shared_is_write_denied,
) )
from agent.search_policy import SEARCH_PRUNE_DIR_NAMES from agent.search_policy import (SEARCH_PRUNE_DIR_NAMES, SEARCH_RG_THREADS,
SEARCH_MAX_CONCURRENT, local_search_scope)
from tools import interrupt as tool_interrupt from tools import interrupt as tool_interrupt
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -73,6 +74,8 @@ _MACOS_TCC_PROTECTED_HOME_DIRS = (
) )
_SEARCH_SLOTS = threading.BoundedSemaphore(SEARCH_MAX_CONCURRENT)
_FILENAME_SEARCH_ADMISSION = threading.Condition() _FILENAME_SEARCH_ADMISSION = threading.Condition()
_ACTIVE_FILENAME_SEARCH_ROOTS: set[tuple[str, str, str]] = set() _ACTIVE_FILENAME_SEARCH_ROOTS: set[tuple[str, str, str]] = set()
_FILENAME_SEARCH_WAIT_SECONDS = 0.05 _FILENAME_SEARCH_WAIT_SECONDS = 0.05
@@ -3432,7 +3435,31 @@ class ShellFileOperations(FileOperations):
def search(self, pattern: str, path: str = ".", target: str = "content", def search(self, pattern: str, path: str = ".", target: str = "content",
file_glob: Optional[str] = None, limit: int = 50, offset: int = 0, file_glob: Optional[str] = None, limit: int = 50, offset: int = 0,
output_mode: str = "content", context: int = 0, output_mode: str = "content", context: int = 0,
order: str = "discovery") -> SearchResult: order: str = "discovery", allow_broad: bool = False) -> SearchResult:
# One admission slot covers discovery, traversal and zero-match probes.
# It is process-wide across file-ops instances, and interruptible.
while not _SEARCH_SLOTS.acquire(timeout=0.05):
if tool_interrupt.is_interrupted():
return SearchResult(error="Search interrupted while waiting for a search slot.")
try:
if tool_interrupt.is_interrupted():
return SearchResult(error="Search interrupted.")
return self._search_scoped(pattern, path, target, file_glob, limit,
offset, output_mode, context, order, allow_broad)
finally:
_SEARCH_SLOTS.release()
def _project_search_scope(self, path, allow_broad=False):
from tools.environments.local import LocalEnvironment, _msys_to_windows_path
if allow_broad or not isinstance(self.env, LocalEnvironment):
return path, None, None
cwd = getattr(self.env, "cwd", None) or self.cwd
return local_search_scope(_msys_to_windows_path(path), cwd, _HOME)
def _search_scoped(self, pattern: str, path: str = ".", target: str = "content",
file_glob: Optional[str] = None, limit: int = 50, offset: int = 0,
output_mode: str = "content", context: int = 0,
order: str = "discovery", allow_broad: bool = False) -> SearchResult:
""" """
Search for content or files. Search for content or files.
@@ -3460,6 +3487,9 @@ class ShellFileOperations(FileOperations):
# Expand ~ and other shell paths # Expand ~ and other shell paths
path = self._expand_path(path) path = self._expand_path(path)
path, scope_notice, scope_error = self._project_search_scope(path, allow_broad)
if scope_error:
return SearchResult(error=scope_error)
# Validate that the path exists before searching # Validate that the path exists before searching
check = self._exec(f"test -e {self._escape_shell_arg(path)} && echo exists || echo not_found") check = self._exec(f"test -e {self._escape_shell_arg(path)} && echo exists || echo not_found")
@@ -3470,7 +3500,7 @@ class ShellFileOperations(FileOperations):
# merge the results, and report the skipped parts. # merge the results, and report the skipped parts.
multi = self._try_multi_path_search( multi = self._try_multi_path_search(
pattern, path, target, file_glob, limit, offset, output_mode, context, pattern, path, target, file_glob, limit, offset, output_mode, context,
order, order, allow_broad,
) )
if multi is not None: if multi is not None:
return multi return multi
@@ -3518,6 +3548,8 @@ class ShellFileOperations(FileOperations):
f"an unattended privacy prompt: {skipped}. Search a protected " f"an unattended privacy prompt: {skipped}. Search a protected "
"folder directly when access is intentional." "folder directly when access is intentional."
) )
if scope_notice:
result.warning = " ".join(filter(None, [scope_notice, result.warning]))
return result return result
def _macos_search_exclusions(self, path: str) -> List[str]: def _macos_search_exclusions(self, path: str) -> List[str]:
@@ -3587,7 +3619,7 @@ class ShellFileOperations(FileOperations):
def _try_multi_path_search(self, pattern: str, path: str, target: str, def _try_multi_path_search(self, pattern: str, path: str, target: str,
file_glob: Optional[str], limit: int, offset: int, file_glob: Optional[str], limit: int, offset: int,
output_mode: str, context: int, output_mode: str, context: int,
order: str = "discovery") -> Optional[SearchResult]: order: str = "discovery", allow_broad: bool = False) -> Optional[SearchResult]:
"""Recover a not-found ``path`` that is really several paths in one string. """Recover a not-found ``path`` that is really several paths in one string.
Production trajectories show models passing "dir1 dir2 dir3" (or Production trajectories show models passing "dir1 dir2 dir3" (or
@@ -3604,9 +3636,14 @@ class ShellFileOperations(FileOperations):
parts = path.split() parts = path.split()
if len(parts) < 2: if len(parts) < 2:
return None return None
existing, missing = [], [] existing, missing, scope_notices = [], [], []
for p in parts: for p in parts:
expanded = self._expand_path(p) expanded = self._expand_path(p)
expanded, notice, error = self._project_search_scope(expanded, allow_broad)
if error:
return SearchResult(error=error)
if notice:
scope_notices.append(notice)
chk = self._exec( chk = self._exec(
f"test -e {self._escape_shell_arg(expanded)} && echo exists || echo not_found" f"test -e {self._escape_shell_arg(expanded)} && echo exists || echo not_found"
) )
@@ -3641,7 +3678,7 @@ class ShellFileOperations(FileOperations):
note += "; skipped missing: " + ", ".join(missing[:3]) note += "; skipped missing: " + ", ".join(missing[:3])
if len(missing) > 3: if len(missing) > 3:
note += f" (+{len(missing) - 3} more)" note += f" (+{len(missing) - 3} more)"
warning_parts = [note] warning_parts = [note, *scope_notices]
if not merged.error: if not merged.error:
protected_paths = [ protected_paths = [
absolute absolute
@@ -3704,7 +3741,7 @@ class ShellFileOperations(FileOperations):
glob_expr = f" --glob {self._escape_shell_arg(file_glob)}" if file_glob else "" glob_expr = f" --glob {self._escape_shell_arg(file_glob)}" if file_glob else ""
probe = self._exec( probe = self._exec(
f"{rg} -i --count-matches{glob_expr} " f"{rg} --threads {SEARCH_RG_THREADS} -i --count-matches{glob_expr} {self._search_prune_glob_args()} "
f"{self._escape_shell_arg(pattern)} {self._escape_native_tool_arg(path)} " f"{self._escape_shell_arg(pattern)} {self._escape_native_tool_arg(path)} "
f"2>/dev/null | head -50", f"2>/dev/null | head -50",
timeout=30, timeout=30,
@@ -3723,7 +3760,7 @@ class ShellFileOperations(FileOperations):
# files remain diagnosable, but prune heavyweight trees before rg can # files remain diagnosable, but prune heavyweight trees before rg can
# recurse into them. # recurse into them.
hidden = self._exec( hidden = self._exec(
f"{rg} --hidden --no-ignore --count-matches{glob_expr}" f"{rg} --threads {SEARCH_RG_THREADS} --hidden --no-ignore --count-matches{glob_expr}"
f" {self._search_prune_glob_args()} " f" {self._search_prune_glob_args()} "
f"{self._escape_shell_arg(pattern)} {self._escape_native_tool_arg(path)} " f"{self._escape_shell_arg(pattern)} {self._escape_native_tool_arg(path)} "
f"2>/dev/null | head -50", f"2>/dev/null | head -50",
@@ -3738,7 +3775,7 @@ class ShellFileOperations(FileOperations):
) )
if re.search(r"[.\[\](){}?*+^$\\|]", pattern): if re.search(r"[.\[\](){}?*+^$\\|]", pattern):
fixed = self._exec( fixed = self._exec(
f"{rg} -F --count-matches{glob_expr} " f"{rg} --threads {SEARCH_RG_THREADS} -F --count-matches{glob_expr} {self._search_prune_glob_args()} "
f"{self._escape_shell_arg(pattern)} {self._escape_native_tool_arg(path)} " f"{self._escape_shell_arg(pattern)} {self._escape_native_tool_arg(path)} "
f"2>/dev/null | head -50", f"2>/dev/null | head -50",
timeout=30, timeout=30,
@@ -3813,7 +3850,7 @@ class ShellFileOperations(FileOperations):
)) ))
try: try:
return self._search_files_rg( return self._search_files_rg(
search_pattern, path, limit, offset, order, pattern, path, limit, offset, order,
rg_executable=rg_executable, rg_executable=rg_executable,
) )
finally: finally:
@@ -3848,6 +3885,9 @@ class ShellFileOperations(FileOperations):
hidden_prune = ( hidden_prune = (
f" \\( -type d -name '.*'{root_exemptions} \\) -prune -o" f" \\( -type d -name '.*'{root_exemptions} \\) -prune -o"
) )
names = " -o ".join(f"-name {self._escape_shell_arg(name)}"
for name in sorted(SEARCH_PRUNE_DIR_NAMES))
hidden_prune += f" \\( -type d \\( {names} \\){root_exemptions} \\) -prune -o"
protected_paths = [ protected_paths = [
absolute absolute
for _root, _relative, absolute for _root, _relative, absolute
@@ -3986,7 +4026,7 @@ class ShellFileOperations(FileOperations):
f"cd {self._escape_shell_arg(scoped_common)} && " if scoped_common else "" f"cd {self._escape_shell_arg(scoped_common)} && " if scoped_common else ""
) )
cmd = ( cmd = (
f"set -o pipefail; {cd_prefix}{rg} --files{sort_arg} -g {self._escape_shell_arg(glob_pattern)}" f"set -o pipefail; {cd_prefix}{rg} --files{sort_arg} --threads {SEARCH_RG_THREADS} -g {self._escape_shell_arg(glob_pattern)} {self._search_prune_glob_args()}"
f"{exclusion_args} -- {root_args} 2>/dev/null | head -n {fetch_limit}" f"{exclusion_args} -- {root_args} 2>/dev/null | head -n {fetch_limit}"
) )
result = self._exec(cmd, timeout=60) result = self._exec(cmd, timeout=60)
@@ -4062,7 +4102,7 @@ class ShellFileOperations(FileOperations):
rg_executable = rg_executable or self._resolve_command("rg") rg_executable = rg_executable or self._resolve_command("rg")
if not rg_executable: if not rg_executable:
return SearchResult(error="Content search requires ripgrep (rg).") return SearchResult(error="Content search requires ripgrep (rg).")
cmd_parts = [self._quote_executable(rg_executable), "--line-number", "--no-heading", "--with-filename"] cmd_parts = [self._quote_executable(rg_executable), "--threads", str(SEARCH_RG_THREADS), "--line-number", "--no-heading", "--with-filename"]
# Giant-single-line containment (ported from cline/cline#13525): a # Giant-single-line containment (ported from cline/cline#13525): a
# match inside a serialized dump (multi-MB single-line JSON/minified # match inside a serialized dump (multi-MB single-line JSON/minified
@@ -4100,6 +4140,8 @@ class ShellFileOperations(FileOperations):
if file_glob: if file_glob:
cmd_parts.extend(["--glob", self._escape_shell_arg(file_glob)]) cmd_parts.extend(["--glob", self._escape_shell_arg(file_glob)])
cmd_parts.append(self._search_prune_glob_args())
# Output mode handling # Output mode handling
if output_mode == "files_only": if output_mode == "files_only":
cmd_parts.append("-l") # Files only cmd_parts.append("-l") # Files only
@@ -4230,6 +4272,8 @@ class ShellFileOperations(FileOperations):
# Exclude hidden directories (matching ripgrep's default behavior). # Exclude hidden directories (matching ripgrep's default behavior).
# This prevents searching inside .hub/index-cache/, .git/, etc. # This prevents searching inside .hub/index-cache/, .git/, etc.
cmd_parts.append("--exclude-dir='.*'") cmd_parts.append("--exclude-dir='.*'")
cmd_parts.extend(f"--exclude-dir={self._escape_shell_arg(name)}"
for name in sorted(SEARCH_PRUNE_DIR_NAMES))
# Protected-dir pruning CANNOT use --exclude-dir here: grep matches # Protected-dir pruning CANNOT use --exclude-dir here: grep matches
# exclude-dir globs against BASENAMES anywhere in the tree, so # exclude-dir globs against BASENAMES anywhere in the tree, so
+6 -2
View File
@@ -2653,7 +2653,7 @@ def search_tool(pattern: str, target: str = "content", path: str = ".",
file_glob: str = None, limit: int = 50, offset: int = 0, file_glob: str = None, limit: int = 50, offset: int = 0,
output_mode: str = "content", context: int = 0, output_mode: str = "content", context: int = 0,
order: str = "discovery", order: str = "discovery",
task_id: str = "default") -> str: task_id: str = "default", allow_broad: bool = False) -> str:
"""Search for content or files.""" """Search for content or files."""
try: try:
offset, limit = normalize_search_pagination(offset, limit) offset, limit = normalize_search_pagination(offset, limit)
@@ -2670,6 +2670,7 @@ def search_tool(pattern: str, target: str = "content", path: str = ".",
limit, limit,
offset, offset,
order, order,
allow_broad,
) )
with _read_tracker_lock: with _read_tracker_lock:
task_data = _read_tracker.setdefault(task_id, { task_data = _read_tracker.setdefault(task_id, {
@@ -2717,6 +2718,7 @@ def search_tool(pattern: str, target: str = "content", path: str = ".",
pattern=pattern, path=path, target=target, file_glob=file_glob, pattern=pattern, path=path, target=target, file_glob=file_glob,
limit=limit, offset=offset, output_mode=output_mode, context=context, limit=limit, offset=offset, output_mode=output_mode, context=context,
order=order, order=order,
**({"allow_broad": True} if allow_broad else {}),
) )
omitted = _filter_read_blocked_search_results(result, task_id) omitted = _filter_read_blocked_search_results(result, task_id)
if hasattr(result, 'matches'): if hasattr(result, 'matches'):
@@ -2912,6 +2914,7 @@ SEARCH_FILES_SCHEMA = {
"parameters": { "parameters": {
"type": "object", "type": "object",
"properties": { "properties": {
"allow_broad": {"type": "boolean", "default": False, "description": "Deliberately search a home, drive or multi-project umbrella. Default scopes to the current project or asks for a scope. Does not remove pruning or CPU limits."},
"pattern": {"type": "string", "description": "Regex pattern for content search, or glob pattern (e.g., '*.py') for file search"}, "pattern": {"type": "string", "description": "Regex pattern for content search, or glob pattern (e.g., '*.py') for file search"},
"target": {"type": "string", "enum": ["content", "files"], "description": "'content' searches inside file contents, 'files' searches for files by name", "default": "content"}, "target": {"type": "string", "enum": ["content", "files"], "description": "'content' searches inside file contents, 'files' searches for files by name", "default": "content"},
"path": {"type": "string", "description": "Directory or file to search in (default: current working directory)", "default": "."}, "path": {"type": "string", "description": "Directory or file to search in (default: current working directory)", "default": "."},
@@ -2979,7 +2982,8 @@ def _handle_search_files(args, **kw):
pattern=args.get("pattern", ""), target=target, path=args.get("path", "."), pattern=args.get("pattern", ""), target=target, path=args.get("path", "."),
file_glob=args.get("file_glob"), limit=args.get("limit", 50), offset=args.get("offset", 0), file_glob=args.get("file_glob"), limit=args.get("limit", 50), offset=args.get("offset", 0),
output_mode=args.get("output_mode", "content"), context=args.get("context", 0), output_mode=args.get("output_mode", "content"), context=args.get("context", 0),
order=args.get("order", "discovery"), task_id=tid) order=args.get("order", "discovery"), task_id=tid,
allow_broad=args.get("allow_broad") is True)
def _read_file_schema_overrides(): def _read_file_schema_overrides():