feat: make QuickJS the default plugin engine everywhere (#2775)

Benchmark (test-target, both engines), QuickJS default on all platforms
with CODEXBAR_PLUGIN_ENGINE=jsc rollback (env + Debug pane), dedicated
4 MiB worker threads with true-interrupt watchdog, native stack headroom
for the overflow guard, CI crash-report collection, and the overflow
regression pinned to production stack geometry.
This commit is contained in:
Peter Steinberger
2026-08-08 14:16:08 -07:00
committed by GitHub
parent af9bc9382f
commit f95f252103
35 changed files with 783 additions and 154 deletions
+17
View File
@@ -171,6 +171,23 @@ jobs:
if: ${{ matrix.shard-index == 0 }}
run: ./Scripts/test-plugin-engines.sh
- name: Collect crash reports on failure
if: ${{ failure() }}
shell: bash
run: |
set -uo pipefail
reports="$HOME/Library/Logs/DiagnosticReports"
found=0
if [[ -d "$reports" ]]; then
while IFS= read -r file; do
found=1
echo "::group::crash report $(basename "$file")"
cat "$file"
echo "::endgroup::"
done < <(find "$reports" \( -name '*.ips' -o -name '*.crash' \) -newer .git/HEAD -print 2>/dev/null | head -5)
fi
[[ "$found" == 1 ]] || echo "no fresh crash reports under $reports"
- name: Summarize macOS shard
if: ${{ always() }}
shell: bash
+3
View File
@@ -10,6 +10,8 @@
- Codex: price persisted usage from token classes when reports are read, so a cold rebuild racing the models.dev catalog can no longer permanently bake fallback rates into SQLite (#2772).
- OpenRouter: keep optional key-quota enrichment on its one-second production fast join while making degraded results explicit and preventing loaded CI parity runs from mistaking the fallback snapshot for a golden mismatch (fixes #2778).
- Codex: the SQLite cost store now writes each save cycle inside one transaction, so a crash or kill mid-save can never leave session rows updated against stale day aggregates — the previous state survives intact, matching the old JSON path's atomic file replace (refs #2760).
- Provider plugins: run each QuickJS context on a dedicated 4 MiB-stack thread, refresh stack bounds at every JavaScript entry, and leave 3 MiB of native headroom so deep recursion raises a clean stack-overflow error instead of crashing.
>>>>>>> bae013ccb (feat: make QuickJS the default plugin engine everywhere)
- Codex: SQLite cost saves no longer rescan every stored row and snapshot per file — baseline counts and file lookups are precomputed once, cutting a large-corpus (1,700+ sessions) save pass from minutes of CPU to seconds (refs #2760).
- Codex: restore JSON-cache retention semantics lost in the SQLite cutover — discovery pruning now reaches the scanner's round-tripped payload so deleted files stop resurfacing, the row budget never sacrifices in-window or recently active sessions, and fork-parent protection again drops stale lineage-only parents (refs #2760).
- Codex: the SQLite cost store no longer deletes the whole database on transient failures — lock contention from a concurrent CLI/app writer, disk-full, or a constraint violation now preserve history and only genuine corruption or schema drift triggers a rebuild, which is now logged (refs #2760).
@@ -26,6 +28,7 @@
- Codex: cost history now lives in a single SQLite store — bounded memory at any corpus size, append-linear catch-up, and no more multi-hundred-MB JSON decode on refresh (#2760). Thanks @xx205 for the accumulator design!
- CLI: dashboard snapshot identity now defaults to full; use `--identity redacted` to restore redacted emails.
- Provider plugins: run the same bundled JavaScript providers and local plugin CLI on Linux through a sandboxed QuickJS engine, removing the cut-over providers' Linux-only Swift twins.
- Provider plugins: use QuickJS on every platform with byte-identical output and hard interruption for hung scripts; Apple builds retain JavaScriptCore as an explicit rollback engine.
### Fixed
- CLI: claude-swap accounts with expired or missing credentials now keep their email on the serve dashboard instead of showing a bare slot number.
+3 -3
View File
@@ -6,8 +6,8 @@ cd "$ROOT_DIR"
FILTER='ProviderPluginRuntimeTests|ProviderPluginParityTests|ProviderPluginDetailsParityTests|ProviderPluginExtensionParityTests|Sub2APIPluginGoldenTests|UserProviderPluginPortableTests'
echo "plugin engine A/B: JavaScriptCore"
echo "plugin engine A/B: QuickJS default"
env -u CODEXBAR_PLUGIN_ENGINE swift test --filter "$FILTER"
echo "plugin engine A/B: QuickJS"
CODEXBAR_PLUGIN_ENGINE=quickjs swift test --skip-build --filter "$FILTER"
echo "plugin engine A/B: JavaScriptCore rollback"
CODEXBAR_PLUGIN_ENGINE=jsc swift test --skip-build --filter "$FILTER"
@@ -7,6 +7,8 @@ struct DebugPane: View {
@Bindable var settings: SettingsStore
@Bindable var store: UsageStore
@AppStorage("debugFileLoggingEnabled") private var debugFileLoggingEnabled = false
@AppStorage(ProviderPluginRuntime.javaScriptCoreRollbackDefaultsKey)
private var useJavaScriptCorePluginEngine = false
// Provider-specific by design: debug probe, fetch, and error pickers historically start on Codex.
@State private var currentLogProvider: UsageProvider = .codex
@State private var currentFetchProvider: UsageProvider = .codex
@@ -73,6 +75,13 @@ struct DebugPane: View {
binding: self.$store.debugForceAnimation)
}
SettingsSection(title: L("Provider Plugins")) {
PreferenceToggleRow(
title: L("use_javascriptcore_plugin_engine"),
subtitle: L("use_javascriptcore_plugin_engine_subtitle"),
binding: self.$useJavaScriptCorePluginEngine)
}
SettingsSection(
title: L("section_loading_animations"),
caption: L("loading_animations_caption"))
@@ -672,6 +672,8 @@
"copyright" = "© 2026 بيتر ستاينبرجر. MIT الرخصة.";
/* Debug Pane */
"use_javascriptcore_plugin_engine" = "استخدام JavaScriptCore لإضافات المزوّد";
"use_javascriptcore_plugin_engine_subtitle" = "للرجوع المتوافق فقط. أعد تشغيل CodexBar للتطبيق؛ يتجاوز CODEXBAR_PLUGIN_ENGINE هذا الإعداد.";
"section_logging" = "قطع الأشجار";
"enable_file_logging" = "تمكين تسجيل الملفات";
"enable_file_logging_subtitle" = "اكتب السجلات إلى %@ للتصحيح.";
@@ -650,6 +650,8 @@
"copyright" = "© 2026 Peter Steinberger. Llicència MIT.";
/* Debug Pane */
"use_javascriptcore_plugin_engine" = "Utilitza JavaScriptCore per als connectors de proveïdor";
"use_javascriptcore_plugin_engine_subtitle" = "Només com a reversió de compatibilitat. Reinicieu CodexBar per aplicar-ho; CODEXBAR_PLUGIN_ENGINE substitueix aquest ajust.";
"section_logging" = "Registre";
"enable_file_logging" = "Activeu el registre en fitxer";
"enable_file_logging_subtitle" = "Escriu els registres a %@ per a la depuració.";
@@ -664,6 +664,8 @@
"copyright" = "© 2026 Peter Steinberger. MIT-Lizenz.";
/* Debug Pane */
"use_javascriptcore_plugin_engine" = "JavaScriptCore für Anbieter-Plugins verwenden";
"use_javascriptcore_plugin_engine_subtitle" = "Nur als Kompatibilitäts-Rollback. CodexBar zum Anwenden neu starten; CODEXBAR_PLUGIN_ENGINE überschreibt diese Einstellung.";
"section_logging" = "Protokollierung";
"enable_file_logging" = "Aktivieren Sie die Dateiprotokollierung";
"enable_file_logging_subtitle" = "Schreiben Sie Protokolle zum Debuggen in %@.";
@@ -649,6 +649,8 @@
"copyright" = "© 2026 Peter Steinberger. MIT License.";
/* Debug Pane */
"use_javascriptcore_plugin_engine" = "Use JavaScriptCore for provider plugins";
"use_javascriptcore_plugin_engine_subtitle" = "Compatibility rollback only. Restart CodexBar to apply; CODEXBAR_PLUGIN_ENGINE overrides this setting.";
"section_logging" = "Logging";
"enable_file_logging" = "Enable file logging";
"enable_file_logging_subtitle" = "Write logs to %@ for debugging.";
@@ -659,6 +659,8 @@
"copyright" = "© 2026 Peter Steinberger. Licencia MIT.";
/* Debug Pane */
"use_javascriptcore_plugin_engine" = "Usar JavaScriptCore para los complementos de proveedores";
"use_javascriptcore_plugin_engine_subtitle" = "Solo para reversión de compatibilidad. Reinicia CodexBar para aplicarlo; CODEXBAR_PLUGIN_ENGINE anula este ajuste.";
"section_logging" = "Registro";
"enable_file_logging" = "Activar registro en archivo";
"enable_file_logging_subtitle" = "Escribir registros en %@ para depuración.";
@@ -672,6 +672,8 @@
"copyright" = "© ۲۰۲۶ پیتر استاینبرگر. MIT مجوز.";
/* Debug Pane */
"use_javascriptcore_plugin_engine" = "استفاده از JavaScriptCore برای افزونه‌های ارائه‌دهنده";
"use_javascriptcore_plugin_engine_subtitle" = "فقط برای بازگشت سازگاری. برای اعمال CodexBar را بازراه‌اندازی کنید؛ CODEXBAR_PLUGIN_ENGINE این تنظیم را لغو می‌کند.";
"section_logging" = "چوب بری";
"enable_file_logging" = "فعال سازی ثبت فایل";
"enable_file_logging_subtitle" = "لاگ ها را برای %@ برای اشکال زدایی بنویسید.";
@@ -666,6 +666,8 @@
"copyright" = "© 2026 Peter Steinberger. Licence MIT.";
/* Debug Pane */
"use_javascriptcore_plugin_engine" = "Utiliser JavaScriptCore pour les extensions de fournisseurs";
"use_javascriptcore_plugin_engine_subtitle" = "Repli de compatibilité uniquement. Redémarrez CodexBar pour lappliquer ; CODEXBAR_PLUGIN_ENGINE remplace ce réglage.";
"section_logging" = "Journalisation";
"enable_file_logging" = "Activer la journalisation des fichiers";
"enable_file_logging_subtitle" = "Écrivez les journaux dans %@ pour le débogage.";
@@ -645,6 +645,8 @@
"copyright" = "© 2026 Peter Steinberger. Licenza MIT.";
/* Debug Pane */
"use_javascriptcore_plugin_engine" = "Usar JavaScriptCore para os complementos de provedores";
"use_javascriptcore_plugin_engine_subtitle" = "Só como reversión de compatibilidade. Reinicia CodexBar para aplicalo; CODEXBAR_PLUGIN_ENGINE substitúe este axuste.";
"section_logging" = "Rexistro";
"enable_file_logging" = "Activar o rexistro en ficheiro";
"enable_file_logging_subtitle" = "Escribe os rexistros en %@ para depuración.";
@@ -674,6 +674,8 @@
"copyright" = "© 2026 Peter Steinberger. Lisensi MIT.";
/* Debug Pane */
"use_javascriptcore_plugin_engine" = "Gunakan JavaScriptCore untuk plugin penyedia";
"use_javascriptcore_plugin_engine_subtitle" = "Hanya untuk rollback kompatibilitas. Mulai ulang CodexBar untuk menerapkan; CODEXBAR_PLUGIN_ENGINE mengganti pengaturan ini.";
"section_logging" = "Pencatatan";
"enable_file_logging" = "Aktifkan pencatatan file";
"enable_file_logging_subtitle" = "Tulis log ke %@ untuk debugging.";
@@ -674,6 +674,8 @@
"copyright" = "© 2026 Peter Steinberger. Licenza MIT.";
/* Debug Pane */
"use_javascriptcore_plugin_engine" = "Usa JavaScriptCore per i plugin dei provider";
"use_javascriptcore_plugin_engine_subtitle" = "Solo ripristino di compatibilità. Riavvia CodexBar per applicarlo; CODEXBAR_PLUGIN_ENGINE sostituisce questa impostazione.";
"section_logging" = "Log";
"enable_file_logging" = "Abilita log su file";
"enable_file_logging_subtitle" = "Scrive i log in %@ per il debug.";
@@ -663,6 +663,8 @@
"copyright" = "© 2026 Peter Steinberger. MIT License.";
/* Debug Pane */
"use_javascriptcore_plugin_engine" = "プロバイダープラグインにJavaScriptCoreを使用";
"use_javascriptcore_plugin_engine_subtitle" = "互換性のためのロールバック専用です。適用するにはCodexBarを再起動してください。CODEXBAR_PLUGIN_ENGINEがこの設定より優先されます。";
"section_logging" = "ログ";
"enable_file_logging" = "ファイルログを有効にする";
"enable_file_logging_subtitle" = "デバッグ用に %@ へログを書き込みます。";
@@ -649,6 +649,8 @@
"check_for_updates" = "업데이트 확인…";
"updates_unavailable" = "이 빌드에서는 업데이트를 사용할 수 없습니다.";
"copyright" = "© 2026 Peter Steinberger. MIT License.";
"use_javascriptcore_plugin_engine" = "제공자 플러그인에 JavaScriptCore 사용";
"use_javascriptcore_plugin_engine_subtitle" = "호환성 롤백 전용입니다. 적용하려면 CodexBar를 재시작하세요. CODEXBAR_PLUGIN_ENGINE이 이 설정보다 우선합니다.";
"section_logging" = "로깅";
"enable_file_logging" = "파일 로깅 사용";
"enable_file_logging_subtitle" = "디버깅을 위해 %@에 로그를 기록합니다.";
@@ -666,6 +666,8 @@
"copyright" = "© 2026 Peter Steinberger. MIT-licentie.";
/* Debug Pane */
"use_javascriptcore_plugin_engine" = "JavaScriptCore gebruiken voor providerplug-ins";
"use_javascriptcore_plugin_engine_subtitle" = "Alleen als compatibiliteitsterugval. Start CodexBar opnieuw; CODEXBAR_PLUGIN_ENGINE overschrijft deze instelling.";
"section_logging" = "Loggen";
"enable_file_logging" = "Bestandsregistratie inschakelen";
"enable_file_logging_subtitle" = "Schrijf logboeken naar %@ voor foutopsporing.";
@@ -674,6 +674,8 @@
"copyright" = "© 2026 Peter Steinberger. Licencja MIT.";
/* Debug Pane */
"use_javascriptcore_plugin_engine" = "Używaj JavaScriptCore dla wtyczek dostawców";
"use_javascriptcore_plugin_engine_subtitle" = "Tylko jako awaryjny tryb zgodności. Uruchom ponownie CodexBar; CODEXBAR_PLUGIN_ENGINE zastępuje to ustawienie.";
"section_logging" = "Logowanie";
"enable_file_logging" = "Włącz logowanie do pliku";
"enable_file_logging_subtitle" = "Zapisuj logi do %@ na potrzeby debugowania.";
@@ -663,6 +663,8 @@
"copyright" = "© 2026 Peter Steinberger. Licença MIT.";
/* Debug Pane */
"use_javascriptcore_plugin_engine" = "Usar JavaScriptCore para plugins de provedores";
"use_javascriptcore_plugin_engine_subtitle" = "Apenas reversão de compatibilidade. Reinicie o CodexBar para aplicar; CODEXBAR_PLUGIN_ENGINE substitui este ajuste.";
"section_logging" = "Logs";
"enable_file_logging" = "Ativar logs em arquivo";
"enable_file_logging_subtitle" = "Grava logs em %@ para depuração.";
@@ -667,6 +667,8 @@
"copyright" = "© 2026 Peter Steinberger. Лицензия MIT.";
/* Debug Pane */
"use_javascriptcore_plugin_engine" = "Использовать JavaScriptCore для плагинов провайдеров";
"use_javascriptcore_plugin_engine_subtitle" = "Только для отката совместимости. Перезапустите CodexBar; CODEXBAR_PLUGIN_ENGINE переопределяет эту настройку.";
"section_logging" = "Журналирование";
"enable_file_logging" = "Включить запись логов";
"enable_file_logging_subtitle" = "Записывать логи в %@ для отладки.";
@@ -665,6 +665,8 @@
"copyright" = "© 2026 Peter Steinberger. MIT-licens.";
/* Debug Pane */
"use_javascriptcore_plugin_engine" = "Använd JavaScriptCore för leverantörsinsticksfiler";
"use_javascriptcore_plugin_engine_subtitle" = "Endast kompatibilitetsåterställning. Starta om CodexBar för att tillämpa; CODEXBAR_PLUGIN_ENGINE åsidosätter inställningen.";
"section_logging" = "Loggning";
"enable_file_logging" = "Aktivera filloggning";
"enable_file_logging_subtitle" = "Skriv loggar till %@ för felsökning.";
@@ -672,6 +672,8 @@
"copyright" = "© 2026 ปีเตอร์ สไตน์เบอร์เกอร์ ใบอนุญาต MIT";
/* Debug Pane */
"use_javascriptcore_plugin_engine" = "ใช้ JavaScriptCore สำหรับปลั๊กอินผู้ให้บริการ";
"use_javascriptcore_plugin_engine_subtitle" = "ใช้เพื่อย้อนกลับด้านความเข้ากันได้เท่านั้น เริ่ม CodexBar ใหม่เพื่อใช้ค่า; CODEXBAR_PLUGIN_ENGINE จะแทนที่การตั้งค่านี้";
"section_logging" = "การบันทึก";
"enable_file_logging" = "เปิดใช้งานการบันทึกไฟล์";
"enable_file_logging_subtitle" = "เขียนบันทึกไปยัง %@ เพื่อแก้ไขข้อบกพร่อง";
@@ -672,6 +672,8 @@
"copyright" = "© 2026 Peter Steinberger. MIT Lisansı.";
/* Debug Pane */
"use_javascriptcore_plugin_engine" = "Sağlayıcı eklentileri için JavaScriptCore kullan";
"use_javascriptcore_plugin_engine_subtitle" = "Yalnızca uyumluluk geri dönüşü. Uygulamak için CodexBar’ı yeniden başlatın; CODEXBAR_PLUGIN_ENGINE bu ayarı geçersiz kılar.";
"section_logging" = "Günlükleme";
"enable_file_logging" = "Dosya günlüğünü etkinleştir";
"enable_file_logging_subtitle" = "Hata ayıklama için günlükleri %@ konumuna yaz.";
@@ -666,6 +666,8 @@
"copyright" = "© 2026 Пітер Штайнбергер. Ліцензія MIT.";
/* Debug Pane */
"use_javascriptcore_plugin_engine" = "Використовувати JavaScriptCore для плагінів постачальників";
"use_javascriptcore_plugin_engine_subtitle" = "Лише для відкату сумісності. Перезапустіть CodexBar; CODEXBAR_PLUGIN_ENGINE перевизначає це налаштування.";
"section_logging" = "Лісозаготівля";
"enable_file_logging" = "Увімкнути журналювання файлів";
"enable_file_logging_subtitle" = "Записати журнали до %@ для налагодження.";
@@ -662,6 +662,8 @@
"copyright" = "© 2026 Peter Steinberger. Giấy phép MIT.";
/* Debug Pane */
"use_javascriptcore_plugin_engine" = "Dùng JavaScriptCore cho phần bổ trợ nhà cung cấp";
"use_javascriptcore_plugin_engine_subtitle" = "Chỉ dùng để quay lui tương thích. Khởi động lại CodexBar để áp dụng; CODEXBAR_PLUGIN_ENGINE ghi đè cài đặt này.";
"section_logging" = "Ghi nhật ký";
"enable_file_logging" = "Cho phép ghi nhật ký tệp";
"enable_file_logging_subtitle" = "Ghi nhật ký vào %@ để gỡ lỗi.";
@@ -639,6 +639,8 @@
"check_for_updates" = "检查更新…";
"updates_unavailable" = "此构建中更新不可用。";
"copyright" = "© 2026 Peter Steinberger。MIT 许可证。";
"use_javascriptcore_plugin_engine" = "为提供商插件使用 JavaScriptCore";
"use_javascriptcore_plugin_engine_subtitle" = "仅用于兼容性回退。重启 CodexBar 后生效;CODEXBAR_PLUGIN_ENGINE 会覆盖此设置。";
"section_logging" = "日志";
"enable_file_logging" = "启用文件日志";
"enable_file_logging_subtitle" = "将日志写入 %@ 以进行调试。";
@@ -660,6 +660,8 @@
"check_for_updates" = "檢查更新…";
"updates_unavailable" = "此建置無法使用更新功能。";
"copyright" = "© 2026 Peter Steinberger。MIT 許可證。";
"use_javascriptcore_plugin_engine" = "為供應商外掛使用 JavaScriptCore";
"use_javascriptcore_plugin_engine_subtitle" = "僅用於相容性回復。重新啟動 CodexBar 後生效;CODEXBAR_PLUGIN_ENGINE 會覆寫此設定。";
"section_logging" = "記錄";
"enable_file_logging" = "啟用檔案記錄";
"enable_file_logging_subtitle" = "將記錄寫入 %@ 以進行除錯。";
@@ -13,6 +13,7 @@ public final class ProviderPluginRuntime: @unchecked Sendable {
public static let defaultTimeout: TimeInterval = 20
public static let maximumResponseBytes = 5 * 1024 * 1024
public static let engineEnvironmentKey = "CODEXBAR_PLUGIN_ENGINE"
public static let javaScriptCoreRollbackDefaultsKey = "debugUseJavaScriptCorePluginEngine"
public let manifest: ProviderPluginManifest
@@ -230,16 +231,30 @@ public final class ProviderPluginRuntime: @unchecked Sendable {
self.lock.unlock()
}
private static func resolveEngineKind(_ requested: ProviderPluginEngineKind) -> ProviderPluginEngineKind {
static func resolveEngineKind(_ requested: ProviderPluginEngineKind) -> ProviderPluginEngineKind {
self.resolveEngineKind(
requested,
environment: ProcessInfo.processInfo.environment,
useJavaScriptCoreRollback: UserDefaults.standard.bool(forKey: self.javaScriptCoreRollbackDefaultsKey))
}
static func resolveEngineKind(
_ requested: ProviderPluginEngineKind,
environment: [String: String],
useJavaScriptCoreRollback: Bool) -> ProviderPluginEngineKind
{
guard requested == .automatic else { return requested }
if ProcessInfo.processInfo.environment[self.engineEnvironmentKey]?.lowercased() == "quickjs" {
return .quickJS
}
#if canImport(JavaScriptCore)
return .javaScriptCore
#else
return .quickJS
switch environment[self.engineEnvironmentKey]?.lowercased() {
case "jsc": return .javaScriptCore
case "quickjs": return .quickJS
default:
if useJavaScriptCoreRollback {
return .javaScriptCore
}
}
#endif
return .quickJS
}
private func redactedError(_ error: Error, secrets: Dictionary<String, String>.Values) -> Error {
@@ -18,6 +18,11 @@ private enum QuickJSHostFunction: Int32 {
case amountFromPercent
}
enum QuickJSRuntimeLimits {
/// Leave ample native-stack headroom for Swift entry frames and QuickJS's stack-overflow error construction.
static let nativeStackSizeBytes = 4 * 1024 * 1024
}
private func quickJSHostCallback(
_ opaque: UnsafeMutableRawPointer?,
_ context: OpaquePointer?,
@@ -137,82 +142,10 @@ private final class QuickJSPluginValue: ProviderPluginValue {
final class QuickJSProviderPluginEngine: ProviderPluginEngine, @unchecked Sendable {
static let memoryLimitBytes = 64 * 1024 * 1024
static let stackLimitBytes = 2 * 1024 * 1024
static let stackLimitBytes = 1 * 1024 * 1024
static func transpileTypeScript(source: String, sucraseSource: String) throws -> String {
guard let runtime = JS_NewRuntime() else {
throw ProviderPluginError.load("QuickJS could not create a TypeScript transpiler runtime")
}
JS_SetMemoryLimit(runtime, self.memoryLimitBytes)
JS_SetMaxStackSize(runtime, self.stackLimitBytes)
guard let context = JS_NewContext(runtime) else {
JS_FreeRuntime(runtime)
throw ProviderPluginError.load("QuickJS could not create a TypeScript transpiler context")
}
JS_UpdateStackTop(runtime)
let watchdog = cqjs_watchdog_create(nil, nil)
if let watchdog {
cqjs_watchdog_install(watchdog, runtime, context)
cqjs_watchdog_arm(watchdog, UInt64(ProviderPluginRuntime.defaultTimeout * 1000))
}
defer {
if let watchdog {
cqjs_watchdog_disarm(watchdog)
cqjs_watchdog_destroy(watchdog)
}
JS_FreeContext(context)
JS_FreeRuntime(runtime)
}
func exceptionMessage() -> String {
let exception = JS_GetException(context)
defer { cqjs_free_value(context, exception) }
var length = 0
guard let pointer = JS_ToCStringLen2(context, &length, exception, false) else { return "unknown error" }
defer { JS_FreeCString(context, pointer) }
let bytes = UnsafeRawPointer(pointer).assumingMemoryBound(to: UInt8.self)
return String(bytes: UnsafeBufferPointer(start: bytes, count: length), encoding: .utf8) ?? "unknown error"
}
func evaluate(_ script: String, filename: String) throws -> JSValue {
let value = script.utf8CString.withUnsafeBufferPointer { scriptBuffer in
filename.withCString { filenamePointer in
JS_Eval(
context,
scriptBuffer.baseAddress,
scriptBuffer.count - 1,
filenamePointer,
JS_EVAL_TYPE_GLOBAL)
}
}
guard !cqjs_is_exception(value) else {
throw ProviderPluginError.load("TypeScript transpilation failed: \(exceptionMessage())")
}
return value
}
let sucrase = try evaluate(sucraseSource, filename: "sucrase.js")
cqjs_free_value(context, sucrase)
let global = JS_GetGlobalObject(context)
defer { cqjs_free_value(context, global) }
let sourceValue = source.utf8CString.withUnsafeBufferPointer { buffer in
JS_NewStringLen(context, buffer.baseAddress, buffer.count - 1)
}
_ = JS_SetPropertyStr(context, global, "__codexbarTypeScriptSource", sourceValue)
let result = try evaluate(
"sucrase.transform(__codexbarTypeScriptSource, {transforms:['typescript']}).code",
filename: "<sucrase-transform>")
defer { cqjs_free_value(context, result) }
var length = 0
guard let pointer = JS_ToCStringLen2(context, &length, result, false) else {
throw ProviderPluginError.load("TypeScript transpilation returned no output")
}
defer { JS_FreeCString(context, pointer) }
let bytes = UnsafeRawPointer(pointer).assumingMemoryBound(to: UInt8.self)
guard let output = String(bytes: UnsafeBufferPointer(start: bytes, count: length), encoding: .utf8),
!output.isEmpty
else { throw ProviderPluginError.load("TypeScript transpilation returned no output") }
return output
try QuickJSTypeScriptTranspiler.transpile(source: source, sucraseSource: sucraseSource)
}
private struct FetchState {
@@ -229,7 +162,9 @@ final class QuickJSProviderPluginEngine: ProviderPluginEngine, @unchecked Sendab
let expiresAt: Date
}
private let queue: DispatchQueue
// @unchecked Sendable is safe because every mutable engine field and QuickJS API call is confined
// to this serial worker. requestInterrupt() is the watchdog's explicitly thread-safe escape hatch.
private let worker: QuickJSSerialWorker
private let runtime: OpaquePointer
fileprivate let context: OpaquePointer
private let transport: any ProviderHTTPTransport
@@ -259,34 +194,37 @@ final class QuickJSProviderPluginEngine: ProviderPluginEngine, @unchecked Sendab
timeout: TimeInterval,
responseSizeLimit: Int,
rejectsNonSuccessResponses: Bool,
allowsDynamicID: Bool) throws -> QuickJSProviderPluginEngine
allowsDynamicID: Bool,
workerStackSizeBytes: Int = QuickJSRuntimeLimits.nativeStackSizeBytes) throws -> QuickJSProviderPluginEngine
{
guard let runtime = JS_NewRuntime() else {
throw ProviderPluginError.load("QuickJS could not create a runtime")
}
JS_SetMemoryLimit(runtime, Self.memoryLimitBytes)
JS_SetMaxStackSize(runtime, Self.stackLimitBytes)
guard let context = JS_NewContext(runtime) else {
JS_FreeRuntime(runtime)
throw ProviderPluginError.load("QuickJS could not create a context")
}
let queue = DispatchQueue(label: "com.steipete.codexbar.provider-plugin.quickjs.\(UUID().uuidString)")
let engine = QuickJSProviderPluginEngine(
queue: queue,
runtime: runtime,
context: context,
transport: transport,
timeout: timeout,
responseSizeLimit: responseSizeLimit,
rejectsNonSuccessResponses: rejectsNonSuccessResponses)
return try queue.sync {
let worker = QuickJSSerialWorker(
name: "CodexBar QuickJS provider plugin",
stackSizeBytes: workerStackSizeBytes)
return try worker.sync {
guard let runtime = JS_NewRuntime() else {
throw ProviderPluginError.load("QuickJS could not create a runtime")
}
JS_SetMemoryLimit(runtime, Self.memoryLimitBytes)
JS_SetMaxStackSize(runtime, Self.stackLimitBytes)
guard let context = JS_NewContext(runtime) else {
JS_FreeRuntime(runtime)
throw ProviderPluginError.load("QuickJS could not create a context")
}
let engine = QuickJSProviderPluginEngine(
worker: worker,
runtime: runtime,
context: context,
transport: transport,
timeout: timeout,
responseSizeLimit: responseSizeLimit,
rejectsNonSuccessResponses: rejectsNonSuccessResponses)
try engine.load(source: source, preludeSource: preludeSource, allowsDynamicID: allowsDynamicID)
return engine
}
}
private init(
queue: DispatchQueue,
worker: QuickJSSerialWorker,
runtime: OpaquePointer,
context: OpaquePointer,
transport: any ProviderHTTPTransport,
@@ -294,7 +232,7 @@ final class QuickJSProviderPluginEngine: ProviderPluginEngine, @unchecked Sendab
responseSizeLimit: Int,
rejectsNonSuccessResponses: Bool)
{
self.queue = queue
self.worker = worker
self.runtime = runtime
self.context = context
self.transport = transport
@@ -304,21 +242,38 @@ final class QuickJSProviderPluginEngine: ProviderPluginEngine, @unchecked Sendab
}
deinit {
if let definition = self.definition {
cqjs_free_value(self.context, definition)
let runtime = self.runtime
let context = self.context
let definition = self.definition
let applyPrelude = self.applyPrelude
let fetchUsage = self.fetchUsage
let watchdog = self.watchdog
let teardown = {
if let definition {
cqjs_free_value(context, definition)
}
if let applyPrelude {
cqjs_free_value(context, applyPrelude)
}
if let fetchUsage {
cqjs_free_value(context, fetchUsage)
}
if let watchdog {
cqjs_watchdog_disarm(watchdog)
}
// The runtime retains the interrupt-handler opaque pointer until it is freed.
JS_FreeContext(context)
JS_FreeRuntime(runtime)
if let watchdog {
cqjs_watchdog_destroy(watchdog)
}
}
if let applyPrelude = self.applyPrelude {
cqjs_free_value(self.context, applyPrelude)
if self.worker.isCurrentThread {
teardown()
} else {
try? self.worker.sync(teardown)
}
if let fetchUsage = self.fetchUsage {
cqjs_free_value(self.context, fetchUsage)
}
if let watchdog = self.watchdog {
cqjs_watchdog_disarm(watchdog)
cqjs_watchdog_destroy(watchdog)
}
JS_FreeContext(self.context)
JS_FreeRuntime(self.runtime)
self.worker.shutdown()
}
// swiftlint:disable:next function_parameter_count
@@ -332,9 +287,9 @@ final class QuickJSProviderPluginEngine: ProviderPluginEngine, @unchecked Sendab
instanceCookieResolver: ProviderPluginRuntime.InstanceCookieResolver?,
completion: @escaping @Sendable (Result<UsageSnapshot, Error>) -> Void)
{
self.queue.async {
self.worker.async {
completion(Result {
try self.fetchOnQueue(
try self.fetchOnWorker(
settings: settings,
secrets: secrets,
now: now,
@@ -347,7 +302,7 @@ final class QuickJSProviderPluginEngine: ProviderPluginEngine, @unchecked Sendab
}
func globalType(of name: String) throws -> String {
try self.queue.sync {
try self.worker.sync {
JS_UpdateStackTop(self.runtime)
let escaped = name.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "'", with: "\\'")
@@ -404,7 +359,7 @@ final class QuickJSProviderPluginEngine: ProviderPluginEngine, @unchecked Sendab
}
// swiftlint:disable:next function_parameter_count
private func fetchOnQueue(
private func fetchOnWorker(
settings: [String: String],
secrets: [String: String],
now: Date,
@@ -458,6 +413,7 @@ final class QuickJSProviderPluginEngine: ProviderPluginEngine, @unchecked Sendab
if JS_IsPromise(result) {
while JS_PromiseState(self.context, result) == JS_PROMISE_PENDING {
var pendingContext: OpaquePointer?
JS_UpdateStackTop(self.runtime)
let executed = JS_ExecutePendingJob(self.runtime, &pendingContext)
if executed < 0 {
cqjs_free_value(self.context, result)
@@ -863,6 +819,7 @@ final class QuickJSProviderPluginEngine: ProviderPluginEngine, @unchecked Sendab
}
private func call(_ function: JSValue, arguments: [JSValue]) throws -> JSValue {
JS_UpdateStackTop(self.runtime)
var mutableArguments = arguments
let result = mutableArguments.withUnsafeMutableBufferPointer { buffer in
JS_Call(self.context, function, cqjs_undefined(), Int32(buffer.count), buffer.baseAddress)
@@ -872,6 +829,7 @@ final class QuickJSProviderPluginEngine: ProviderPluginEngine, @unchecked Sendab
}
private func evaluate(_ source: String, filename: String) throws -> JSValue {
JS_UpdateStackTop(self.runtime)
let result = source.utf8CString.withUnsafeBufferPointer { sourceBuffer in
filename.withCString { filenamePointer in
JS_Eval(
@@ -987,7 +945,106 @@ final class QuickJSProviderPluginEngine: ProviderPluginEngine, @unchecked Sendab
}
}
private final class QuickJSBlockingResult<Value: Sendable>: @unchecked Sendable {
private final class QuickJSSerialWorker: @unchecked Sendable {
typealias Job = () -> Void
private final class State: @unchecked Sendable {
private let condition = NSCondition()
private var jobs: [Job] = []
private var acceptsJobs = true
private var stopped = false
func enqueue(_ job: @escaping Job) -> Bool {
self.condition.lock()
defer { self.condition.unlock() }
guard self.acceptsJobs else { return false }
self.jobs.append(job)
self.condition.signal()
return true
}
func next() -> Job? {
self.condition.lock()
defer { self.condition.unlock() }
while self.jobs.isEmpty, self.acceptsJobs {
self.condition.wait()
}
guard !self.jobs.isEmpty else { return nil }
return self.jobs.removeFirst()
}
func beginShutdown() {
self.condition.lock()
self.acceptsJobs = false
self.condition.broadcast()
self.condition.unlock()
}
func markStopped() {
self.condition.lock()
self.stopped = true
self.condition.broadcast()
self.condition.unlock()
}
func waitUntilStopped() {
self.condition.lock()
while !self.stopped {
self.condition.wait()
}
self.condition.unlock()
}
}
private let state: State
private let thread: Thread
init(name: String, stackSizeBytes: Int) {
let state = State()
self.state = state
self.thread = Thread {
defer { state.markStopped() }
while let job = state.next() {
job()
}
}
self.thread.name = name
self.thread.stackSize = stackSizeBytes
self.thread.start()
}
deinit {
self.shutdown()
}
var isCurrentThread: Bool {
Thread.current === self.thread
}
func async(_ operation: @escaping Job) {
precondition(self.state.enqueue(operation), "QuickJS worker accepted work after shutdown")
}
func sync<Value: Sendable>(_ operation: @escaping () throws -> Value) throws -> Value {
if self.isCurrentThread {
return try operation()
}
let box = QuickJSBlockingResult<Value>()
precondition(self.state.enqueue {
box.finish(Result { try operation() })
}, "QuickJS worker accepted work after shutdown")
return try box.wait().get()
}
func shutdown() {
self.state.beginShutdown()
if !self.isCurrentThread {
self.state.waitUntilStopped()
}
}
}
final class QuickJSBlockingResult<Value: Sendable>: @unchecked Sendable {
private let condition = NSCondition()
private var result: Result<Value, Error>?
@@ -1006,6 +1063,18 @@ private final class QuickJSBlockingResult<Value: Sendable>: @unchecked Sendable
}
return self.result
}
func wait() -> Result<Value, Error> {
self.condition.lock()
defer { self.condition.unlock() }
while self.result == nil {
self.condition.wait()
}
guard let result = self.result else {
preconditionFailure("QuickJS blocking result signaled without a value")
}
return result
}
}
private final class QuickJSRedactionValues: @unchecked Sendable {
@@ -0,0 +1,103 @@
import CQuickJS
import Foundation
enum QuickJSTypeScriptTranspiler {
static func transpile(source: String, sucraseSource: String) throws -> String {
let box = QuickJSBlockingResult<String>()
let thread = Thread {
box.finish(Result {
try self.transpileOnCurrentThread(source: source, sucraseSource: sucraseSource)
})
}
// Dispatch/Swift cooperative workers can have less native stack than QuickJS's 2 MiB limit.
thread.stackSize = QuickJSRuntimeLimits.nativeStackSizeBytes
thread.name = "CodexBar QuickJS TypeScript transpiler"
thread.start()
let deadline = Date().addingTimeInterval(ProviderPluginRuntime.defaultTimeout + 1)
while Date() < deadline {
if let result = box.wait(until: deadline) {
return try result.get()
}
}
throw ProviderPluginError.timedOut
}
private static func transpileOnCurrentThread(source: String, sucraseSource: String) throws -> String {
guard let runtime = JS_NewRuntime() else {
throw ProviderPluginError.load("QuickJS could not create a TypeScript transpiler runtime")
}
JS_SetMemoryLimit(runtime, QuickJSProviderPluginEngine.memoryLimitBytes)
JS_SetMaxStackSize(runtime, QuickJSProviderPluginEngine.stackLimitBytes)
guard let context = JS_NewContext(runtime) else {
JS_FreeRuntime(runtime)
throw ProviderPluginError.load("QuickJS could not create a TypeScript transpiler context")
}
JS_UpdateStackTop(runtime)
guard let watchdog = cqjs_watchdog_create(nil, nil) else {
JS_FreeContext(context)
JS_FreeRuntime(runtime)
throw ProviderPluginError.load("QuickJS could not create its TypeScript transpiler watchdog")
}
cqjs_watchdog_install(watchdog, runtime, context)
cqjs_watchdog_arm(watchdog, UInt64(ProviderPluginRuntime.defaultTimeout * 1000))
defer {
cqjs_watchdog_disarm(watchdog)
// The runtime retains the interrupt-handler opaque pointer until it is freed.
JS_FreeContext(context)
JS_FreeRuntime(runtime)
cqjs_watchdog_destroy(watchdog)
}
func exceptionMessage() -> String {
let exception = JS_GetException(context)
defer { cqjs_free_value(context, exception) }
var length = 0
guard let pointer = JS_ToCStringLen2(context, &length, exception, false) else { return "unknown error" }
defer { JS_FreeCString(context, pointer) }
let bytes = UnsafeRawPointer(pointer).assumingMemoryBound(to: UInt8.self)
return String(bytes: UnsafeBufferPointer(start: bytes, count: length), encoding: .utf8) ?? "unknown error"
}
func evaluate(_ script: String, filename: String) throws -> JSValue {
JS_UpdateStackTop(runtime)
let value = script.utf8CString.withUnsafeBufferPointer { scriptBuffer in
filename.withCString { filenamePointer in
JS_Eval(
context,
scriptBuffer.baseAddress,
scriptBuffer.count - 1,
filenamePointer,
JS_EVAL_TYPE_GLOBAL)
}
}
guard !cqjs_is_exception(value) else {
throw ProviderPluginError.load("TypeScript transpilation failed: \(exceptionMessage())")
}
return value
}
let sucrase = try evaluate(sucraseSource, filename: "sucrase.js")
cqjs_free_value(context, sucrase)
let global = JS_GetGlobalObject(context)
defer { cqjs_free_value(context, global) }
let sourceValue = source.utf8CString.withUnsafeBufferPointer { buffer in
JS_NewStringLen(context, buffer.baseAddress, buffer.count - 1)
}
_ = JS_SetPropertyStr(context, global, "__codexbarTypeScriptSource", sourceValue)
let result = try evaluate(
"sucrase.transform(__codexbarTypeScriptSource, {transforms:['typescript']}).code",
filename: "<sucrase-transform>")
defer { cqjs_free_value(context, result) }
var length = 0
guard let pointer = JS_ToCStringLen2(context, &length, result, false) else {
throw ProviderPluginError.load("TypeScript transpilation returned no output")
}
defer { JS_FreeCString(context, pointer) }
let bytes = UnsafeRawPointer(pointer).assumingMemoryBound(to: UInt8.self)
guard let output = String(bytes: UnsafeBufferPointer(start: bytes, count: length), encoding: .utf8),
!output.isEmpty
else { throw ProviderPluginError.load("TypeScript transpilation returned no output") }
return output
}
}
@@ -363,6 +363,22 @@ public final class UserProviderPluginLoader: @unchecked Sendable {
throw ProviderPluginError.load("bundled Sucrase \(Self.sucraseVersion) resource was not found")
}
let sucraseSource = try String(contentsOf: resourceURL, encoding: .utf8)
let output = switch ProviderPluginRuntime.resolveEngineKind(.automatic) {
case .automatic:
preconditionFailure("automatic plugin engine selection must be resolved")
case .javaScriptCore:
try Self.transpileTypeScriptWithJavaScriptCore(source: source, sucraseSource: sucraseSource)
case .quickJS:
try QuickJSProviderPluginEngine.transpileTypeScript(source: source, sucraseSource: sucraseSource)
}
try Data(output.utf8).write(to: cacheURL, options: .atomic)
return (output, cacheURL, false)
}
private static func transpileTypeScriptWithJavaScriptCore(
source: String,
sucraseSource: String) throws -> String
{
#if canImport(JavaScriptCore)
guard let context = JSContext() else {
throw ProviderPluginError.load("JavaScriptCore could not create a TypeScript transpiler context")
@@ -383,13 +399,10 @@ public final class UserProviderPluginLoader: @unchecked Sendable {
guard let output = result?.toString(), !output.isEmpty else {
throw ProviderPluginError.load("TypeScript transpilation returned no output")
}
return output
#else
let output = try QuickJSProviderPluginEngine.transpileTypeScript(
source: source,
sucraseSource: sucraseSource)
throw ProviderPluginError.load("JavaScriptCore is unavailable on this platform")
#endif
try Data(output.utf8).write(to: cacheURL, options: .atomic)
return (output, cacheURL, false)
}
}
@@ -6,6 +6,40 @@ import Testing
@testable import CodexBarCore
struct ProviderPluginRuntimeTests {
@Test
func `automatic engine defaults to QuickJS`() {
#expect(ProviderPluginRuntime.resolveEngineKind(
.automatic,
environment: [:],
useJavaScriptCoreRollback: false) == .quickJS)
}
@Test
func `explicit engine selection bypasses automatic policy`() {
#expect(ProviderPluginRuntime.resolveEngineKind(
.quickJS,
environment: [ProviderPluginRuntime.engineEnvironmentKey: "jsc"],
useJavaScriptCoreRollback: true) == .quickJS)
}
#if canImport(JavaScriptCore)
@Test
func `JavaScriptCore rollback supports environment and debug defaults`() {
#expect(ProviderPluginRuntime.resolveEngineKind(
.automatic,
environment: [ProviderPluginRuntime.engineEnvironmentKey: "jsc"],
useJavaScriptCoreRollback: false) == .javaScriptCore)
#expect(ProviderPluginRuntime.resolveEngineKind(
.automatic,
environment: [:],
useJavaScriptCoreRollback: true) == .javaScriptCore)
#expect(ProviderPluginRuntime.resolveEngineKind(
.automatic,
environment: [ProviderPluginRuntime.engineEnvironmentKey: "quickjs"],
useJavaScriptCoreRollback: true) == .quickJS)
}
#endif
@Test
func `missing resource bundle throws a provider load error`() {
#expect(throws: ProviderPluginError.load(CodexBarCoreResources.missingBundleMessage)) {
@@ -489,6 +523,55 @@ struct ProviderPluginRuntimeTests {
}
}
@Test
func `QuickJS engine supports bounded recursion on its dedicated stack`() async throws {
let runtime = try ProviderPluginRuntime(
source: Self.plugin(fetchBody: """
function recurse(depth) {
return depth <= 0 ? 0 : 1 + recurse(depth - 1);
}
return { primary: { usedPercent: recurse(100) } };
"""),
engine: .quickJS)
let snapshot = try await runtime.fetchUsage(secrets: ["TEST_KEY": "secret"])
#expect(snapshot.primary?.usedPercent == 100)
}
@Test
func `QuickJS engine reports recursion beyond its JavaScript stack limit`() async throws {
// Production geometry only: QuickJS's overflow guard is unreliable when the native
// margin above the JS limit is thin (macOS crashes instead of throwing — see the
// quickjs-ng/zipline reports), so a deliberately starved worker stack turns this
// test into a layout-lottery process crash on CI runners. The default 4 MiB worker
// with the 1 MiB JS limit leaves 3 MiB of margin for the guard and throw path while
// still proving the semantics: over-limit recursion yields a clean script error.
let engine = try Self.quickJSEngine(
source: Self.plugin(fetchBody: """
function recurse(depth) {
return depth <= 0 ? 0 : 1 + recurse(depth - 1);
}
return { primary: { usedPercent: recurse(100000) } };
"""),
workerStackSizeBytes: QuickJSRuntimeLimits.nativeStackSizeBytes)
do {
_ = try await Self.fetchUsage(engine: engine)
Issue.record("Expected stack overflow")
} catch let error as ProviderPluginError {
guard case let .script(message) = error else {
Issue.record("Unexpected plugin error: \(error)")
return
}
let normalizedMessage = message.lowercased()
#expect(normalizedMessage.contains("stack"))
#expect(normalizedMessage.contains("overflow") || normalizedMessage.contains("exceeded"))
} catch {
Issue.record("Unexpected error: \(error)")
}
}
@Test
func `hung script times out and next fetch uses a fresh context`() async throws {
let runtime = try ProviderPluginRuntime(
@@ -532,6 +615,41 @@ struct ProviderPluginRuntimeTests {
"""
}
private static func quickJSEngine(
source: String,
workerStackSizeBytes: Int) throws -> QuickJSProviderPluginEngine
{
let bundle = try #require(CodexBarCoreResources.bundle)
let preludeURL = try #require(bundle.url(
forResource: "provider-plugin-prelude",
withExtension: "js"))
let preludeSource = try String(contentsOf: preludeURL, encoding: .utf8)
return try QuickJSProviderPluginEngine.make(
source: source,
preludeSource: preludeSource,
transport: ProviderHTTPTransportHandler { _ in throw URLError(.unsupportedURL) },
timeout: ProviderPluginRuntime.defaultTimeout,
responseSizeLimit: ProviderPluginRuntime.maximumResponseBytes,
rejectsNonSuccessResponses: false,
allowsDynamicID: false,
workerStackSizeBytes: workerStackSizeBytes)
}
private static func fetchUsage(engine: QuickJSProviderPluginEngine) async throws -> UsageSnapshot {
let result = await withCheckedContinuation { continuation in
engine.fetch(
settings: [:],
secrets: ["TEST_KEY": "secret"],
now: Date(),
timeZone: .current,
contextOptions: .production,
cookieResolver: nil,
instanceCookieResolver: nil)
{ continuation.resume(returning: $0) }
}
return try result.get()
}
private static func transport(
recorder: RequestRecorder,
body: String = #"{"ok":true}"#) -> ProviderHTTPTransportHandler
@@ -0,0 +1,191 @@
#if os(macOS)
import Darwin
import Foundation
import Testing
@testable import CodexBarCore
struct ProviderPluginEngineBenchmarkTests {
private static let bundledPlugins = [
"clawrouter", "crof", "deepgram", "manus", "openai", "openrouter", "perplexity", "poe", "qoder",
"sub2api", "synthetic", "t3chat", "venice", "xai", "zai",
]
private static let creationSamples = 5
private static let fetchIterations = 50
private static let now = Date(timeIntervalSince1970: 1_785_816_000)
@Test
func `compare provider plugin engines`() async throws {
guard ProcessInfo.processInfo.environment["CODEXBAR_PLUGIN_BENCHMARK"] == "1" else { return }
let javaScriptCore = try await self.measure(engine: .javaScriptCore, label: "JavaScriptCore")
let quickJS = try await self.measure(engine: .quickJS, label: "QuickJS")
Self.printCreationTable([javaScriptCore, quickJS])
Self.printFetchTable([javaScriptCore, quickJS])
}
private func measure(engine: ProviderPluginEngineKind, label: String) async throws -> EngineResult {
var creationMilliseconds: [String: Double] = [:]
for plugin in Self.bundledPlugins {
var samples: [Double] = []
for _ in 0..<Self.creationSamples {
let started = ContinuousClock.now
let runtime = try Self.runtime(plugin: plugin, engine: engine, transport: Self.fixtureTransport())
samples.append(Self.milliseconds(since: started))
withExtendedLifetime(runtime) {}
}
creationMilliseconds[plugin] = samples.sorted()[samples.count / 2]
}
let memoryBefore = Self.physicalFootprintBytes()
var peakMemory = memoryBefore
var retainedContexts: [ProviderPluginRuntime] = []
for plugin in Self.bundledPlugins {
try retainedContexts.append(Self.runtime(
plugin: plugin,
engine: engine,
transport: Self.fixtureTransport()))
peakMemory = max(peakMemory, Self.physicalFootprintBytes())
}
let memoryDeltaPerContext = Double(peakMemory - memoryBefore) / Double(retainedContexts.count)
withExtendedLifetime(retainedContexts) {}
var fetchMilliseconds: [String: Double] = [:]
for plugin in ["poe", "openrouter", "crof"] {
let runtime = try Self.runtime(plugin: plugin, engine: engine, transport: Self.fixtureTransport())
_ = try await Self.fetch(plugin: plugin, runtime: runtime)
let started = ContinuousClock.now
for _ in 0..<Self.fetchIterations {
_ = try await Self.fetch(plugin: plugin, runtime: runtime)
}
fetchMilliseconds[plugin] = Self.milliseconds(since: started)
}
return EngineResult(
label: label,
creationMilliseconds: creationMilliseconds,
fetchMilliseconds: fetchMilliseconds,
memoryDeltaPerContextBytes: memoryDeltaPerContext)
}
private static func runtime(
plugin: String,
engine: ProviderPluginEngineKind,
transport: any ProviderHTTPTransport) throws -> ProviderPluginRuntime
{
let bundle = try #require(CodexBarCoreResources.bundle)
let sourceURL = try #require(bundle.url(forResource: plugin, withExtension: "js"))
let source = try String(contentsOf: sourceURL, encoding: .utf8)
try ProviderPluginSourceLint.validateBundled(source, name: plugin)
return try ProviderPluginRuntime(source: source, transport: transport, engine: engine)
}
private static func fetch(plugin: String, runtime: ProviderPluginRuntime) async throws -> UsageSnapshot {
let secretKey = switch plugin {
case "poe": "POE_API_KEY"
case "openrouter": "OPENROUTER_API_KEY"
case "crof": "CROF_API_KEY"
default: preconditionFailure("missing benchmark secret for \(plugin)")
}
return try await runtime.fetchUsage(secrets: [secretKey: "fixture-key"], now: Self.now)
}
private static func fixtureTransport() -> ProviderHTTPTransportHandler {
ProviderHTTPTransportHandler { request in
let body: String
switch request.url?.path {
case "/usage/current_balance":
body = #"{"current_point_balance":2500}"#
case "/usage/points_history":
body = Self.poeHistoryPage(request.url)
case "/api/v1/credits":
body = #"{"data":{"total_credits":100,"total_usage":40}}"#
case "/api/v1/key":
body = #"{"data":{"limit":20,"limit_remaining":15,"limit_reset":"monthly","usage":5,"usage_daily":1,"usage_weekly":2,"usage_monthly":4,"rate_limit":{"requests":120,"interval":"10s"}}}"#
case "/usage_api", "/usage_api/":
body = #"{"credits":9.9999,"requests_plan":1000,"usable_requests":998}"#
default:
throw BenchmarkError.unexpectedURL(request.url)
}
let response = try #require(HTTPURLResponse(
url: request.url!,
statusCode: 200,
httpVersion: nil,
headerFields: ["Content-Type": "application/json"]))
return (Data(body.utf8), response)
}
}
private static func poeHistoryPage(_ url: URL?) -> String {
let components = url.flatMap { URLComponents(url: $0, resolvingAgainstBaseURL: false) }
let cursor = components?.queryItems?.first(where: { $0.name == "starting_after" })?.value
let page = cursor.flatMap { Int($0.replacingOccurrences(of: "page-", with: "")) } ?? 0
let entries = (0..<100).map { index in
let queryID = "entry-\(page)-\(index)"
let points = Double((page * 100) + index + 1) / 10
return #"{"query_id":"\#(queryID)","creation_time":1785772800000000,"bot_name":"fixture-model","usage_type":"API","cost_points":\#(points),"cost_usd":0.01}"#
}.joined(separator: ",")
let nextCursor = page < 4 ? #""page-\#(page + 1)""# : "null"
return #"{"data":[\#(entries)],"next_cursor":\#(nextCursor)}"#
}
private static func physicalFootprintBytes() -> UInt64 {
var info = task_vm_info_data_t()
var count = mach_msg_type_number_t(
MemoryLayout<task_vm_info_data_t>.size / MemoryLayout<natural_t>.size)
let result = withUnsafeMutablePointer(to: &info) { pointer in
pointer.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), $0, &count)
}
}
return result == KERN_SUCCESS ? info.phys_footprint : 0
}
private static func milliseconds(since started: ContinuousClock.Instant) -> Double {
let duration = started.duration(to: .now)
return Double(duration.components.seconds) * 1000
+ Double(duration.components.attoseconds) / 1_000_000_000_000_000
}
private static func printCreationTable(_ results: [EngineResult]) {
print("\nProvider plugin creation + manifest load (median of \(self.creationSamples), milliseconds)")
print("| Plugin | JavaScriptCore | QuickJS |")
print("| --- | ---: | ---: |")
for plugin in self.bundledPlugins {
let values = results.map { $0.creationMilliseconds[plugin, default: 0] }
print(String(format: "| %@ | %.3f | %.3f |", plugin, values[0], values[1]))
}
}
private static func printFetchTable(_ results: [EngineResult]) {
print("\nProvider plugin fetch benchmark (\(self.fetchIterations) iterations, milliseconds)")
print("| Engine | Poe | OpenRouter | Crof | Rough peak memory delta/context |")
print("| --- | ---: | ---: | ---: | ---: |")
for result in results {
print(String(
format: "| %@ | %.3f | %.3f | %.3f | %.1f KiB |",
result.label,
result.fetchMilliseconds["poe", default: 0],
result.fetchMilliseconds["openrouter", default: 0],
result.fetchMilliseconds["crof", default: 0],
result.memoryDeltaPerContextBytes / 1024))
}
}
}
private struct EngineResult {
let label: String
let creationMilliseconds: [String: Double]
let fetchMilliseconds: [String: Double]
let memoryDeltaPerContextBytes: Double
}
private enum BenchmarkError: LocalizedError {
case unexpectedURL(URL?)
var errorDescription: String? {
switch self {
case let .unexpectedURL(url): "unexpected benchmark URL: \(url?.absoluteString ?? "nil")"
}
}
}
#endif
+58 -18
View File
@@ -1,5 +1,5 @@
---
summary: "JavaScriptCore provider-plugin prototype API, safety boundary, enablement, and limitations."
summary: "JavaScript provider-plugin prototype API, engine benchmark, safety boundary, enablement, and limitations."
read_when:
- Working on the JavaScript provider prototype
- Converting a first-party provider to a bundled JavaScript resource
@@ -17,9 +17,49 @@ system: IDs remain compile-time `UsageProvider` cases and scripts ship inside Co
ClawRouter, Deepgram, sub2api, Synthetic, Poe, xAI, and z.ai use the same bundled script on Apple platforms and Linux;
their native fetch twins have been removed.
The runtime selects JavaScriptCore by default on Apple platforms and QuickJS on Linux. Set
`CODEXBAR_PLUGIN_ENGINE=quickjs` on macOS to exercise QuickJS locally. QuickJS uses a 20-second in-engine interrupt
watchdog, a 64 MiB heap limit, and a 2 MiB JavaScript stack limit; JavaScriptCore retains the existing worker behavior.
The runtime selects QuickJS on every platform. QuickJS uses a 20-second in-engine interrupt watchdog, a 64 MiB heap
limit, and a 2 MiB JavaScript stack limit. On Apple platforms, set `CODEXBAR_PLUGIN_ENGINE=jsc` or enable the
JavaScriptCore rollback in **Settings → Debug → Provider Plugins** and restart CodexBar. An explicit engine environment
value overrides the persisted Debug setting. JavaScriptCore remains in-tree for rollback and A/B drift detection.
## Engine benchmark
The test-only `ProviderPluginEngineBenchmarkTests` instrumentation compares both engines without pass/fail thresholds.
Run it on macOS with
`CODEXBAR_PLUGIN_BENCHMARK=1 swift test --filter ProviderPluginEngineBenchmarkTests`. The August 8, 2026 baseline below
was captured from a Swift debug build on an Apple M3 Ultra. Creation includes loading and linting each bundled source,
creating its runtime, and reading its manifest; values are the median of five samples in milliseconds.
| Bundled plugin | JavaScriptCore | QuickJS |
| --- | ---: | ---: |
| clawrouter | 2.167 | 3.563 |
| crof | 1.606 | 1.736 |
| deepgram | 2.074 | 3.127 |
| manus | 0.907 | 4.241 |
| openai | 2.859 | 4.279 |
| openrouter | 1.901 | 3.333 |
| perplexity | 1.114 | 3.861 |
| poe | 1.972 | 3.681 |
| qoder | 0.863 | 2.362 |
| sub2api | 2.848 | 4.018 |
| synthetic | 2.617 | 5.985 |
| t3chat | 1.521 | 1.734 |
| venice | 1.483 | 1.881 |
| xai | 1.359 | 2.470 |
| zai | 2.862 | 8.986 |
Fetch timings reuse one context for 50 iterations with fixture transport. Poe exercises all five history pages with
100 entries per page, OpenRouter performs its credits and key requests, and Crof performs its single usage request.
Memory is a rough macOS task physical-footprint delta while retaining all 15 contexts, divided by context count.
| Engine | Poe (50) | OpenRouter (50) | Crof (50) | Rough peak delta/context |
| --- | ---: | ---: | ---: | ---: |
| JavaScriptCore | 321.237 ms | 36.424 ms | 23.917 ms | 806.4 KiB |
| QuickJS | 1000.066 ms | 90.610 ms | 38.119 ms | 117.3 KiB |
At this representative workload QuickJS trades roughly 1.63.1× fetch time for a much smaller measured context
footprint. All operations stay well below the 20-second watchdog; these figures are instrumentation for future engine
work, not a performance contract.
Plugin manifests and their projected snapshots now carry a validated `ProviderInstanceID`. The prototype still maps
that instance ID to an existing first-party `UsageProvider` before using browser-cookie brokerage or other bespoke
@@ -92,9 +132,9 @@ Cookie plugins omit `auth`, declare `capabilities: ["browser-cookies"]`, and lis
## `ctx` reference
`ctx` exists only as the argument to `fetchUsage`; it is not a global. JavaScriptCore supplies standard ECMAScript
built-ins, but no browser or Node host environment. Tests assert that `fetch`, `XMLHttpRequest`, `setTimeout`, and
`setInterval` are undefined.
`ctx` exists only as the argument to `fetchUsage`; it is not a global. QuickJS and the JavaScriptCore rollback engine
supply standard ECMAScript built-ins, but no browser or Node host environment. Tests assert that `fetch`,
`XMLHttpRequest`, `setTimeout`, and `setInterval` are undefined.
- `await ctx.http.getJSON(url, opts?)` performs a GET and returns `{status, headers, json}`.
- `await ctx.http.get(url, opts?)` performs a GET and returns `{status, headers, bodyText}`.
@@ -173,18 +213,18 @@ bound violation fails the entire fetch with its property path.
## Concurrency and execution limit
Each runtime owns one `JSContext` confined to a dedicated serial dispatch queue; `JSContext` and every `JSValue` remain
on that executor. Promise `then`/rejection callbacks converge on a lock-protected checked continuation gate, so network,
timeout, and script completion can resume Swift exactly once. The exported `JSContextGroupSetExecutionTimeLimit` symbol
has no declaration in the public macOS JavaScriptCore headers, so the prototype does not bind that private SPI.
Each runtime owns one engine context confined to a dedicated serial worker. QuickJS uses a 4 MiB native-stack thread,
leaving guard-page margin beyond its 2 MiB JavaScript stack limit. Promise `then`/rejection callbacks converge on a
lock-protected checked continuation gate, so network, timeout, and script completion can resume Swift exactly once.
QuickJS's `JS_SetInterruptHandler` stops evaluation on that worker when the 20-second watchdog fires;
the poisoned context is discarded and the next refresh creates a fresh one, which the hung-script recovery test proves.
Instead, a 20-second wall-clock watchdog fails the refresh and discards the poisoned worker; the next refresh creates a
new context on a fresh executor, which the hung-script recovery test proves. This keeps refresh callers responsive but
cannot interrupt the abandoned JavaScriptCore thread, which may remain alive until process exit. A production plugin
runtime needs a public interrupt API or a killable helper-process boundary before accepting untrusted scripts.
The same watchdog is production-default for first-party cut-over providers. It is part of the shared runtime, not the
prototype flag, so cut-over providers retain timeout and fresh-context recovery without `CODEXBAR_JS_PROVIDERS`.
The same hard-interrupt watchdog is production-default for first-party cut-over providers. It is part of the shared
runtime, not the prototype flag, so cut-over providers retain timeout and fresh-context recovery without
`CODEXBAR_JS_PROVIDERS`. The Apple-only JavaScriptCore rollback still uses a `JSContext` and `JSValue` objects confined
to its executor. The exported `JSContextGroupSetExecutionTimeLimit` symbol has no declaration in public macOS headers,
so the rollback path does not bind that private SPI: its watchdog returns to the caller and discards the poisoned
context, but it cannot interrupt the abandoned JavaScriptCore thread, which may remain alive until process exit.
## Current limitations
+11 -6
View File
@@ -81,8 +81,9 @@ example, `acme-usage` and `API_KEY` use `CODEXBAR_PLUGIN_ACME_USAGE_API_KEY`.
## `ctx` API
`ctx` exists only during `fetchUsage`. CodexBar uses JavaScriptCore on Apple platforms and QuickJS on Linux; both
provide ECMAScript built-ins but no browser or Node environment. `Intl` is engine-dependent and unavailable in QuickJS,
`ctx` exists only during `fetchUsage`. CodexBar uses QuickJS on every platform; both QuickJS and the Apple-only
JavaScriptCore rollback engine provide ECMAScript built-ins but no browser or Node environment. `Intl` is
engine-dependent and unavailable in QuickJS,
so portable third-party plugins must use the host helpers below instead of ECMA-402. `fetch`, `XMLHttpRequest`, timers,
`require`, `process`, and filesystem APIs are unavailable.
@@ -120,9 +121,12 @@ response bytes are capped at 1 MiB. Request URLs must match a declared, approved
Bundled first-party providers that have cut over to JavaScript use the shared runtime's 20-second hung-script watchdog.
A timeout fails that refresh and discards the poisoned worker so the next refresh starts with a fresh context; this is
production-default and does not depend on `CODEXBAR_JS_PROVIDERS`.
On Linux, QuickJS enforces the watchdog in-engine with `JS_SetInterruptHandler`, caps the runtime heap at 64 MiB, and
caps the JavaScript stack at 2 MiB. The interrupt terminates evaluation on its confined thread; timed-out scripts do not
leave an abandoned evaluation thread behind.
QuickJS enforces the watchdog in-engine with `JS_SetInterruptHandler`, caps the runtime heap at 64 MiB, and caps the
JavaScript stack at 2 MiB. The interrupt terminates evaluation on its confined thread; timed-out scripts do not leave an
abandoned evaluation thread behind. On Apple platforms, `CODEXBAR_PLUGIN_ENGINE=jsc` selects the JavaScriptCore rollback
engine; the same rollback is available in **Settings → Debug → Provider Plugins** and takes effect after restarting
CodexBar. JavaScriptCore has no public interrupt API, so a timed-out rollback-engine context is discarded but its
abandoned evaluation thread can remain alive until process exit.
## Snapshot result
@@ -158,7 +162,8 @@ and 120 characters per detail string. Wrong types and limit violations fail the
## TypeScript
TypeScript files are transpiled with the bundled Sucrase 3.35.1 build using its `typescript` transform. Use ordinary
TypeScript files are transpiled by the selected plugin engine with the bundled Sucrase 3.35.1 build using its
`typescript` transform. Use ordinary
type syntax but no module imports, JSX, decorators, or runtime TypeScript features that require module resolution.
Transpiled output is cached in `~/Library/Caches/CodexBar/plugins/` under a filename containing the SHA-256 of the source
and the Sucrase version. An unchanged file is a cache hit; any source or compiler-version change produces a new key.