From 4d8ebe0ca05130e5465528b63a2ef45e2eaf2042 Mon Sep 17 00:00:00 2001
From: mimeding <264272563+mimeding@users.noreply.github.com>
Date: Sun, 26 Apr 2026 10:27:25 -0300
Subject: [PATCH 1/4] Gate auto thinking toggle by runtime markers
---
.../Commands/Bundle/MCPBundleManifest.swift | 8 ++++----
.../OsaurusCore/Models/API/AnthropicAPI.swift | 2 +-
.../Models/Chat/ChatConfiguration.swift | 2 +-
.../Models/Configuration/ModelOptions.swift | 9 +++++----
.../Services/LocalReasoningCapability.swift | 5 ++++-
.../Model/ModelProfileRegistryTests.swift | 6 +++---
.../LocalReasoningCapabilityTests.swift | 20 +++++++++++++++++++
README.md | 4 ++--
docs/FEATURES.md | 2 +-
docs/OpenAI_API_GUIDE.md | 4 ++--
10 files changed, 43 insertions(+), 19 deletions(-)
diff --git a/Packages/OsaurusCLI/Sources/OsaurusCLICore/Commands/Bundle/MCPBundleManifest.swift b/Packages/OsaurusCLI/Sources/OsaurusCLICore/Commands/Bundle/MCPBundleManifest.swift
index 4e9375362..73f4c2127 100644
--- a/Packages/OsaurusCLI/Sources/OsaurusCLICore/Commands/Bundle/MCPBundleManifest.swift
+++ b/Packages/OsaurusCLI/Sources/OsaurusCLICore/Commands/Bundle/MCPBundleManifest.swift
@@ -3,7 +3,7 @@
// osaurus
//
// Model for MCPB (MCP Bundle) manifest.json files.
-// Supports both standard MCPB format and Claude Desktop integration format.
+// Supports both standard MCPB format and desktop-client integration format.
//
import Foundation
@@ -12,7 +12,7 @@ struct MCPBundleManifest: Codable {
// Standard MCPB format
let mcpVersion: String?
- // Claude Desktop format
+ // Desktop-client integration format
let manifestVersion: String?
let name: String
@@ -21,7 +21,7 @@ struct MCPBundleManifest: Codable {
let description: String?
let entry: EntryPoint?
- // Claude Desktop format
+ // Desktop-client integration format
let server: ServerConfig?
let icon: String?
@@ -81,7 +81,7 @@ struct MCPBundleManifest: Codable {
return (entry.command, entry.args, entry.env)
}
- // Try Claude Desktop format
+ // Try desktop-client integration format
if let server = server, let config = server.mcpConfig {
return (config.command, config.args, config.env)
}
diff --git a/Packages/OsaurusCore/Models/API/AnthropicAPI.swift b/Packages/OsaurusCore/Models/API/AnthropicAPI.swift
index fee6973cd..013183e42 100644
--- a/Packages/OsaurusCore/Models/API/AnthropicAPI.swift
+++ b/Packages/OsaurusCore/Models/API/AnthropicAPI.swift
@@ -2,7 +2,7 @@
// AnthropicAPI.swift
// osaurus
//
-// Anthropic Messages API compatible request/response models for Claude Code compatibility.
+// Anthropic Messages API compatible request/response models for SDK-compatible clients.
//
import Foundation
diff --git a/Packages/OsaurusCore/Models/Chat/ChatConfiguration.swift b/Packages/OsaurusCore/Models/Chat/ChatConfiguration.swift
index a5c0c30cd..1783a29ec 100644
--- a/Packages/OsaurusCore/Models/Chat/ChatConfiguration.swift
+++ b/Packages/OsaurusCore/Models/Chat/ChatConfiguration.swift
@@ -90,7 +90,7 @@ public struct ChatConfiguration: Codable, Equatable, Sendable {
// MARK: - Tool Settings
/// When true, no tools or preflight context are passed to the model. The raw message is sent
/// directly, keeping the prompt stable across turns for maximum KV-cache reuse. Recommended
- /// when osaurus is acting as a plain LLM backend for an external agent (e.g. Claude via API).
+ /// when osaurus is acting as a plain LLM backend for an external agent.
public var disableTools: Bool
/// Default tool selection mode for the built-in Default agent (nil => .auto).
diff --git a/Packages/OsaurusCore/Models/Configuration/ModelOptions.swift b/Packages/OsaurusCore/Models/Configuration/ModelOptions.swift
index 57bc403cd..bb33998bd 100644
--- a/Packages/OsaurusCore/Models/Configuration/ModelOptions.swift
+++ b/Packages/OsaurusCore/Models/Configuration/ModelOptions.swift
@@ -161,14 +161,15 @@ struct QwenThinkingProfile: ModelProfile {
// MARK: - Auto Thinking Profile (chat-template driven)
-/// Fallback profile that activates for any locally-installed model whose
-/// chat template reads an `enable_thinking` kwarg. Registered last so that
-/// explicit family profiles (Qwen, Venice, etc.) still win when they match.
+/// Fallback profile that activates for locally-installed models whose chat
+/// template exposes an `enable_thinking` kwarg and uses thinking markers the
+/// runtime can process. Registered last so that explicit family profiles
+/// (Qwen, Venice, etc.) still win when they match.
struct AutoThinkingProfile: ModelProfile {
static let displayName = "Thinking"
static func matches(modelId: String) -> Bool {
- LocalReasoningCapability.capability(forModelId: modelId).hasEnableThinkingKwarg
+ LocalReasoningCapability.capability(forModelId: modelId).isToggleableThinking
}
static let options: [ModelOptionDefinition] = [
diff --git a/Packages/OsaurusCore/Services/LocalReasoningCapability.swift b/Packages/OsaurusCore/Services/LocalReasoningCapability.swift
index 0d8a11eae..f6d223ede 100644
--- a/Packages/OsaurusCore/Services/LocalReasoningCapability.swift
+++ b/Packages/OsaurusCore/Services/LocalReasoningCapability.swift
@@ -21,12 +21,15 @@ enum LocalReasoningCapability {
struct Capability: Sendable {
/// Template references `` or `` tags.
let supportsThinking: Bool
- /// Template reads an `enable_thinking` kwarg — i.e. thinking is toggleable.
+ /// Template reads an `enable_thinking` kwarg.
let hasEnableThinkingKwarg: Bool
/// Template itself injects a literal `` opener into the assistant prompt
/// tail, which means the model's generated stream will only contain the closing
/// `` and needs a middleware prepend for the UI tag parser to work.
let templateInjectsThinkTag: Bool
+ /// True when the template both exposes a toggle kwarg and uses
+ /// reasoning markers the runtime recognizes.
+ var isToggleableThinking: Bool { supportsThinking && hasEnableThinkingKwarg }
static let none = Capability(
supportsThinking: false,
diff --git a/Packages/OsaurusCore/Tests/Model/ModelProfileRegistryTests.swift b/Packages/OsaurusCore/Tests/Model/ModelProfileRegistryTests.swift
index c92f59f35..cf63f8bf8 100644
--- a/Packages/OsaurusCore/Tests/Model/ModelProfileRegistryTests.swift
+++ b/Packages/OsaurusCore/Tests/Model/ModelProfileRegistryTests.swift
@@ -85,9 +85,9 @@ struct ModelProfileRegistryTests {
@Test("Non-reasoning Gemma variants do not get a thinking toggle")
func gemma_noThinkingToggle() {
- let profile = ModelProfileRegistry.profile(for: "gemma-4-e2b-it-4bit")
- // Gemma can match an image-options profile but should never expose
- // the thinking toggle because it doesn't honor `enable_thinking`.
+ let profile = ModelProfileRegistry.profile(for: "gemma-2-non-reasoning-\(UUID().uuidString)")
+ // Use a guaranteed-missing suffix so this stays independent of the
+ // developer's locally installed model directory.
#expect(profile?.thinkingOption == nil)
}
}
diff --git a/Packages/OsaurusCore/Tests/Service/LocalReasoningCapabilityTests.swift b/Packages/OsaurusCore/Tests/Service/LocalReasoningCapabilityTests.swift
index c5747d11f..bc09bf5c5 100644
--- a/Packages/OsaurusCore/Tests/Service/LocalReasoningCapabilityTests.swift
+++ b/Packages/OsaurusCore/Tests/Service/LocalReasoningCapabilityTests.swift
@@ -21,6 +21,7 @@ struct LocalReasoningCapabilityTests {
#expect(cap.supportsThinking)
#expect(cap.hasEnableThinkingKwarg)
#expect(cap.templateInjectsThinkTag)
+ #expect(cap.isToggleableThinking)
}
@Test("Qwen3-style: supports thinking but no template-side injection")
@@ -35,6 +36,7 @@ struct LocalReasoningCapabilityTests {
#expect(cap.supportsThinking)
#expect(cap.hasEnableThinkingKwarg)
#expect(!cap.templateInjectsThinkTag)
+ #expect(cap.isToggleableThinking)
}
@Test("Non-reasoning template: all signals false")
@@ -46,6 +48,21 @@ struct LocalReasoningCapabilityTests {
#expect(!cap.supportsThinking)
#expect(!cap.hasEnableThinkingKwarg)
#expect(!cap.templateInjectsThinkTag)
+ #expect(!cap.isToggleableThinking)
+ }
+
+ @Test("enable_thinking alone is not enough for a UI toggle")
+ func enableThinkingWithoutRecognizedThinkingMarkers() {
+ let template = """
+ {%- if enable_thinking is defined and enable_thinking -%}
+ {{- '<|reason|>' -}}
+ {%- endif -%}
+ """
+ let cap = LocalReasoningCapability.analyze(template: template)
+ #expect(!cap.supportsThinking)
+ #expect(cap.hasEnableThinkingKwarg)
+ #expect(!cap.templateInjectsThinkTag)
+ #expect(!cap.isToggleableThinking)
}
@Test("GLM-flash style: emits without injection (middleware-needed)")
@@ -60,6 +77,7 @@ struct LocalReasoningCapabilityTests {
#expect(cap.supportsThinking)
#expect(!cap.hasEnableThinkingKwarg)
#expect(!cap.templateInjectsThinkTag)
+ #expect(!cap.isToggleableThinking)
}
/// Gemma-4's chat_template.jinja opens thinking with the pipe-wrapped
@@ -83,6 +101,7 @@ struct LocalReasoningCapabilityTests {
#expect(cap.supportsThinking)
#expect(cap.hasEnableThinkingKwarg)
#expect(cap.templateInjectsThinkTag)
+ #expect(cap.isToggleableThinking)
}
// MARK: - jang_config.json chat.reasoning fallback (DSV4-class bundles)
@@ -123,6 +142,7 @@ struct LocalReasoningCapabilityTests {
// Python encoder takes `thinking_mode` as a positional argument
// instead, so the kwarg flag stays false.
#expect(cap?.hasEnableThinkingKwarg == false)
+ #expect(cap?.isToggleableThinking == false)
// DSV4's template is outside the bundle (Python module) — vmlx
// injects the thinking tag itself when the caller picks thinking
// mode. From osaurus's perspective there is no on-disk Jinja to
diff --git a/README.md b/README.md
index 8c8ca4adf..6a19fb33d 100644
--- a/README.md
+++ b/README.md
@@ -126,7 +126,7 @@ Connect to OpenAI, Anthropic, Gemini, xAI/Grok, [Venice AI](https://venice.ai),
## MCP
-Osaurus is a full MCP (Model Context Protocol) server. Give Cursor, Claude Desktop, or any MCP client access to your tools:
+Osaurus is a full MCP (Model Context Protocol) server. Give any MCP-compatible client access to your tools:
```json
{
@@ -172,7 +172,7 @@ Drop-in endpoints for existing tools:
| Anthropic | `http://127.0.0.1:1337/anthropic/v1/messages` |
| Ollama | `http://127.0.0.1:1337/api/chat` |
-All prefixes supported (`/v1`, `/api`, `/v1/api`). Full function calling with streaming tool call deltas. `/chat/completions` keeps **strict OpenAI semantics** — it returns `tool_calls` and the client executes them, so Osaurus drops in cleanly behind harnesses (Cursor, OpenWebUI, Continue, Aider) that already manage their own tool loop. For server-side autonomous loops use `POST /agents/{id}/run`; to expose Osaurus tools to remote MCP harnesses use `/mcp/tools` + `/mcp/call`. See [OpenAI API Guide](docs/OpenAI_API_GUIDE.md) for tool calling, streaming, and SDK examples. Building a macOS app that connects to Osaurus? See the [Shared Configuration Guide](docs/SHARED_CONFIGURATION_GUIDE.md).
+All prefixes supported (`/v1`, `/api`, `/v1/api`). Full function calling with streaming tool call deltas. `/chat/completions` keeps **strict OpenAI semantics** -- it returns `tool_calls` and the client executes them, so Osaurus drops in cleanly behind harnesses that already manage their own tool loop. For server-side autonomous loops use `POST /agents/{id}/run`; to expose Osaurus tools to remote MCP harnesses use `/mcp/tools` + `/mcp/call`. See [OpenAI API Guide](docs/OpenAI_API_GUIDE.md) for tool calling, streaming, and SDK examples. Building a macOS app that connects to Osaurus? See the [Shared Configuration Guide](docs/SHARED_CONFIGURATION_GUIDE.md).
## CLI
diff --git a/docs/FEATURES.md b/docs/FEATURES.md
index fbb7fe9c7..f064b3e41 100644
--- a/docs/FEATURES.md
+++ b/docs/FEATURES.md
@@ -277,7 +277,7 @@ See [INFERENCE_RUNTIME.md](./INFERENCE_RUNTIME.md) for the full runtime architec
### Anthropic API Compatibility
-**Purpose:** Provide Anthropic Messages API compatibility for Claude Code and other Anthropic SDK clients.
+**Purpose:** Provide Anthropic Messages API compatibility for Anthropic SDK-compatible clients.
**Components:**
diff --git a/docs/OpenAI_API_GUIDE.md b/docs/OpenAI_API_GUIDE.md
index b68fc12ae..f7339bf8e 100644
--- a/docs/OpenAI_API_GUIDE.md
+++ b/docs/OpenAI_API_GUIDE.md
@@ -38,7 +38,7 @@ Example response:
Generate chat completions using the specified model.
-> **Tool calling:** `/chat/completions` follows **strict OpenAI semantics** — when the model emits `tool_calls`, the response (or final SSE chunk) returns those calls and the **client is expected to execute them and POST the results back** in the next request. Osaurus deliberately does **not** auto-execute tools on this endpoint so it can serve as a drop-in backend for harnesses that already manage their own tool loop (Cursor, OpenWebUI, Continue, Aider, etc.).
+> **Tool calling:** `/chat/completions` follows **strict OpenAI semantics** — when the model emits `tool_calls`, the response (or final SSE chunk) returns those calls and the **client is expected to execute them and POST the results back** in the next request. Osaurus deliberately does **not** auto-execute tools on this endpoint so it can serve as a drop-in backend for harnesses that already manage their own tool loop.
>
> If you want server-side autonomous loops, use `POST /agents/{id}/run` (it executes tools, manages iteration budget, and streams hint/done frames). If you want to expose Osaurus tools to a remote model harness, use the MCP endpoints (`GET /mcp/tools`, `POST /mcp/call`).
@@ -257,7 +257,7 @@ When you want Osaurus to execute tools on your behalf (manage the iteration budg
### Aggregating Osaurus tools through MCP
-The Model Context Protocol endpoints let any MCP-aware harness (e.g. Cursor, Claude Desktop with MCP plugins) connect and discover Osaurus tools without committing to the agent endpoint:
+The Model Context Protocol endpoints let any MCP-aware harness connect and discover Osaurus tools without committing to the agent endpoint:
- `GET /mcp/tools` — list registered tools as MCP `Tool` definitions
- `POST /mcp/call` — invoke a tool by name with structured arguments
From b34ba26e399af4b711ca17e6e933fafc6ac18444 Mon Sep 17 00:00:00 2001
From: mimeding <264272563+mimeding@users.noreply.github.com>
Date: Mon, 27 Apr 2026 09:06:22 -0300
Subject: [PATCH 2/4] Hide SQLCipher extension API during Swift import
---
Packages/OsaurusCore/Package.swift | 17 ++-
Packages/OsaurusCore/SQLCipher/README.md | 26 ++++
.../SQLCipher/include/OsaurusSQLCipher.h | 16 +--
.../SQLCipher/include/sqlite3ext.h | 27 ++--
.../Storage/SQLCipherVendorGuardTests.swift | 117 +++++++++---------
5 files changed, 115 insertions(+), 88 deletions(-)
diff --git a/Packages/OsaurusCore/Package.swift b/Packages/OsaurusCore/Package.swift
index 8d3f55887..c008a1f77 100644
--- a/Packages/OsaurusCore/Package.swift
+++ b/Packages/OsaurusCore/Package.swift
@@ -88,10 +88,13 @@ let package = Package(
// both the header guard and the cSettings flag are in
// place — CI fails if a SQLCipher bump strips the guard.
//
- // ⚠️ `sqlite3ext.h` struct collision. A similar issue occurs
- // with `struct sqlite3_api_routines` in `sqlite3ext.h`.
- // We wrap its contents in `#ifndef OSAURUS_OMIT_SQLITE3EXT_HEADERS`
- // and define it below.
+ // ⚠️ sqlite3ext.h collision. Newer macOS SDKs append fields
+ // to `sqlite3_api_routines` before our pinned SQLCipher
+ // adopts that SQLite version. Osaurus does not compile
+ // SQLite loadable extensions, so the umbrella header hides
+ // sqlite3ext.h's loadable-extension API from the Swift
+ // Clang importer while still including the header to keep
+ // module import warnings quiet.
.target(
name: "OsaurusSQLCipher",
path: "SQLCipher",
@@ -128,12 +131,6 @@ let package = Package(
// copy of sqlite3.h text is unaffected, so the C
// compilation of FTS5 keeps working.
.define("OSAURUS_OMIT_FTS5_HEADERS"),
- // Hide the `sqlite3_api_routines` struct from
- // `include/sqlite3ext.h` so the Swift Clang importer
- // doesn't conflict with the system SQLite3 module.
- // `sqlite3.c`'s inlined copy of sqlite3ext.h text is
- // unaffected.
- .define("OSAURUS_OMIT_SQLITE3EXT_HEADERS"),
// The SQLite amalgamation calls a few self-references
// before their forward declarations show up; modern
// Apple clang upgrades this from a warning to an
diff --git a/Packages/OsaurusCore/SQLCipher/README.md b/Packages/OsaurusCore/SQLCipher/README.md
index 9e550d8d8..128d0fb03 100644
--- a/Packages/OsaurusCore/SQLCipher/README.md
+++ b/Packages/OsaurusCore/SQLCipher/README.md
@@ -105,6 +105,32 @@ The amalgamation `sqlite3.c` itself is **not** modified — it inlines
its own copy of sqlite3.h text, so the C compilation of FTS5 keeps
working.
+### Re-applying the sqlite3ext import guard
+
+`include/sqlite3ext.h` has an OSAURUS LOCAL MODIFICATION that hides the
+loadable-extension API behind `OSAURUS_OMIT_SQLITE_EXTENSION_API`.
+`include/OsaurusSQLCipher.h` defines that macro before including
+`sqlite3ext.h`.
+
+This exists because Osaurus imports SQLCipher from Swift for the core
+SQLite API, not for compiling SQLite loadable extensions. Apple's
+system `SQLite3` module is also imported by dependencies in the same
+Swift build. When newer macOS SDKs append fields to
+`sqlite3_api_routines` before this pinned SQLCipher version adopts the
+same upstream SQLite release, Swift's Clang importer rejects the build
+with a cross-module struct-definition mismatch.
+
+After copying a fresh `sqlite3ext.h` over the top, re-apply by:
+
+1. Find `#include "sqlite3.h"` near the top of the file. Insert the
+ `#ifndef OSAURUS_OMIT_SQLITE_EXTENSION_API` block immediately after
+ it.
+2. Insert the matching close immediately before the final
+ `#endif /* SQLITE3EXT_H */`.
+3. Confirm `OsaurusSQLCipher.h` still defines
+ `OSAURUS_OMIT_SQLITE_EXTENSION_API` immediately before it includes
+ `sqlite3ext.h`.
+
## Why CommonCrypto?
`SQLCIPHER_CRYPTO_CC` selects Apple's CommonCrypto library as the
diff --git a/Packages/OsaurusCore/SQLCipher/include/OsaurusSQLCipher.h b/Packages/OsaurusCore/SQLCipher/include/OsaurusSQLCipher.h
index 6f67714b0..c20ccc8cc 100644
--- a/Packages/OsaurusCore/SQLCipher/include/OsaurusSQLCipher.h
+++ b/Packages/OsaurusCore/SQLCipher/include/OsaurusSQLCipher.h
@@ -34,18 +34,18 @@
/* `sqlite3ext.h` lives in the same `include/` dir alongside us, so
* Clang's umbrella-header consistency check requires it to either
* be included from this umbrella or excluded via a module map.
- * Pulling it in here is harmless (it's just the loadable-extension
- * SQLite API) and silences the
+ * We include it with the loadable-extension API hidden because
+ * Osaurus does not compile SQLite loadable extensions, and newer
+ * macOS SDKs may expose extension fields that SQLCipher 4.6.1 does
+ * not. This silences the
*
* warning: umbrella header for module 'OsaurusSQLCipher' does not
* include header 'sqlite3ext.h'
*
- * that would otherwise fire on every Swift import of this module.
- * We define `OSAURUS_OMIT_SQLITE3EXT_HEADERS` to hide the
- * `sqlite3_api_routines` struct from the Swift Clang importer
- * so it doesn't conflict with Apple's system `SQLite3` module. */
-#ifndef OSAURUS_OMIT_SQLITE3EXT_HEADERS
-#define OSAURUS_OMIT_SQLITE3EXT_HEADERS 1
+ * without reintroducing Swift Clang-importer type collisions against
+ * Apple's system SQLite3 module. */
+#ifndef OSAURUS_OMIT_SQLITE_EXTENSION_API
+#define OSAURUS_OMIT_SQLITE_EXTENSION_API 1
#endif
#include "sqlite3ext.h"
diff --git a/Packages/OsaurusCore/SQLCipher/include/sqlite3ext.h b/Packages/OsaurusCore/SQLCipher/include/sqlite3ext.h
index bc2bbab04..dba82484c 100644
--- a/Packages/OsaurusCore/SQLCipher/include/sqlite3ext.h
+++ b/Packages/OsaurusCore/SQLCipher/include/sqlite3ext.h
@@ -17,22 +17,27 @@
*/
#ifndef SQLITE3EXT_H
#define SQLITE3EXT_H
+#include "sqlite3.h"
/*
* BEGIN OSAURUS LOCAL MODIFICATION (do not remove on SQLCipher
* version bumps; re-apply after copying a new sqlite3ext.h).
*
- * The `sqlite3_api_routines` struct collides with the same
- * struct from Apple's system `SQLite3` module when both modules
- * end up imported in the same Swift compilation unit. We hide
- * the entire contents from the Clang importer here (activated
- * by `OSAURUS_OMIT_SQLITE3EXT_HEADERS` in `Package.swift`).
- * `sqlite3.c` includes its own copy of this text so the C
- * compilation is unaffected.
+ * Osaurus imports SQLCipher from Swift for the core SQLite API, not
+ * for compiling SQLite loadable extensions. Apple's system SQLite3
+ * module is also imported by dependencies in the same Swift build,
+ * and modern SDKs may append fields to `sqlite3_api_routines` before
+ * SQLCipher has adopted the same upstream SQLite version. Swift's
+ * Clang importer treats those cross-module struct differences as a
+ * build error.
+ *
+ * The umbrella header defines OSAURUS_OMIT_SQLITE_EXTENSION_API
+ * before including this file so the module stays warning-free without
+ * exporting the loadable-extension API that Osaurus does not use.
+ * The SQLCipher amalgamation is unaffected because sqlite3.c inlines
+ * its own sqlite3ext.h text.
*/
-#ifndef OSAURUS_OMIT_SQLITE3EXT_HEADERS
-
-#include "sqlite3.h"
+#ifndef OSAURUS_OMIT_SQLITE_EXTENSION_API
/*
** The following structure holds pointers to all of the SQLite API
@@ -731,6 +736,6 @@ typedef int (*sqlite3_loadext_entry)(
# define SQLITE_EXTENSION_INIT3 /*no-op*/
#endif
-#endif /* OSAURUS_OMIT_SQLITE3EXT_HEADERS — END OSAURUS LOCAL MODIFICATION */
+#endif /* OSAURUS_OMIT_SQLITE_EXTENSION_API - END OSAURUS LOCAL MODIFICATION */
#endif /* SQLITE3EXT_H */
diff --git a/Packages/OsaurusCore/Tests/Storage/SQLCipherVendorGuardTests.swift b/Packages/OsaurusCore/Tests/Storage/SQLCipherVendorGuardTests.swift
index 5059ce21b..5f29187b7 100644
--- a/Packages/OsaurusCore/Tests/Storage/SQLCipherVendorGuardTests.swift
+++ b/Packages/OsaurusCore/Tests/Storage/SQLCipherVendorGuardTests.swift
@@ -51,7 +51,7 @@ struct SQLCipherVendorGuardTests {
return FileManager.default.fileExists(atPath: header.path) ? header : nil
}
- private static func sqlite3ExtHeaderURL() -> URL? {
+ private static func sqlCipherHeaderURL(named name: String) -> URL? {
let here = URL(fileURLWithPath: #filePath)
var cursor = here.deletingLastPathComponent() // Storage/
cursor.deleteLastPathComponent() // Tests/
@@ -60,7 +60,7 @@ struct SQLCipherVendorGuardTests {
pkg
.appendingPathComponent("SQLCipher", isDirectory: true)
.appendingPathComponent("include", isDirectory: true)
- .appendingPathComponent("sqlite3ext.h")
+ .appendingPathComponent(name)
return FileManager.default.fileExists(atPath: header.path) ? header : nil
}
@@ -108,54 +108,12 @@ struct SQLCipherVendorGuardTests {
)
}
- @Test
- func sqlite3ExtHeader_containsSqlite3ExtOmitGuard() throws {
- guard let url = Self.sqlite3ExtHeaderURL() else {
- Issue.record(
- "Could not locate Packages/OsaurusCore/SQLCipher/include/sqlite3ext.h. If the path moved, update this test."
- )
- return
- }
- let contents = try String(contentsOf: url, encoding: .utf8)
-
- let openGuard = "#ifndef OSAURUS_OMIT_SQLITE3EXT_HEADERS"
- let closeGuard = "#endif /* OSAURUS_OMIT_SQLITE3EXT_HEADERS"
-
- #expect(
- contents.contains(openGuard),
- """
- sqlite3ext.h is missing the OSAURUS_OMIT_SQLITE3EXT_HEADERS open guard.
-
- This means a SQLCipher amalgamation bump overwrote the
- OSAURUS LOCAL MODIFICATION block. Without this guard,
- `import OsaurusSQLCipher` collides with Apple's system
- `SQLite3` module on `struct sqlite3_api_routines`.
-
- Re-apply the guard by wrapping the contents of sqlite3ext.h.
- """
- )
-
- #expect(
- contents.contains(closeGuard),
- """
- sqlite3ext.h has the OSAURUS_OMIT_SQLITE3EXT_HEADERS open guard
- but not the matching close. The wrap is incomplete.
- """
- )
- }
-
@Test
func umbrellaHeader_definesSqliteHasCodec() throws {
- let here = URL(fileURLWithPath: #filePath)
- var cursor = here.deletingLastPathComponent() // Storage/
- cursor.deleteLastPathComponent() // Tests/
- let pkg = cursor.deletingLastPathComponent() // OsaurusCore/
- let umbrella =
- pkg
- .appendingPathComponent("SQLCipher", isDirectory: true)
- .appendingPathComponent("include", isDirectory: true)
- .appendingPathComponent("OsaurusSQLCipher.h")
- guard let contents = try? String(contentsOf: umbrella, encoding: .utf8) else {
+ guard
+ let umbrella = Self.sqlCipherHeaderURL(named: "OsaurusSQLCipher.h"),
+ let contents = try? String(contentsOf: umbrella, encoding: .utf8)
+ else {
Issue.record(
"""
OsaurusSQLCipher.h umbrella header is missing. Despite
@@ -184,6 +142,58 @@ struct SQLCipherVendorGuardTests {
)
}
+ @Test
+ func sqlite3ExtHeader_omitsLoadableExtensionApiFromSwiftImport() throws {
+ guard let url = Self.sqlCipherHeaderURL(named: "sqlite3ext.h") else {
+ Issue.record(
+ "Could not locate Packages/OsaurusCore/SQLCipher/include/sqlite3ext.h. If the path moved, update this test."
+ )
+ return
+ }
+ let contents = try String(contentsOf: url, encoding: .utf8)
+
+ #expect(
+ contents.contains("#ifndef OSAURUS_OMIT_SQLITE_EXTENSION_API"),
+ """
+ sqlite3ext.h is missing the OSAURUS_OMIT_SQLITE_EXTENSION_API open guard.
+
+ Newer macOS SDKs can append fields to sqlite3_api_routines
+ before this pinned SQLCipher version adopts the same SQLite
+ version. Without this guard, Swift's Clang importer can
+ reject OsaurusSQLCipher when another dependency imports
+ Apple's system SQLite3 module in the same build.
+ """
+ )
+ #expect(
+ contents.contains("#endif /* OSAURUS_OMIT_SQLITE_EXTENSION_API"),
+ """
+ sqlite3ext.h has the OSAURUS_OMIT_SQLITE_EXTENSION_API open guard
+ but not the matching close. Re-read the README
+ "Re-applying the sqlite3ext import guard" section.
+ """
+ )
+ }
+
+ @Test
+ func umbrellaHeader_definesOmitExtensionApi() throws {
+ guard
+ let umbrella = Self.sqlCipherHeaderURL(named: "OsaurusSQLCipher.h"),
+ let contents = try? String(contentsOf: umbrella, encoding: .utf8)
+ else {
+ Issue.record("Could not read OsaurusSQLCipher.h")
+ return
+ }
+ #expect(
+ contents.contains("OSAURUS_OMIT_SQLITE_EXTENSION_API"),
+ """
+ OsaurusSQLCipher.h must define OSAURUS_OMIT_SQLITE_EXTENSION_API
+ before including sqlite3ext.h. Otherwise the vendored
+ sqlite3_api_routines struct can collide with the system
+ SQLite3 module on newer macOS SDKs.
+ """
+ )
+ }
+
@Test
func packageManifest_definesOmitFts5HeadersFlag() throws {
// Walk up from `#filePath` to `Packages/OsaurusCore/Package.swift`.
@@ -207,16 +217,5 @@ struct SQLCipherVendorGuardTests {
.define("OSAURUS_OMIT_FTS5_HEADERS"),
"""
)
-
- #expect(
- contents.contains("OSAURUS_OMIT_SQLITE3EXT_HEADERS"),
- """
- Package.swift no longer defines OSAURUS_OMIT_SQLITE3EXT_HEADERS
- in the OsaurusSQLCipher target's cSettings. The header
- guard is useless without this flag. Add it back:
-
- .define("OSAURUS_OMIT_SQLITE3EXT_HEADERS"),
- """
- )
}
}
From 051c31abc63bb5e0736484275b02706089fba315 Mon Sep 17 00:00:00 2001
From: mimeding <264272563+mimeding@users.noreply.github.com>
Date: Mon, 27 Apr 2026 09:50:45 -0300
Subject: [PATCH 3/4] Align CI Xcode with release workflow
---
.github/workflows/ci.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7e4307edf..b4b0b98d6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -29,7 +29,7 @@ env:
CACHE_SALT: v2-vmlx-5b84387
# Pin Xcode so cache keys are stable across runner image bumps. When you
# need to upgrade, change here AND in setup-xcode below.
- XCODE_VERSION: "26.3"
+ XCODE_VERSION: "26.4.1"
jobs:
test-core:
@@ -333,7 +333,7 @@ jobs:
uses: actions/checkout@v5
# OsaurusCLI's Package.swift requires `swift-tools-version: 6.2`, which
- # ships with Xcode 26.3. The runner's default `swift` may be older
+ # ships with the pinned Xcode. The runner's default `swift` may be older
# (currently 6.1), so point xcrun at the pinned Xcode for these steps.
# GitHub Actions doesn't let job-level `env:` reference workflow-level
# `env:`, so keep XCODE_VERSION in sync here if it ever changes.
From 2d47d12ee5436d80cc0441b6854dba2fe1d8431a Mon Sep 17 00:00:00 2001
From: mimeding <264272563+mimeding@users.noreply.github.com>
Date: Tue, 28 Apr 2026 18:53:10 -0300
Subject: [PATCH 4/4] Define SQLCipher FTS5 import guard in umbrella
---
Packages/OsaurusCore/Package.swift | 14 ++++++----
Packages/OsaurusCore/SQLCipher/README.md | 27 +++----------------
.../SQLCipher/include/OsaurusSQLCipher.h | 4 +++
.../Storage/SQLCipherVendorGuardTests.swift | 9 +++++++
4 files changed, 26 insertions(+), 28 deletions(-)
diff --git a/Packages/OsaurusCore/Package.swift b/Packages/OsaurusCore/Package.swift
index c008a1f77..910c9e7ad 100644
--- a/Packages/OsaurusCore/Package.swift
+++ b/Packages/OsaurusCore/Package.swift
@@ -74,19 +74,23 @@ let package = Package(
// Swift's Clang importer sees two different definitions
// of those typedefs and rejects the build with
// 'Fts5ExtensionApi' has different definitions in different modules
- // The fix is two-part:
+ // The fix is three-part:
// 1. `include/sqlite3.h` wraps the `_FTS5_H` block in
// `#ifndef OSAURUS_OMIT_FTS5_HEADERS` (search for
// OSAURUS LOCAL MODIFICATION inside that file).
- // 2. The `cSettings` `.define("OSAURUS_OMIT_FTS5_HEADERS")`
- // below activates the wrap.
+ // 2. `include/OsaurusSQLCipher.h` defines
+ // `OSAURUS_OMIT_FTS5_HEADERS` before including
+ // sqlite3.h so Swift's Clang module import sees the
+ // hidden extension API.
+ // 3. The `cSettings` `.define("OSAURUS_OMIT_FTS5_HEADERS")`
+ // below keeps the C compilation path aligned.
// `sqlite3.c` itself inlines its own copy of the header
// text, so FTS5's SQL-level functionality keeps working;
// we only hide the C-extension API, which Osaurus
// doesn't use.
// `Tests/Storage/SQLCipherVendorGuardTests.swift` asserts
- // both the header guard and the cSettings flag are in
- // place — CI fails if a SQLCipher bump strips the guard.
+ // the header guard, umbrella define, and cSettings flag
+ // are in place — CI fails if a SQLCipher bump strips them.
//
// ⚠️ sqlite3ext.h collision. Newer macOS SDKs append fields
// to `sqlite3_api_routines` before our pinned SQLCipher
diff --git a/Packages/OsaurusCore/SQLCipher/README.md b/Packages/OsaurusCore/SQLCipher/README.md
index 128d0fb03..227130327 100644
--- a/Packages/OsaurusCore/SQLCipher/README.md
+++ b/Packages/OsaurusCore/SQLCipher/README.md
@@ -77,29 +77,10 @@ After copying a fresh `sqlite3.h` over the top, re-apply by:
#endif /* OSAURUS_OMIT_FTS5_HEADERS — END OSAURUS LOCAL MODIFICATION */
```
-Similarly, `include/sqlite3ext.h` has an OSAURUS LOCAL MODIFICATION that
-wraps the entire file in `#ifndef OSAURUS_OMIT_SQLITE3EXT_HEADERS` to
-prevent `struct sqlite3_api_routines` from colliding with Apple's system
-`SQLite3` module.
-
-After copying a fresh `sqlite3ext.h` over the top, re-apply by:
-
-1. Find the `#ifndef SQLITE3EXT_H` line. Insert this block immediately
- after `#define SQLITE3EXT_H`:
-
- ```c
- #ifndef OSAURUS_OMIT_SQLITE3EXT_HEADERS
- ```
-
-2. Find the matching `#endif /* SQLITE3EXT_H */` line at the end. Insert
- immediately before it:
-
- ```c
- #endif /* OSAURUS_OMIT_SQLITE3EXT_HEADERS — END OSAURUS LOCAL MODIFICATION */
- ```
-
-3. Run `swift build`. If anything red, see the explainer in
- `sqlcipher_amalgamation.c`.
+3. Confirm `OsaurusSQLCipher.h` still defines
+ `OSAURUS_OMIT_FTS5_HEADERS` before it includes `sqlite3.h`, and
+ `Package.swift` still defines the same flag in the
+ `OsaurusSQLCipher` target `cSettings`.
The amalgamation `sqlite3.c` itself is **not** modified — it inlines
its own copy of sqlite3.h text, so the C compilation of FTS5 keeps
diff --git a/Packages/OsaurusCore/SQLCipher/include/OsaurusSQLCipher.h b/Packages/OsaurusCore/SQLCipher/include/OsaurusSQLCipher.h
index c20ccc8cc..f205ee6c4 100644
--- a/Packages/OsaurusCore/SQLCipher/include/OsaurusSQLCipher.h
+++ b/Packages/OsaurusCore/SQLCipher/include/OsaurusSQLCipher.h
@@ -30,6 +30,10 @@
#define SQLITE_HAS_CODEC 1
#endif
+#ifndef OSAURUS_OMIT_FTS5_HEADERS
+#define OSAURUS_OMIT_FTS5_HEADERS 1
+#endif
+
#include "sqlite3.h"
/* `sqlite3ext.h` lives in the same `include/` dir alongside us, so
* Clang's umbrella-header consistency check requires it to either
diff --git a/Packages/OsaurusCore/Tests/Storage/SQLCipherVendorGuardTests.swift b/Packages/OsaurusCore/Tests/Storage/SQLCipherVendorGuardTests.swift
index 5f29187b7..8e3babce7 100644
--- a/Packages/OsaurusCore/Tests/Storage/SQLCipherVendorGuardTests.swift
+++ b/Packages/OsaurusCore/Tests/Storage/SQLCipherVendorGuardTests.swift
@@ -136,6 +136,15 @@ struct SQLCipherVendorGuardTests {
fail to compile.
"""
)
+ #expect(
+ contents.contains("OSAURUS_OMIT_FTS5_HEADERS"),
+ """
+ OsaurusSQLCipher.h must define OSAURUS_OMIT_FTS5_HEADERS
+ before including sqlite3.h. The target cSettings flag
+ protects the C compilation, but Swift's Clang module
+ import parses this umbrella header separately.
+ """
+ )
#expect(
contents.contains("#include \"sqlite3.h\""),
"OsaurusSQLCipher.h must #include \"sqlite3.h\" so the codec define applies in the same translation unit."