From bade6b6e875244c974a7a049b0b4f9bc2bd565c4 Mon Sep 17 00:00:00 2001 From: serezha93 Date: Sun, 17 May 2026 10:43:54 +0300 Subject: [PATCH 1/7] docs: add proxy core implementation plan --- .../plans/2026-05-17-network-proxy-core.md | 289 ++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-17-network-proxy-core.md diff --git a/docs/superpowers/plans/2026-05-17-network-proxy-core.md b/docs/superpowers/plans/2026-05-17-network-proxy-core.md new file mode 100644 index 000000000..92b2e90d1 --- /dev/null +++ b/docs/superpowers/plans/2026-05-17-network-proxy-core.md @@ -0,0 +1,289 @@ +# Network Proxy Core Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add invisible network proxy configuration support to CodexBar's config model and validation so the next UI PR can build on a stable, testable core. + +**Architecture:** Keep proxy state as a codable value inside `CodexBarConfig`, validate it centrally in `CodexBarConfigValidator`, and rely on the existing `CodexBarConfigStore` save/load path to persist it unchanged. Do not touch transport wiring, password storage, or any Settings UI in this PR. + +**Tech Stack:** Swift 6, Foundation, existing `CodexBarCore` config types, XCTest-style unit tests in `Tests/CodexBarTests`. + +--- + +### Task 1: Add proxy config model to `CodexBarCore` + +**Files:** +- Create: `/Users/sergey-selderey/Projects/CodexBar/Sources/CodexBarCore/Config/NetworkProxyConfiguration.swift` +- Modify: `/Users/sergey-selderey/Projects/CodexBar/Sources/CodexBarCore/Config/CodexBarConfig.swift:1-220` +- Modify: `/Users/sergey-selderey/Projects/CodexBar/Tests/CodexBarTests/ConfigValidationTests.swift` + +- [ ] **Step 1: Write the failing test** + +Add a round-trip test that encodes and decodes a config with proxy data: + +```swift +func testNetworkProxyConfigEncodesAndDecodes() throws { + let config = CodexBarConfig( + providers: [], + networkProxy: NetworkProxyConfiguration( + enabled: true, + scheme: .http, + host: "proxy.example.com", + port: "8080", + username: "codex")) + + let data = try JSONEncoder().encode(config) + let decoded = try JSONDecoder().decode(CodexBarConfig.self, from: data) + + #expect(decoded.networkProxy?.enabled == true) + #expect(decoded.networkProxy?.scheme == .http) + #expect(decoded.networkProxy?.host == "proxy.example.com") + #expect(decoded.networkProxy?.port == "8080") + #expect(decoded.networkProxy?.username == "codex") +} +``` + +- [ ] **Step 2: Run the test and confirm it fails** + +Run: `swift test --filter ConfigValidationTests/testNetworkProxyConfigEncodesAndDecodes` + +Expected: fail because `networkProxy` is not yet part of the config model. + +- [ ] **Step 3: Implement the model** + +Add this type: + +```swift +import Foundation + +public enum NetworkProxyScheme: String, CaseIterable, Codable, Sendable { + case http + case socks5 +} + +public struct NetworkProxyConfiguration: Codable, Sendable, Equatable { + public var enabled: Bool + public var scheme: NetworkProxyScheme + public var host: String + public var port: String + public var username: String + + public init(enabled: Bool, scheme: NetworkProxyScheme, host: String, port: String, username: String) { + self.enabled = enabled + self.scheme = scheme + self.host = host + self.port = port + self.username = username + } + + public var trimmedHost: String { self.host.trimmingCharacters(in: .whitespacesAndNewlines) } + public var trimmedPort: String { self.port.trimmingCharacters(in: .whitespacesAndNewlines) } + public var trimmedUsername: String { self.username.trimmingCharacters(in: .whitespacesAndNewlines) } + + public var resolvedPort: Int? { + guard let port = Int(self.trimmedPort), (1...65535).contains(port) else { return nil } + return port + } + + public var isActive: Bool { + self.enabled && !self.trimmedHost.isEmpty && self.resolvedPort != nil + } +} +``` + +Update `CodexBarConfig` to store: + +```swift +public var networkProxy: NetworkProxyConfiguration? +``` + +and extend its initializer to accept `networkProxy: NetworkProxyConfiguration? = nil`. + +- [ ] **Step 4: Run the test and confirm it passes** + +Run: `swift test --filter ConfigValidationTests/testNetworkProxyConfigEncodesAndDecodes` + +Expected: pass. + +- [ ] **Step 5: Commit** + +```bash +git add Sources/CodexBarCore/Config/NetworkProxyConfiguration.swift Sources/CodexBarCore/Config/CodexBarConfig.swift +git commit -m "feat(proxy): add network proxy config model" +``` + +### Task 2: Validate proxy config centrally + +**Files:** +- Modify: `/Users/sergey-selderey/Projects/CodexBar/Sources/CodexBarCore/Config/CodexBarConfigValidation.swift:1-260` +- Modify: `/Users/sergey-selderey/Projects/CodexBar/Tests/CodexBarTests/ConfigValidationTests.swift` + +- [ ] **Step 1: Write the failing test** + +Add a validation test that asserts missing host and invalid port are reported: + +```swift +func testNetworkProxyValidationReportsMissingHostAndInvalidPort() { + let config = CodexBarConfig( + providers: [], + networkProxy: NetworkProxyConfiguration( + enabled: true, + scheme: .http, + host: " ", + port: "not-a-port", + username: "codex")) + + let issues = CodexBarConfigValidator.validate(config) + #expect(issues.contains(where: { $0.field == "networkProxy.host" && $0.code == "proxy_host_missing" })) + #expect(issues.contains(where: { $0.field == "networkProxy.port" && $0.code == "proxy_port_invalid" })) +} +``` + +- [ ] **Step 2: Run the test and confirm it fails** + +Run: `swift test --filter ConfigValidationTests/testNetworkProxyValidationReportsMissingHostAndInvalidPort` + +Expected: fail because the validator does not yet inspect `networkProxy`. + +- [ ] **Step 3: Implement validation** + +Add a dedicated proxy validation branch to `CodexBarConfigValidator.validate(_:)`: + +```swift +if let proxy = config.networkProxy { + self.validateNetworkProxy(proxy, issues: &issues) +} +``` + +Use exact issue payloads: + +```swift +CodexBarConfigIssue( + severity: .error, + provider: nil, + field: "networkProxy.host", + code: "proxy_host_missing", + message: "Network proxy host is required when proxy is enabled.") +``` + +```swift +CodexBarConfigIssue( + severity: .error, + provider: nil, + field: "networkProxy.port", + code: "proxy_port_invalid", + message: "Network proxy port must be a number between 1 and 65535.") +``` + +Keep validation gated behind `proxy.enabled` so disabled configs stay quiet. + +- [ ] **Step 4: Run the test and confirm it passes** + +Run: `swift test --filter ConfigValidationTests/testNetworkProxyValidationReportsMissingHostAndInvalidPort` + +Expected: pass. + +- [ ] **Step 5: Commit** + +```bash +git add Sources/CodexBarCore/Config/CodexBarConfigValidation.swift Tests/CodexBarTests/ConfigValidationTests.swift +git commit -m "feat(proxy): validate network proxy config" +``` + +### Task 3: Verify config store round-trip and keep scope invisible + +**Files:** +- Modify: `/Users/sergey-selderey/Projects/CodexBar/Tests/CodexBarTests/ConfigValidationTests.swift` + +- [ ] **Step 1: Write the failing test** + +Add a store round-trip test that writes a config with proxy settings and reloads it from disk: + +```swift +func testConfigStoreRoundTripsNetworkProxy() throws { + let tempDirectory = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString, isDirectory: true) + let store = CodexBarConfigStore(fileURL: tempDirectory.appendingPathComponent("config.json")) + defer { try? FileManager.default.removeItem(at: tempDirectory) } + + let config = CodexBarConfig( + providers: [], + networkProxy: NetworkProxyConfiguration( + enabled: true, + scheme: .socks5, + host: "127.0.0.1", + port: "1080", + username: "codex")) + + try store.save(config) + let loaded = try store.load() + + #expect(loaded?.networkProxy?.scheme == .socks5) + #expect(loaded?.networkProxy?.host == "127.0.0.1") + #expect(loaded?.networkProxy?.port == "1080") +} +``` + +- [ ] **Step 2: Run the test and confirm it fails** + +Run: `swift test --filter ConfigValidationTests/testConfigStoreRoundTripsNetworkProxy` + +Expected: fail until the config model persists `networkProxy`. + +- [ ] **Step 3: Keep the store implementation unchanged unless the test proves otherwise** + +`CodexBarConfigStore` should already serialize and deserialize the new optional field once the model includes it. Do not add migration logic, password handling, or UI side effects. + +- [ ] **Step 4: Run the test and confirm it passes** + +Run: `swift test --filter ConfigValidationTests/testConfigStoreRoundTripsNetworkProxy` + +Expected: pass. + +- [ ] **Step 5: Commit** + +```bash +git add Tests/CodexBarTests/ConfigValidationTests.swift +git commit -m "test(proxy): cover config round-trip" +``` + +### Task 4: Final verification and PR prep + +**Files:** +- Review: `/Users/sergey-selderey/Projects/CodexBar/Sources/CodexBarCore/Config/CodexBarConfig.swift` +- Review: `/Users/sergey-selderey/Projects/CodexBar/Sources/CodexBarCore/Config/CodexBarConfigValidation.swift` +- Review: `/Users/sergey-selderey/Projects/CodexBar/Tests/CodexBarTests/ConfigValidationTests.swift` + +- [ ] **Step 1: Run the focused test set** + +Run: + +```bash +swift test --filter ConfigValidationTests +swift test --filter ConfigValidationTests/testConfigStoreRoundTripsNetworkProxy +swift test --filter ConfigValidationTests/testNetworkProxyConfigEncodesAndDecodes +``` + +Expected: all pass. + +- [ ] **Step 2: Sanity-check the diff** + +Run: + +```bash +git diff --check +git status --short +``` + +Expected: no whitespace errors and only the intended files changed. + +- [ ] **Step 3: Commit the branch state** + +```bash +git add Sources/CodexBarCore/Config/CodexBarConfig.swift Sources/CodexBarCore/Config/CodexBarConfigValidation.swift Tests/CodexBarTests/ConfigValidationTests.swift +git commit -m "feat(proxy): add invisible proxy config core" +``` + +- [ ] **Step 4: Prepare the follow-up visible PR** + +Write a short PR description that explicitly says this is only the invisible config core and that Settings UI and transport wiring will follow in a separate PR. From a9eeef83cc613ebcaefa6806264e309f08b111de Mon Sep 17 00:00:00 2001 From: serezha93 Date: Sun, 17 May 2026 10:47:32 +0300 Subject: [PATCH 2/7] feat(proxy): add invisible proxy config core --- .../CodexBarCore/Config/CodexBarConfig.swift | 11 +++- .../Config/CodexBarConfigValidation.swift | 29 +++++++++ .../Config/NetworkProxyConfiguration.swift | 51 +++++++++++++++ .../CodexBarTests/ConfigValidationTests.swift | 63 +++++++++++++++++++ 4 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 Sources/CodexBarCore/Config/NetworkProxyConfiguration.swift diff --git a/Sources/CodexBarCore/Config/CodexBarConfig.swift b/Sources/CodexBarCore/Config/CodexBarConfig.swift index ab0526d66..696e9e2c0 100644 --- a/Sources/CodexBarCore/Config/CodexBarConfig.swift +++ b/Sources/CodexBarCore/Config/CodexBarConfig.swift @@ -5,10 +5,16 @@ public struct CodexBarConfig: Codable, Sendable { public var version: Int public var providers: [ProviderConfig] + public var networkProxy: NetworkProxyConfiguration? - public init(version: Int = Self.currentVersion, providers: [ProviderConfig]) { + public init( + version: Int = Self.currentVersion, + providers: [ProviderConfig], + networkProxy: NetworkProxyConfiguration? = nil) + { self.version = version self.providers = providers + self.networkProxy = networkProxy } public static func makeDefault( @@ -43,7 +49,8 @@ public struct CodexBarConfig: Codable, Sendable { return CodexBarConfig( version: Self.currentVersion, - providers: normalized) + providers: normalized, + networkProxy: self.networkProxy) } public func orderedProviders() -> [UsageProvider] { diff --git a/Sources/CodexBarCore/Config/CodexBarConfigValidation.swift b/Sources/CodexBarCore/Config/CodexBarConfigValidation.swift index a0dd11b83..b37b398cb 100644 --- a/Sources/CodexBarCore/Config/CodexBarConfigValidation.swift +++ b/Sources/CodexBarCore/Config/CodexBarConfigValidation.swift @@ -44,6 +44,10 @@ public enum CodexBarConfigValidator { self.validateProvider(entry, issues: &issues) } + if let proxy = config.networkProxy { + self.validateNetworkProxy(proxy, issues: &issues) + } + return issues } @@ -254,4 +258,29 @@ public enum CodexBarConfigValidator { code: "invalid_region", message: "Region \(region) is not a valid \(displayName) region.")) } + + private static func validateNetworkProxy( + _ proxy: NetworkProxyConfiguration, + issues: inout [CodexBarConfigIssue]) + { + guard proxy.enabled else { return } + + if proxy.trimmedHost.isEmpty { + issues.append(CodexBarConfigIssue( + severity: .error, + provider: nil, + field: "networkProxy.host", + code: "proxy_host_missing", + message: "Network proxy host is required when proxy is enabled.")) + } + + if proxy.resolvedPort == nil { + issues.append(CodexBarConfigIssue( + severity: .error, + provider: nil, + field: "networkProxy.port", + code: "proxy_port_invalid", + message: "Network proxy port must be a number between 1 and 65535.")) + } + } } diff --git a/Sources/CodexBarCore/Config/NetworkProxyConfiguration.swift b/Sources/CodexBarCore/Config/NetworkProxyConfiguration.swift new file mode 100644 index 000000000..d0bcbb015 --- /dev/null +++ b/Sources/CodexBarCore/Config/NetworkProxyConfiguration.swift @@ -0,0 +1,51 @@ +import Foundation + +public enum NetworkProxyScheme: String, CaseIterable, Codable, Sendable { + case http + case socks5 +} + +public struct NetworkProxyConfiguration: Codable, Sendable, Equatable { + public var enabled: Bool + public var scheme: NetworkProxyScheme + public var host: String + public var port: String + public var username: String + + public init( + enabled: Bool, + scheme: NetworkProxyScheme, + host: String, + port: String, + username: String) + { + self.enabled = enabled + self.scheme = scheme + self.host = host + self.port = port + self.username = username + } + + public var trimmedHost: String { + self.host.trimmingCharacters(in: .whitespacesAndNewlines) + } + + public var trimmedPort: String { + self.port.trimmingCharacters(in: .whitespacesAndNewlines) + } + + public var trimmedUsername: String { + self.username.trimmingCharacters(in: .whitespacesAndNewlines) + } + + public var resolvedPort: Int? { + guard let port = Int(self.trimmedPort), (1...65535).contains(port) else { + return nil + } + return port + } + + public var isActive: Bool { + self.enabled && !self.trimmedHost.isEmpty && self.resolvedPort != nil + } +} diff --git a/Tests/CodexBarTests/ConfigValidationTests.swift b/Tests/CodexBarTests/ConfigValidationTests.swift index e0c0f449f..d509d393a 100644 --- a/Tests/CodexBarTests/ConfigValidationTests.swift +++ b/Tests/CodexBarTests/ConfigValidationTests.swift @@ -83,4 +83,67 @@ struct ConfigValidationTests { #expect(url.path.hasSuffix("/tmp/codexbar-test-config.json")) } + + @Test + func `network proxy config encodes and decodes`() throws { + let config = CodexBarConfig( + providers: [], + networkProxy: NetworkProxyConfiguration( + enabled: true, + scheme: .http, + host: "proxy.example.com", + port: "8080", + username: "codex")) + + let data = try JSONEncoder().encode(config) + let decoded = try JSONDecoder().decode(CodexBarConfig.self, from: data) + + #expect(decoded.networkProxy?.enabled == true) + #expect(decoded.networkProxy?.scheme == .http) + #expect(decoded.networkProxy?.host == "proxy.example.com") + #expect(decoded.networkProxy?.port == "8080") + #expect(decoded.networkProxy?.username == "codex") + } + + @Test + func `config store round trips network proxy`() throws { + let tempDirectory = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString, isDirectory: true) + let store = CodexBarConfigStore(fileURL: tempDirectory.appendingPathComponent("config.json")) + defer { try? FileManager.default.removeItem(at: tempDirectory) } + + let config = CodexBarConfig( + providers: [], + networkProxy: NetworkProxyConfiguration( + enabled: true, + scheme: .socks5, + host: "127.0.0.1", + port: "1080", + username: "codex")) + + try store.save(config) + let loaded = try store.load() + + #expect(loaded?.networkProxy?.enabled == true) + #expect(loaded?.networkProxy?.scheme == .socks5) + #expect(loaded?.networkProxy?.host == "127.0.0.1") + #expect(loaded?.networkProxy?.port == "1080") + #expect(loaded?.networkProxy?.username == "codex") + } + + @Test + func `network proxy validation reports missing host and invalid port`() { + let config = CodexBarConfig( + providers: [], + networkProxy: NetworkProxyConfiguration( + enabled: true, + scheme: .http, + host: " ", + port: "not-a-port", + username: "codex")) + + let issues = CodexBarConfigValidator.validate(config) + #expect(issues.contains(where: { $0.field == "networkProxy.host" && $0.code == "proxy_host_missing" })) + #expect(issues.contains(where: { $0.field == "networkProxy.port" && $0.code == "proxy_port_invalid" })) + } } From b8c4f8216f02ed789e046605a74aaa7f4631ab3c Mon Sep 17 00:00:00 2001 From: serezha93 Date: Sun, 17 May 2026 11:05:09 +0300 Subject: [PATCH 3/7] chore: remove superpowers plan doc from repo --- .../plans/2026-05-17-network-proxy-core.md | 289 ------------------ 1 file changed, 289 deletions(-) delete mode 100644 docs/superpowers/plans/2026-05-17-network-proxy-core.md diff --git a/docs/superpowers/plans/2026-05-17-network-proxy-core.md b/docs/superpowers/plans/2026-05-17-network-proxy-core.md deleted file mode 100644 index 92b2e90d1..000000000 --- a/docs/superpowers/plans/2026-05-17-network-proxy-core.md +++ /dev/null @@ -1,289 +0,0 @@ -# Network Proxy Core Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add invisible network proxy configuration support to CodexBar's config model and validation so the next UI PR can build on a stable, testable core. - -**Architecture:** Keep proxy state as a codable value inside `CodexBarConfig`, validate it centrally in `CodexBarConfigValidator`, and rely on the existing `CodexBarConfigStore` save/load path to persist it unchanged. Do not touch transport wiring, password storage, or any Settings UI in this PR. - -**Tech Stack:** Swift 6, Foundation, existing `CodexBarCore` config types, XCTest-style unit tests in `Tests/CodexBarTests`. - ---- - -### Task 1: Add proxy config model to `CodexBarCore` - -**Files:** -- Create: `/Users/sergey-selderey/Projects/CodexBar/Sources/CodexBarCore/Config/NetworkProxyConfiguration.swift` -- Modify: `/Users/sergey-selderey/Projects/CodexBar/Sources/CodexBarCore/Config/CodexBarConfig.swift:1-220` -- Modify: `/Users/sergey-selderey/Projects/CodexBar/Tests/CodexBarTests/ConfigValidationTests.swift` - -- [ ] **Step 1: Write the failing test** - -Add a round-trip test that encodes and decodes a config with proxy data: - -```swift -func testNetworkProxyConfigEncodesAndDecodes() throws { - let config = CodexBarConfig( - providers: [], - networkProxy: NetworkProxyConfiguration( - enabled: true, - scheme: .http, - host: "proxy.example.com", - port: "8080", - username: "codex")) - - let data = try JSONEncoder().encode(config) - let decoded = try JSONDecoder().decode(CodexBarConfig.self, from: data) - - #expect(decoded.networkProxy?.enabled == true) - #expect(decoded.networkProxy?.scheme == .http) - #expect(decoded.networkProxy?.host == "proxy.example.com") - #expect(decoded.networkProxy?.port == "8080") - #expect(decoded.networkProxy?.username == "codex") -} -``` - -- [ ] **Step 2: Run the test and confirm it fails** - -Run: `swift test --filter ConfigValidationTests/testNetworkProxyConfigEncodesAndDecodes` - -Expected: fail because `networkProxy` is not yet part of the config model. - -- [ ] **Step 3: Implement the model** - -Add this type: - -```swift -import Foundation - -public enum NetworkProxyScheme: String, CaseIterable, Codable, Sendable { - case http - case socks5 -} - -public struct NetworkProxyConfiguration: Codable, Sendable, Equatable { - public var enabled: Bool - public var scheme: NetworkProxyScheme - public var host: String - public var port: String - public var username: String - - public init(enabled: Bool, scheme: NetworkProxyScheme, host: String, port: String, username: String) { - self.enabled = enabled - self.scheme = scheme - self.host = host - self.port = port - self.username = username - } - - public var trimmedHost: String { self.host.trimmingCharacters(in: .whitespacesAndNewlines) } - public var trimmedPort: String { self.port.trimmingCharacters(in: .whitespacesAndNewlines) } - public var trimmedUsername: String { self.username.trimmingCharacters(in: .whitespacesAndNewlines) } - - public var resolvedPort: Int? { - guard let port = Int(self.trimmedPort), (1...65535).contains(port) else { return nil } - return port - } - - public var isActive: Bool { - self.enabled && !self.trimmedHost.isEmpty && self.resolvedPort != nil - } -} -``` - -Update `CodexBarConfig` to store: - -```swift -public var networkProxy: NetworkProxyConfiguration? -``` - -and extend its initializer to accept `networkProxy: NetworkProxyConfiguration? = nil`. - -- [ ] **Step 4: Run the test and confirm it passes** - -Run: `swift test --filter ConfigValidationTests/testNetworkProxyConfigEncodesAndDecodes` - -Expected: pass. - -- [ ] **Step 5: Commit** - -```bash -git add Sources/CodexBarCore/Config/NetworkProxyConfiguration.swift Sources/CodexBarCore/Config/CodexBarConfig.swift -git commit -m "feat(proxy): add network proxy config model" -``` - -### Task 2: Validate proxy config centrally - -**Files:** -- Modify: `/Users/sergey-selderey/Projects/CodexBar/Sources/CodexBarCore/Config/CodexBarConfigValidation.swift:1-260` -- Modify: `/Users/sergey-selderey/Projects/CodexBar/Tests/CodexBarTests/ConfigValidationTests.swift` - -- [ ] **Step 1: Write the failing test** - -Add a validation test that asserts missing host and invalid port are reported: - -```swift -func testNetworkProxyValidationReportsMissingHostAndInvalidPort() { - let config = CodexBarConfig( - providers: [], - networkProxy: NetworkProxyConfiguration( - enabled: true, - scheme: .http, - host: " ", - port: "not-a-port", - username: "codex")) - - let issues = CodexBarConfigValidator.validate(config) - #expect(issues.contains(where: { $0.field == "networkProxy.host" && $0.code == "proxy_host_missing" })) - #expect(issues.contains(where: { $0.field == "networkProxy.port" && $0.code == "proxy_port_invalid" })) -} -``` - -- [ ] **Step 2: Run the test and confirm it fails** - -Run: `swift test --filter ConfigValidationTests/testNetworkProxyValidationReportsMissingHostAndInvalidPort` - -Expected: fail because the validator does not yet inspect `networkProxy`. - -- [ ] **Step 3: Implement validation** - -Add a dedicated proxy validation branch to `CodexBarConfigValidator.validate(_:)`: - -```swift -if let proxy = config.networkProxy { - self.validateNetworkProxy(proxy, issues: &issues) -} -``` - -Use exact issue payloads: - -```swift -CodexBarConfigIssue( - severity: .error, - provider: nil, - field: "networkProxy.host", - code: "proxy_host_missing", - message: "Network proxy host is required when proxy is enabled.") -``` - -```swift -CodexBarConfigIssue( - severity: .error, - provider: nil, - field: "networkProxy.port", - code: "proxy_port_invalid", - message: "Network proxy port must be a number between 1 and 65535.") -``` - -Keep validation gated behind `proxy.enabled` so disabled configs stay quiet. - -- [ ] **Step 4: Run the test and confirm it passes** - -Run: `swift test --filter ConfigValidationTests/testNetworkProxyValidationReportsMissingHostAndInvalidPort` - -Expected: pass. - -- [ ] **Step 5: Commit** - -```bash -git add Sources/CodexBarCore/Config/CodexBarConfigValidation.swift Tests/CodexBarTests/ConfigValidationTests.swift -git commit -m "feat(proxy): validate network proxy config" -``` - -### Task 3: Verify config store round-trip and keep scope invisible - -**Files:** -- Modify: `/Users/sergey-selderey/Projects/CodexBar/Tests/CodexBarTests/ConfigValidationTests.swift` - -- [ ] **Step 1: Write the failing test** - -Add a store round-trip test that writes a config with proxy settings and reloads it from disk: - -```swift -func testConfigStoreRoundTripsNetworkProxy() throws { - let tempDirectory = FileManager.default.temporaryDirectory - .appendingPathComponent(UUID().uuidString, isDirectory: true) - let store = CodexBarConfigStore(fileURL: tempDirectory.appendingPathComponent("config.json")) - defer { try? FileManager.default.removeItem(at: tempDirectory) } - - let config = CodexBarConfig( - providers: [], - networkProxy: NetworkProxyConfiguration( - enabled: true, - scheme: .socks5, - host: "127.0.0.1", - port: "1080", - username: "codex")) - - try store.save(config) - let loaded = try store.load() - - #expect(loaded?.networkProxy?.scheme == .socks5) - #expect(loaded?.networkProxy?.host == "127.0.0.1") - #expect(loaded?.networkProxy?.port == "1080") -} -``` - -- [ ] **Step 2: Run the test and confirm it fails** - -Run: `swift test --filter ConfigValidationTests/testConfigStoreRoundTripsNetworkProxy` - -Expected: fail until the config model persists `networkProxy`. - -- [ ] **Step 3: Keep the store implementation unchanged unless the test proves otherwise** - -`CodexBarConfigStore` should already serialize and deserialize the new optional field once the model includes it. Do not add migration logic, password handling, or UI side effects. - -- [ ] **Step 4: Run the test and confirm it passes** - -Run: `swift test --filter ConfigValidationTests/testConfigStoreRoundTripsNetworkProxy` - -Expected: pass. - -- [ ] **Step 5: Commit** - -```bash -git add Tests/CodexBarTests/ConfigValidationTests.swift -git commit -m "test(proxy): cover config round-trip" -``` - -### Task 4: Final verification and PR prep - -**Files:** -- Review: `/Users/sergey-selderey/Projects/CodexBar/Sources/CodexBarCore/Config/CodexBarConfig.swift` -- Review: `/Users/sergey-selderey/Projects/CodexBar/Sources/CodexBarCore/Config/CodexBarConfigValidation.swift` -- Review: `/Users/sergey-selderey/Projects/CodexBar/Tests/CodexBarTests/ConfigValidationTests.swift` - -- [ ] **Step 1: Run the focused test set** - -Run: - -```bash -swift test --filter ConfigValidationTests -swift test --filter ConfigValidationTests/testConfigStoreRoundTripsNetworkProxy -swift test --filter ConfigValidationTests/testNetworkProxyConfigEncodesAndDecodes -``` - -Expected: all pass. - -- [ ] **Step 2: Sanity-check the diff** - -Run: - -```bash -git diff --check -git status --short -``` - -Expected: no whitespace errors and only the intended files changed. - -- [ ] **Step 3: Commit the branch state** - -```bash -git add Sources/CodexBarCore/Config/CodexBarConfig.swift Sources/CodexBarCore/Config/CodexBarConfigValidation.swift Tests/CodexBarTests/ConfigValidationTests.swift -git commit -m "feat(proxy): add invisible proxy config core" -``` - -- [ ] **Step 4: Prepare the follow-up visible PR** - -Write a short PR description that explicitly says this is only the invisible config core and that Settings UI and transport wiring will follow in a separate PR. From b3a9217183177d205a63d6bb99fbdd08792232c9 Mon Sep 17 00:00:00 2001 From: serezha93 Date: Sun, 17 May 2026 20:23:50 +0300 Subject: [PATCH 4/7] Fix Linux provider HTTP transport --- Sources/CodexBarCore/ProviderHTTPClient.swift | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/Sources/CodexBarCore/ProviderHTTPClient.swift b/Sources/CodexBarCore/ProviderHTTPClient.swift index 1c8a3f85a..81e669051 100644 --- a/Sources/CodexBarCore/ProviderHTTPClient.swift +++ b/Sources/CodexBarCore/ProviderHTTPClient.swift @@ -7,19 +7,11 @@ public protocol ProviderHTTPTransport: Sendable { func data(for request: URLRequest) async throws -> (Data, URLResponse) } -#if !os(Linux) -extension URLSession: ProviderHTTPTransport {} +#if os(Linux) +extension URLSession: @unchecked Sendable {} #endif -extension URLSession { - public func response(for request: URLRequest) async throws -> ProviderHTTPResponse { - let (data, response) = try await self.data(for: request) - guard let httpResponse = response as? HTTPURLResponse else { - throw URLError(.badServerResponse) - } - return ProviderHTTPResponse(data: data, response: httpResponse) - } -} +extension URLSession: ProviderHTTPTransport {} public struct ProviderHTTPResponse: Sendable { public let data: Data From de1a53294ae8cca5a38c7a35e7f41ad94634bbe7 Mon Sep 17 00:00:00 2001 From: serezha93 Date: Mon, 18 May 2026 00:35:58 +0300 Subject: [PATCH 5/7] Relax FoundationNetworking concurrency import --- Sources/CodexBarCore/ProviderHTTPClient.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Sources/CodexBarCore/ProviderHTTPClient.swift b/Sources/CodexBarCore/ProviderHTTPClient.swift index 81e669051..7fbfb7607 100644 --- a/Sources/CodexBarCore/ProviderHTTPClient.swift +++ b/Sources/CodexBarCore/ProviderHTTPClient.swift @@ -1,16 +1,12 @@ import Foundation #if canImport(FoundationNetworking) -import FoundationNetworking +@preconcurrency import FoundationNetworking #endif public protocol ProviderHTTPTransport: Sendable { func data(for request: URLRequest) async throws -> (Data, URLResponse) } -#if os(Linux) -extension URLSession: @unchecked Sendable {} -#endif - extension URLSession: ProviderHTTPTransport {} public struct ProviderHTTPResponse: Sendable { From 0d3028a7fecacfeb7d3e356432e7de29de7c030f Mon Sep 17 00:00:00 2001 From: serezha93 Date: Tue, 19 May 2026 00:16:57 +0300 Subject: [PATCH 6/7] Relax provider transport concurrency --- Sources/CodexBarCore/ProviderHTTPClient.swift | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Sources/CodexBarCore/ProviderHTTPClient.swift b/Sources/CodexBarCore/ProviderHTTPClient.swift index 7fbfb7607..f3e3cd3de 100644 --- a/Sources/CodexBarCore/ProviderHTTPClient.swift +++ b/Sources/CodexBarCore/ProviderHTTPClient.swift @@ -3,12 +3,10 @@ import Foundation @preconcurrency import FoundationNetworking #endif -public protocol ProviderHTTPTransport: Sendable { +@preconcurrency public protocol ProviderHTTPTransport: Sendable { func data(for request: URLRequest) async throws -> (Data, URLResponse) } -extension URLSession: ProviderHTTPTransport {} - public struct ProviderHTTPResponse: Sendable { public let data: Data public let response: HTTPURLResponse @@ -23,7 +21,19 @@ public struct ProviderHTTPResponse: Sendable { } } -public struct ProviderHTTPTransportHandler: ProviderHTTPTransport { +extension URLSession { + public func response(for request: URLRequest) async throws -> ProviderHTTPResponse { + let (data, response) = try await self.data(for: request) + guard let httpResponse = response as? HTTPURLResponse else { + throw URLError(.badServerResponse) + } + return ProviderHTTPResponse(data: data, response: httpResponse) + } +} + +extension URLSession: ProviderHTTPTransport {} + +public struct ProviderHTTPTransportHandler: ProviderHTTPTransport, Sendable { private let handler: @Sendable (URLRequest) async throws -> (Data, URLResponse) public init(_ handler: @escaping @Sendable (URLRequest) async throws -> (Data, URLResponse)) { From 29210244e3385e8bce6ed50cd3a60c29f901d768 Mon Sep 17 00:00:00 2001 From: serezha93 Date: Tue, 19 May 2026 00:26:24 +0300 Subject: [PATCH 7/7] Add Linux URLSession async shim --- Sources/CodexBarCore/ProviderHTTPClient.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Sources/CodexBarCore/ProviderHTTPClient.swift b/Sources/CodexBarCore/ProviderHTTPClient.swift index f3e3cd3de..08d5c67ef 100644 --- a/Sources/CodexBarCore/ProviderHTTPClient.swift +++ b/Sources/CodexBarCore/ProviderHTTPClient.swift @@ -7,6 +7,27 @@ import Foundation func data(for request: URLRequest) async throws -> (Data, URLResponse) } +#if canImport(FoundationNetworking) +extension URLSession { + public func data(for request: URLRequest) async throws -> (Data, URLResponse) { + try await withCheckedThrowingContinuation { continuation in + let task = self.dataTask(with: request) { data, response, error in + if let error { + continuation.resume(throwing: error) + return + } + guard let data, let response else { + continuation.resume(throwing: URLError(.badServerResponse)) + return + } + continuation.resume(returning: (data, response)) + } + task.resume() + } + } +} +#endif + public struct ProviderHTTPResponse: Sendable { public let data: Data public let response: HTTPURLResponse