diff --git a/CHANGELOG.md b/CHANGELOG.md index eee2a6e..9149a7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ ### Changed +- Adopt swift-codex 0.5.1 and MCP SDK fork 0.13.2-computer-mcp.1, preserving + existing runtime behavior and refreshing the SDK-derived method inventory. +- Classify native plugin reconciliation as destructive, Bedrock discovery as + read-only, and Bedrock setup as an external write. +- Ship swift-codex's Apache-2.0 WITH Swift-exception license and notices. - Adapter databases initialize from one complete schema. Unknown migration histories fail explicitly and require a new state directory. @@ -22,4 +27,6 @@ ### Fixed +- Undecodable MCP tool arguments, including out-of-range integers, receive + JSON-RPC invalid-params errors correlated with the request ID. - Valid upstream Exec session identifiers are accepted by release builds. diff --git a/Documentation/Reference/Release.md b/Documentation/Reference/Release.md index 266c50a..e102bd3 100644 --- a/Documentation/Reference/Release.md +++ b/Documentation/Reference/Release.md @@ -45,8 +45,8 @@ requirements are: | Dependency | Requirement | | --- | --- | -| MCP Swift SDK organization fork (`computer-mcp/swift-sdk`) | `exact: "0.13.1-computer-mcp.1"` | -| swift-codex | `exact: "0.4.1"` | +| MCP Swift SDK organization fork (`computer-mcp/swift-sdk`) | `exact: "0.13.2-computer-mcp.1"` | +| swift-codex | `exact: "0.5.1"` | | swift-subprocess | `.upToNextMinor(from: "1.0.2")` | | swift-argument-parser | `from: "1.8.2"` | | GRDB | `from: "7.11.1"` | diff --git a/Package.resolved b/Package.resolved index 421237c..03ff6bb 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "d140872fb02b7e4c197a7bb62bce1caab4f2de6b609de7fb3d887dea098ca31d", + "originHash" : "c971c1b3c7b824696af67bc95c33746b8e9b01a810924c0e3ed165e93b8c3112", "pins" : [ { "identity" : "async-http-client", @@ -150,8 +150,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swift-library/swift-codex.git", "state" : { - "revision" : "35e4a3dc07e60f084efb8a06424a8315c2ab6f64", - "version" : "0.4.1" + "revision" : "827e750a265d46dc062eb99015e532100bb09975", + "version" : "0.5.1" } }, { @@ -285,8 +285,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/computer-mcp/swift-sdk.git", "state" : { - "revision" : "61914e07e36440e0a4ee08d9f4644f20e4fa41f4", - "version" : "0.13.1-computer-mcp.1" + "revision" : "0603094e1c1a6fe249ebb1a1fd71c499cc2c55a3", + "version" : "0.13.2-computer-mcp.1" } }, { diff --git a/Package.swift b/Package.swift index 6e1c32c..09455d4 100644 --- a/Package.swift +++ b/Package.swift @@ -9,8 +9,8 @@ let package = Package( dependencies: [ .package(url: "https://github.com/groue/GRDB.swift.git", from: "7.11.1"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.8.2"), - .package(url: "https://github.com/computer-mcp/swift-sdk.git", exact: "0.13.1-computer-mcp.1"), - .package(url: "https://github.com/swift-library/swift-codex.git", exact: "0.4.1"), + .package(url: "https://github.com/computer-mcp/swift-sdk.git", exact: "0.13.2-computer-mcp.1"), + .package(url: "https://github.com/swift-library/swift-codex.git", exact: "0.5.1"), .package( url: "https://github.com/swiftlang/swift-subprocess.git", .upToNextMinor(from: "1.0.2")), ], diff --git a/Sources/CodexAdapter/CodexAppServerMethodCatalog.swift b/Sources/CodexAdapter/CodexAppServerMethodCatalog.swift index e5157dd..39ae24a 100644 --- a/Sources/CodexAdapter/CodexAppServerMethodCatalog.swift +++ b/Sources/CodexAdapter/CodexAppServerMethodCatalog.swift @@ -125,7 +125,8 @@ enum CodexAppServerMethodCatalog { static func risk(for method: String) -> CodexOperationRisk? { switch method { - case "account/rateLimits/read", "account/read", "account/usage/read", + case "account/bedrock/discover", "account/rateLimits/read", "account/read", + "account/usage/read", "account/workspaceMessages/read", "app/installed", "app/list", "app/read", "config/read", "configRequirements/read", "experimentalFeature/list", "externalAgentConfig/detect", "externalAgentConfig/import/readHistories", "fs/getMetadata", "fs/readDirectory", @@ -146,7 +147,7 @@ enum CodexAppServerMethodCatalog { "thread/increment_elicitation", "thread/memoryMode/set", "thread/queue/delete", "thread/queue/reorder", "command/exec/resize", "process/resizePty": return .workspaceWrite - case "account/login/cancel", "account/login/start", "account/logout", + case "account/bedrock/setup", "account/login/cancel", "account/login/start", "account/logout", "account/rateLimitResetCredit/consume", "account/sendAddCreditsNudgeEmail", "config/batchWrite", "config/value/write", "experimentalFeature/enablement/set", "externalAgentConfig/import", "externalAgentConfig/import/recordHistory", "feedback/upload", @@ -158,7 +159,8 @@ enum CodexAppServerMethodCatalog { "remoteControl/enable", "remoteControl/pairing/start", "mcpServer/event/stream/start", "mcpServer/event/stream/stop": return .externalWrite - case "fs/remove", "marketplace/remove", "plugin/share/delete", "plugin/uninstall", + case "fs/remove", "marketplace/remove", "plugin/reconcile", "plugin/share/delete", + "plugin/uninstall", "thread/revert", "threadSection/delete", "memory/reset", "project/delete", "remoteControl/client/revoke", "thread/backgroundTerminals/clean", "thread/backgroundTerminals/terminate", "command/exec/terminate", "process/kill": diff --git a/Sources/CodexAdapter/Resources/Protocol/adoption.json b/Sources/CodexAdapter/Resources/Protocol/adoption.json index 58ee5d3..7418f43 100644 --- a/Sources/CodexAdapter/Resources/Protocol/adoption.json +++ b/Sources/CodexAdapter/Resources/Protocol/adoption.json @@ -55,6 +55,7 @@ "plugin/installed", "plugin/list", "plugin/read", + "plugin/reconcile", "plugin/share/checkout", "plugin/share/delete", "plugin/share/list", @@ -101,6 +102,8 @@ "windowsSandbox/setupStart" ], "experimental": [ + "account/bedrock/discover", + "account/bedrock/setup", "collaborationMode/list", "environment/add", "environment/info", @@ -155,22 +158,66 @@ ] }, "excluded": [ - { "method": "FuzzyFileSearch", "reason": "legacy method is not adopted" }, - { "method": "GetAuthStatus", "reason": "legacy method is not adopted" }, - { "method": "GetConversationSummary", "reason": "legacy method is not adopted" }, - { "method": "GitDiffToRemote", "reason": "legacy method is not adopted" }, - { "method": "account/bedrock/discover", "reason": "Bedrock account onboarding is not adopted" }, - { "method": "account/bedrock/setup", "reason": "Bedrock account onboarding is not adopted" }, - { "method": "fuzzyFileSearch", "reason": "deprecated upstream method is not adopted" }, - { "method": "fuzzyFileSearch/sessionStart", "reason": "experimental fuzzy session is not adopted" }, - { "method": "fuzzyFileSearch/sessionStop", "reason": "experimental fuzzy session is not adopted" }, - { "method": "fuzzyFileSearch/sessionUpdate", "reason": "experimental fuzzy session is not adopted" }, - { "method": "initialize", "reason": "connection lifecycle owns the handshake" }, - { "method": "initialized", "reason": "connection lifecycle owns the handshake" }, - { "method": "plugin/reconcile", "reason": "plugin reconciliation is not adopted" }, - { "method": "userVerification/delete", "reason": "user verification administration is not adopted" }, - { "method": "userVerification/enroll", "reason": "user verification administration is not adopted" }, - { "method": "userVerification/status", "reason": "user verification administration is not adopted" }, - { "method": "userVerification/verify", "reason": "user verification administration is not adopted" } - ] + { + "method": "FuzzyFileSearch", + "reason": "legacy method is not adopted" + }, + { + "method": "GetAuthStatus", + "reason": "legacy method is not adopted" + }, + { + "method": "GetConversationSummary", + "reason": "legacy method is not adopted" + }, + { + "method": "GitDiffToRemote", + "reason": "legacy method is not adopted" + }, + { + "method": "fuzzyFileSearch", + "reason": "deprecated upstream method is not adopted" + }, + { + "method": "fuzzyFileSearch/sessionStart", + "reason": "experimental fuzzy session is not adopted" + }, + { + "method": "fuzzyFileSearch/sessionStop", + "reason": "experimental fuzzy session is not adopted" + }, + { + "method": "fuzzyFileSearch/sessionUpdate", + "reason": "experimental fuzzy session is not adopted" + }, + { + "method": "initialize", + "reason": "connection lifecycle owns the handshake" + }, + { + "method": "initialized", + "reason": "connection lifecycle owns the handshake" + }, + { + "method": "userVerification/delete", + "reason": "user verification administration is not adopted" + }, + { + "method": "userVerification/enroll", + "reason": "user verification administration is not adopted" + }, + { + "method": "userVerification/status", + "reason": "user verification administration is not adopted" + }, + { + "method": "userVerification/verify", + "reason": "user verification administration is not adopted" + } + ], + "adoptedServerRequests": { + "experimental": [ + "currentTime/read" + ] + } } diff --git a/Sources/CodexAdapter/Resources/Protocol/receipt.json b/Sources/CodexAdapter/Resources/Protocol/receipt.json index 5c63d29..f49a32d 100644 --- a/Sources/CodexAdapter/Resources/Protocol/receipt.json +++ b/Sources/CodexAdapter/Resources/Protocol/receipt.json @@ -1,14 +1,14 @@ { "codexVersion": "0.154.0", "sdk": { - "version": "0.4.1", - "revision": "35e4a3dc07e60f084efb8a06424a8315c2ab6f64" + "version": "0.5.1", + "revision": "827e750a265d46dc062eb99015e532100bb09975" }, "upstream": { "tag": "rust-v0.154.0", "commit": "6b9826e3aa83b1a5947db50f4332cb9c65f1b340" }, - "adoptionSHA256": "a4b439ce69cb5993b2907c480fe7caf338a6e15dab2892746c0ad320cff794c3", + "adoptionSHA256": "0a60742f6b7dfe11881f4f2d26233be55414798d3af2c240c31b2f6a56de0bd1", "files": { "stable/ClientRequest.json": { "sha256": "da767fedd73502ceb644f5d7974927e1a6b7f1917b092aaa029e81949abf0125", diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 623097b..c99e945 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -3,8 +3,12 @@ The adapter's original and extracted integration code is Computer MCP code licensed under this repository's [LICENSE](LICENSE), FSL-1.1-ALv2. -The MCP Swift SDK transport fork and swift-codex are MIT-licensed. Swift Subprocess -and other Swift dependencies retain their own upstream licenses. The packaging +| Component | Selected release | License | +| --- | --- | --- | +| swift-codex | 0.5.1 | Apache-2.0 WITH Swift-exception | +| MCP Swift SDK transport fork (`computer-mcp/swift-sdk`) | 0.13.2-computer-mcp.1 | Apache-2.0 and retained MIT contributions, with CC-BY-4.0 documentation, under the upstream licensing transition | + +Swift Subprocess and other Swift dependencies retain their own upstream licenses. The packaging script copies license and notice files from each pinned SwiftPM checkout into `ThirdPartyNotices/`, together with the unchanged `Package.resolved`. This is a resolved-dependency inventory, not a claim that every resolved product is linked. diff --git a/Tests/CodexAdapterTests/CodexAppServerProviderTests.swift b/Tests/CodexAdapterTests/CodexAppServerProviderTests.swift index c52ab31..ed61774 100644 --- a/Tests/CodexAdapterTests/CodexAppServerProviderTests.swift +++ b/Tests/CodexAdapterTests/CodexAppServerProviderTests.swift @@ -294,7 +294,7 @@ struct CodexAppServerProviderTests { @Test func experimentalCallsRequireExplicitAdmissionAndRemainDistinctFromStableTools() async throws { let provider = makeProvider() - #expect(provider.tools.filter { $0.name.hasPrefix("codex.app.native.") }.count == 96) + #expect(provider.tools.filter { $0.name.hasPrefix("codex.app.native.") }.count == 97) #expect(!provider.tools.contains { $0.name == "codex.app.native.mock.experimentalMethod" }) await #expect(throws: CodexToolError.self) { try await provider.call( diff --git a/Tests/CodexAdapterTests/CodexAppServerRuntimeTests.swift b/Tests/CodexAdapterTests/CodexAppServerRuntimeTests.swift index ca88c0f..d1ac1dd 100644 --- a/Tests/CodexAdapterTests/CodexAppServerRuntimeTests.swift +++ b/Tests/CodexAdapterTests/CodexAppServerRuntimeTests.swift @@ -1037,6 +1037,17 @@ final class CodexAppServerRuntimeTests { request.payload.objectValue?["params"]?.objectValue?["message"] == .string("Use token=[REDACTED] to continue") ) + let params = try #require(request.payload.objectValue?["params"]) + let native = try JSONDecoder().decode( + CodexAppServerProtocol.Stable.McpServerElicitationRequestParams.self, + from: JSONEncoder().encode(params)) + guard case .mcpserverelicitationrequestparamsoption4(let scoped) = native else { + Issue.record("Expected a URL elicitation request.") + return + } + #expect(scoped.serverName == "fixture-mcp") + #expect(scoped.threadId == "thread_fixture") + #expect(scoped.turnId == "turn-fixture") _ = try await runtime.respond( requestID: request.id, response: .object(["action": .string("decline")]) @@ -1774,6 +1785,9 @@ struct AppServerProcessFixture { "elicitationId": .string("elicitation-fixture"), "message": .string(message), "mode": .string("url"), + "serverName": .string("fixture-mcp"), + "threadId": .string("thread_fixture"), + "turnId": .string("turn-fixture"), "url": .string("https://example.invalid/authorize"), ]), ]) diff --git a/Tests/CodexAdapterTests/CodexNativeProjectionTests.swift b/Tests/CodexAdapterTests/CodexNativeProjectionTests.swift index 9ac0867..93d62fe 100644 --- a/Tests/CodexAdapterTests/CodexNativeProjectionTests.swift +++ b/Tests/CodexAdapterTests/CodexNativeProjectionTests.swift @@ -43,14 +43,35 @@ struct CodexNativeProjectionTests { #expect(Set(actual.map(\.method)) == Set(inventory.adoption.adopted[channel.rawValue] ?? [])) #expect(actual.allSatisfy { $0.risk == CodexAppServerMethodCatalog.risk(for: $0.method) }) } - #expect(methods.filter { $0.channel == .stable }.count == 96) - #expect(methods.filter { $0.channel == .experimental }.count == 51) + #expect(methods.filter { $0.channel == .stable }.count == 97) + #expect(methods.filter { $0.channel == .experimental }.count == 53) #expect(Set(methods.map(\.toolName)).count == methods.count) for exclusion in inventory.adoption.excluded { #expect(!methods.contains { $0.method == exclusion.method }) } } + @Test(arguments: [ + ( + "account/bedrock/discover", CodexOperationRisk.readOnly, + ProtocolInventory.Channel.experimental + ), + ("account/bedrock/setup", .externalWrite, .experimental), + ("plugin/reconcile", .destructive, .stable), + ]) + func adoptedMethodEffectsDefineToolAnnotations( + method: String, risk: CodexOperationRisk, channel: ProtocolInventory.Channel + ) throws { + let descriptor = try #require(CodexAppServerMethodCatalog.method(named: method)) + #expect(descriptor.channel == channel) + #expect(descriptor.risk == risk) + #expect(descriptor.tool._meta?["io.github.computer-mcp/risk"] == .string(risk.rawValue)) + #expect(descriptor.tool.annotations.readOnlyHint == (risk == .readOnly)) + #expect(descriptor.tool.annotations.destructiveHint == (risk == .destructive)) + #expect(descriptor.tool.annotations.idempotentHint == (risk == .readOnly)) + #expect(descriptor.tool.annotations.openWorldHint == true) + } + @Test func schemasPreserveNativeFieldsAndReferences() throws { let method = try #require(CodexAppServerMethodCatalog.method(named: "turn/start")) let schema = try #require(method.tool.inputSchema.objectValue) @@ -91,15 +112,20 @@ struct CodexNativeProjectionTests { try write.validate(params: .object(["path": .string("/tmp/file"), "dataBase64": .string("")])) } - @Test func sdkAdoptionCannotAddAnUnclassifiedMethod() throws { + @Test(arguments: ProtocolInventory.Channel.allCases) + func sdkAdoptionCannotAddAnUnclassifiedMethod(channel: ProtocolInventory.Channel) throws { let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: directory) } let request = Data( #"{"oneOf":[{"properties":{"method":{"enum":["future/new"]}},"required":["method"]}]}"#.utf8) - let adoption = Data( - #"{"schema":"swift-codex.codex-app-server-method-adoption.v1","upstreamTag":"rust-v1.2.3","adopted":{"stable":["future/new"],"experimental":[]},"excluded":[]}"# - .utf8) + let adoption = try JSONSerialization.data(withJSONObject: [ + "schema": "swift-codex.codex-app-server-method-adoption.v1", "upstreamTag": "rust-v1.2.3", + "adopted": Dictionary( + uniqueKeysWithValues: ProtocolInventory.Channel.allCases.map { + ($0.rawValue, $0 == channel ? ["future/new"] : []) + }), "excluded": [], + ]) var files: [String: Any] = [:] for channel in ProtocolInventory.Channel.allCases { try FileManager.default.createDirectory( @@ -115,7 +141,9 @@ struct CodexNativeProjectionTests { "codexVersion": "1.2.3", "adoptionSHA256": AppServerSchema.digest(adoption), "files": files, ]).write(to: directory.appendingPathComponent("receipt.json")) let inventory = try ProtocolInventory(directory: directory) - #expect(throws: SchemaError.self) { + #expect(CodexAppServerMethodCatalog.risk(for: "future/new") == nil) + #expect(throws: SchemaError.invalid("Adopted SDK method lacks projection policy: future/new.")) + { try CodexAppServerMethodCatalog.derive(inventory: inventory) } } diff --git a/Tests/CodexAdapterTests/JSONIntegerTests.swift b/Tests/CodexAdapterTests/JSONIntegerTests.swift index 9f81c5f..2f16eb3 100644 --- a/Tests/CodexAdapterTests/JSONIntegerTests.swift +++ b/Tests/CodexAdapterTests/JSONIntegerTests.swift @@ -29,6 +29,77 @@ struct JSONIntegerTests { } } + @Test( + .timeLimit(.minutes(1)), + arguments: ["9223372036854775808", "-9223372036854775809", "18446744073709551615"]) + func servedStdioCorrelatesIntegerDecodingErrorsAndRemainsUsable(text: String) async throws { + let input = Pipe() + let output = Pipe() + defer { + for pipe in [input, output] { + try? pipe.fileHandleForReading.close() + try? pipe.fileHandleForWriting.close() + } + } + let server = StdioTransport( + input: .init(rawValue: input.fileHandleForReading.fileDescriptor), + output: .init(rawValue: output.fileHandleForWriting.fileDescriptor)) + let peer = StdioTransport( + input: .init(rawValue: output.fileHandleForReading.fileDescriptor), + output: .init(rawValue: input.fileHandleForWriting.fileDescriptor)) + let serving = Task { try await CodexAdapterServer.serve(transport: server) } + do { + try await peer.connect() + var messages = await peer.receive().makeAsyncIterator() + try await peer.send( + Data( + """ + {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"\(Version.latest)","capabilities":{},"clientInfo":{"name":"integer-tests","version":"1"}}} + """.utf8)) + let initialized = try JSONDecoder().decode( + MCP.Value.self, from: #require(try await messages.next())) + #expect(initialized.objectValue?["id"] == .int(0)) + #expect(initialized.objectValue?["result"] != nil) + try await peer.send(Data(#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#.utf8)) + + for id in [MCP.Value.int(9_007_199_254_740_993), .string("9007199254740993")] { + let wireID = String(decoding: try JSONEncoder().encode(id), as: UTF8.self) + try await peer.send( + Data( + """ + {"jsonrpc":"2.0","id":\(wireID),"method":"tools/call","params":{"name":"codex.protocol.methods.list","arguments":{"limit":\(text)}}} + """.utf8)) + let error = try JSONDecoder().decode( + MCP.Value.self, from: #require(try await messages.next())) + #expect(error.objectValue?["jsonrpc"] == .string("2.0")) + #expect(error.objectValue?["id"] == id) + #expect(error.objectValue?["error"]?.objectValue?["code"] == .int(-32602)) + #expect(error.objectValue?["result"] == nil) + + try await peer.send( + Data( + """ + {"jsonrpc":"2.0","id":\(wireID),"method":"tools/call","params":{"name":"codex.protocol.methods.list","arguments":{"limit":1}}} + """.utf8)) + let response = try JSONDecoder().decode( + MCP.Value.self, from: #require(try await messages.next())) + #expect(response.objectValue?["id"] == id) + #expect(response.objectValue?["error"] == nil) + let result = try #require(response.objectValue?["result"]?.objectValue) + #expect(result["isError"] == .bool(false)) + #expect(result["structuredContent"]?.objectValue?["methods"]?.arrayValue?.count == 1) + } + await peer.disconnect() + await server.disconnect() + try await serving.value + } catch { + await peer.disconnect() + await server.disconnect() + _ = try? await serving.value + throw error + } + } + @Test(arguments: [Int64.min, 9_007_199_254_740_993, Int64.max]) func sdkDecimalStoragePreservesIntegralValues(value: Int64) throws { let native = AppServerJSON.object(["nested": .array([.number(.decimal(Decimal(value)))])]) diff --git a/Tests/test_package.py b/Tests/test_package.py index 67b1da5..7c7318e 100644 --- a/Tests/test_package.py +++ b/Tests/test_package.py @@ -46,7 +46,8 @@ def make_repository(self, root): checkout = repo / ".build/checkouts/swift-codex" schema = checkout / "Vendor/CodexAppServerProtocolSchema" schema.mkdir(parents=True) - (checkout / "LICENSE").write_text("fixture dependency license") + (checkout / "LICENSE.txt").write_text("fixture dependency license") + (checkout / "NOTICE").write_text("fixture dependency notice") for name in ("LICENSE", "NOTICE"): (schema / name).write_text("fixture schema notice") return repo, built @@ -83,6 +84,10 @@ def command(arguments, cwd): result = package.package(root / "output", "debug") with zipfile.ZipFile(result["archive"]) as archive: self.assertEqual(archive.read("computer-mcp-plugin.toml"), manifest) + for name in ("LICENSE.txt", "NOTICE"): + original = (repo / ".build/checkouts/swift-codex" / name).read_bytes() + self.assertEqual(archive.read("ThirdPartyNotices/swift-codex/" + name), original) + self.assertIn(original, archive.read("ThirdPartyNotices.txt")) receipt = json.loads(Path(result["receipt"]).read_text()) self.assertEqual(receipt["architectures"], ["arm64"]) self.assertEqual(receipt["files"]["computer-mcp-plugin.toml"], package.hashlib.sha256(manifest).hexdigest())