diff --git a/.agents/skills/autoreview/scripts/autoreview b/.agents/skills/autoreview/scripts/autoreview index 3ddffc7f229a..2e2b50d49752 100755 --- a/.agents/skills/autoreview/scripts/autoreview +++ b/.agents/skills/autoreview/scripts/autoreview @@ -4767,6 +4767,7 @@ def mask_reference_declaration_evidence(text: str) -> str: def bare_code_reference( text: str, start: int, + end: int, separator: str, value: str, ) -> bool: @@ -4796,6 +4797,31 @@ def bare_code_reference( re.DOTALL, ): return True + # TS/JS named-function parameter annotations use PascalCase type names. + function_prefix = re.search( + r"\bfunction\b[^()\r\n]*\((?P[^()]*)$", + declaration, + ) + annotation_suffix = re.match( + r"[ \t\r\n]*(?P[,)=])", + text[end:], + ) + if ( + function_prefix is not None + and re.fullmatch( + r"\s*(?:\.\.\.\s*)?", + split_top_level_call_arguments( + function_prefix.group("parameters") + )[-1], + ) + and annotation_suffix is not None + ): + if annotation_suffix.group("terminator") != "=": + return True + return not fallback_secret_risk( + text[end + annotation_suffix.end() :], + minimum_length=8, + ) if camel_reference is None and snake_reference is None: return False return bool( @@ -4909,7 +4935,13 @@ def secret_text_risk(text: str) -> bool: return True if ( match.group("bare_value") is not None - and bare_code_reference(text, match.start(), separator, value) + and bare_code_reference( + text, + match.start(), + match.end(), + separator, + value, + ) and safe_secret_assignment_suffix(text, match.end()) ): continue diff --git a/.agents/skills/autoreview/tests/test_autoreview_hardening.py b/.agents/skills/autoreview/tests/test_autoreview_hardening.py index b8debf986d84..fb332ee9243b 100644 --- a/.agents/skills/autoreview/tests/test_autoreview_hardening.py +++ b/.agents/skills/autoreview/tests/test_autoreview_hardening.py @@ -1762,6 +1762,44 @@ class AutoreviewHardeningTests(unittest.TestCase): ) ) + def test_secret_detector_allows_typescript_function_parameter_types(self) -> None: + signature = ( + "function formatCredentialLabel(" + + "credential" + + ": ClaudeCliReadableCredential" + + "): string {" + ) + access_key = "AKIA" + "ABCDEFGHIJKLMNOP" + secret_assignment = "const api" + 'Key = "' + access_key + '";' + literal_value = "actual-production-" + "secret" + parameter_name = "api" + "Key" + type_name = "Api" + "Credential" + typed_default = ( + "function connect(" + + parameter_name + + ": " + + type_name + + ' = "' + + literal_value + + '") {}' + ) + benign_default = ( + "function connect(" + + parameter_name + + ": " + + type_name + + " = defaultCredential) {}" + ) + + self.assertFalse(self.helper["secret_text_risk"](signature)) + self.assertFalse(self.helper["secret_text_risk"](benign_default)) + self.assertTrue( + self.helper["secret_text_risk"]( + signature + "\n " + secret_assignment + "\n}" + ) + ) + self.assertTrue(self.helper["secret_text_risk"](typed_default)) + def test_secret_detector_handles_punctuation_and_multiline_diff_values(self) -> None: value = "Correct-Horse!" + "@Battery$Staple" patch = ( diff --git a/apps/ios/Tests/Logic/WatchChatStatusLocalizationTests.swift b/apps/ios/Tests/Logic/WatchChatStatusLocalizationTests.swift index bd85c6aa43d3..960baf507d4b 100644 --- a/apps/ios/Tests/Logic/WatchChatStatusLocalizationTests.swift +++ b/apps/ios/Tests/Logic/WatchChatStatusLocalizationTests.swift @@ -183,7 +183,7 @@ struct WatchChatStatusLocalizationTests { ], "gatewayConnected": false, "agentName": "Main", - "agentAvatarURL": "https://example.com/avatar.png", + "agentAvatarUrl": "https://example.com/avatar.png", "sessionKey": "main", "talkStatus": [ "code": OpenClawWatchAppStatusCode.talkOff.rawValue, diff --git a/apps/ios/Tests/NodeAppModelInvokeTests.swift b/apps/ios/Tests/NodeAppModelInvokeTests.swift index 52b92a605538..a92c4e9cf8fa 100644 --- a/apps/ios/Tests/NodeAppModelInvokeTests.swift +++ b/apps/ios/Tests/NodeAppModelInvokeTests.swift @@ -6904,6 +6904,7 @@ private func overrideNotificationServingPreference(_ enabled: Bool) -> () -> Voi gatewayStatusText: "Connected", gatewayConnected: true, agentName: "Main", + agentAvatarURL: "https://example.com/avatar.png", sessionKey: "main", gatewayStableID: "gateway-a", talkStatus: OpenClawWatchAppStatus(code: .talkOff), @@ -6941,6 +6942,8 @@ private func overrideNotificationServingPreference(_ enabled: Bool) -> () -> Voi let nestedApprovals = try #require( combined[OpenClawWatchPayloadType.execApprovalSnapshot.rawValue] as? [String: Any]) #expect(nestedApp["gatewayStableID"] as? String == "gateway-a") + #expect(nestedApp["agentAvatarUrl"] as? String == "https://example.com/avatar.png") + #expect(nestedApp["agentAvatarURL"] == nil) let nestedChatStatus = try #require(nestedApp["chatStatus"] as? [String: Any]) #expect(nestedChatStatus["code"] as? String == "chatConnectIPhone") #expect(nestedApp["chatStatusCode"] == nil) diff --git a/apps/ios/WatchApp/Sources/WatchInboxMessages.swift b/apps/ios/WatchApp/Sources/WatchInboxMessages.swift index d9fd587c68e3..21673a0a22da 100644 --- a/apps/ios/WatchApp/Sources/WatchInboxMessages.swift +++ b/apps/ios/WatchApp/Sources/WatchInboxMessages.swift @@ -541,7 +541,7 @@ struct WatchAppSnapshotMessage: Codable, Equatable { let gatewayConnected = Self.boolValue(payload["gatewayConnected"]) let agentName = (payload["agentName"] as? String)? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - let agentAvatarURL = (payload["agentAvatarURL"] as? String)? + let agentAvatarURL = (payload["agentAvatarUrl"] as? String)? .trimmingCharacters(in: .whitespacesAndNewlines) let agentAvatarText = (payload["agentAvatarText"] as? String)? .trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/apps/macos/Sources/OpenClaw/RuntimeLocator.swift b/apps/macos/Sources/OpenClaw/RuntimeLocator.swift index ea107456d53c..d02c8708f14a 100644 --- a/apps/macos/Sources/OpenClaw/RuntimeLocator.swift +++ b/apps/macos/Sources/OpenClaw/RuntimeLocator.swift @@ -53,18 +53,24 @@ enum RuntimeResolutionError: Error { enum RuntimeLocator { private static let logger = Logger(subsystem: "ai.openclaw", category: "runtime") - private static let minNode22 = RuntimeVersion(major: 22, minor: 19, patch: 0) - private static let minNode23 = RuntimeVersion(major: 23, minor: 11, patch: 0) - private static let supportedNodeRange = ">=22.19.0 <23 or >=23.11.0" + // Keep these floors aligned with package.json engines so the app never launches + // the gateway on an unsupported odd release or an older even-major runtime. + private static let minNode22 = RuntimeVersion(major: 22, minor: 22, patch: 3) + private static let minNode24 = RuntimeVersion(major: 24, minor: 15, patch: 0) + private static let minNode25 = RuntimeVersion(major: 25, minor: 9, patch: 0) + private static let supportedNodeRange = ">=22.22.3 <23, >=24.15.0 <25, or >=25.9.0" static func isSupportedNodeVersion(_ version: RuntimeVersion) -> Bool { if version.major == self.minNode22.major { return version >= self.minNode22 } - if version.major == self.minNode23.major { - return version >= self.minNode23 + if version.major == self.minNode24.major { + return version >= self.minNode24 } - return version.major > self.minNode23.major + if version.major == self.minNode25.major { + return version >= self.minNode25 + } + return version.major > self.minNode25.major } static func resolve( diff --git a/apps/macos/Tests/OpenClawIPCTests/CommandResolverTests.swift b/apps/macos/Tests/OpenClawIPCTests/CommandResolverTests.swift index 8d38b8026ed6..888ab9d9b67e 100644 --- a/apps/macos/Tests/OpenClawIPCTests/CommandResolverTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/CommandResolverTests.swift @@ -67,7 +67,7 @@ import Testing let nodePath = tmp.appendingPathComponent("node_modules/.bin/node") let scriptPath = tmp.appendingPathComponent("bin/openclaw.js") try makeExecutableForTests(at: nodePath) - try "#!/bin/sh\necho v22.19.0\n".write(to: nodePath, atomically: true, encoding: .utf8) + try "#!/bin/sh\necho v22.22.3\n".write(to: nodePath, atomically: true, encoding: .utf8) try FileManager().setAttributes([.posixPermissions: 0o755], ofItemAtPath: nodePath.path) try makeExecutableForTests(at: scriptPath) @@ -162,7 +162,7 @@ import Testing let binDir = tmp.appendingPathComponent("bin") let nodePath = binDir.appendingPathComponent("node") try makeExecutableForTests(at: nodePath) - try "#!/bin/sh\necho v22.19.0\n".write(to: nodePath, atomically: true, encoding: .utf8) + try "#!/bin/sh\necho v22.22.3\n".write(to: nodePath, atomically: true, encoding: .utf8) try FileManager().setAttributes([.posixPermissions: 0o755], ofItemAtPath: nodePath.path) let cmd = CommandResolver.openclawCommand( @@ -215,7 +215,7 @@ import Testing @Test func `node manager runtimes precede system runtimes`() throws { let home = try makeTempDirForTests() - let nodeManagerBin = home.appendingPathComponent(".nvm/versions/node/v22.19.0/bin") + let nodeManagerBin = home.appendingPathComponent(".nvm/versions/node/v22.22.3/bin") try makeExecutableForTests(at: nodeManagerBin.appendingPathComponent("node")) let paths = CommandResolver.preferredPaths( diff --git a/apps/macos/Tests/OpenClawIPCTests/RuntimeLocatorTests.swift b/apps/macos/Tests/OpenClawIPCTests/RuntimeLocatorTests.swift index 968f6c03e9ac..a022e6d4bbb2 100644 --- a/apps/macos/Tests/OpenClawIPCTests/RuntimeLocatorTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/RuntimeLocatorTests.swift @@ -16,7 +16,7 @@ struct RuntimeLocatorTests { @Test func `resolve succeeds with valid node`() throws { let script = """ #!/bin/sh - echo v22.19.0 + echo v22.22.3 """ let node = try self.makeTempExecutable(contents: script) let result = RuntimeLocator.resolve(searchPaths: [node.deletingLastPathComponent().path]) @@ -25,13 +25,13 @@ struct RuntimeLocatorTests { return } #expect(res.path == node.path) - #expect(res.version == RuntimeVersion(major: 22, minor: 19, patch: 0)) + #expect(res.version == RuntimeVersion(major: 22, minor: 22, patch: 3)) } @Test func `resolve fails on boundary below minimum`() throws { let script = """ #!/bin/sh - echo v22.18.9 + echo v22.22.2 """ let node = try self.makeTempExecutable(contents: script) let result = RuntimeLocator.resolve(searchPaths: [node.deletingLastPathComponent().path]) @@ -39,38 +39,38 @@ struct RuntimeLocatorTests { Issue.record("Expected unsupported error, got \(result)") return } - #expect(found == RuntimeVersion(major: 22, minor: 18, patch: 9)) + #expect(found == RuntimeVersion(major: 22, minor: 22, patch: 2)) #expect(path == node.path) } - @Test func `resolve rejects early node 23`() throws { - let script = """ - #!/bin/sh - echo v23.7.0 - """ - let node = try self.makeTempExecutable(contents: script) - let result = RuntimeLocator.resolve(searchPaths: [node.deletingLastPathComponent().path]) - guard case let .failure(.unsupported(_, found, path, _)) = result else { - Issue.record("Expected unsupported error, got \(result)") - return - } - #expect(found == RuntimeVersion(major: 23, minor: 7, patch: 0)) - #expect(path == node.path) - } - - @Test func `resolve accepts node 23 with statement columns`() throws { + @Test func `resolve rejects node 23`() throws { let script = """ #!/bin/sh echo v23.11.0 """ let node = try self.makeTempExecutable(contents: script) let result = RuntimeLocator.resolve(searchPaths: [node.deletingLastPathComponent().path]) - guard case let .success(res) = result else { - Issue.record("Expected success, got \(result)") + guard case let .failure(.unsupported(_, found, path, _)) = result else { + Issue.record("Expected unsupported error, got \(result)") return } - #expect(res.path == node.path) - #expect(res.version == RuntimeVersion(major: 23, minor: 11, patch: 0)) + #expect(found == RuntimeVersion(major: 23, minor: 11, patch: 0)) + #expect(path == node.path) + } + + @Test(arguments: [ + ("22.22.2", false), + ("22.22.3", true), + ("23.11.0", false), + ("24.14.1", false), + ("24.15.0", true), + ("25.8.1", false), + ("25.9.0", true), + ("26.0.0", true), + ]) + func `node support matches the core runtime contract`(version: String, supported: Bool) throws { + let parsed = try #require(RuntimeVersion.from(string: version)) + #expect(RuntimeLocator.isSupportedNodeVersion(parsed) == supported) } @Test func `resolve fails when too old`() throws { @@ -105,7 +105,7 @@ struct RuntimeLocatorTests { @Test func `describe failure includes paths`() { let msg = RuntimeLocator.describeFailure(.notFound(searchPaths: ["/tmp/a", "/tmp/b"])) - #expect(msg.contains("Node >=22.19.0 <23 or >=23.11.0")) + #expect(msg.contains("Node >=22.22.3 <23, >=24.15.0 <25, or >=25.9.0")) #expect(msg.contains("PATH searched: /tmp/a:/tmp/b")) let parseMsg = RuntimeLocator.describeFailure( @@ -114,7 +114,7 @@ struct RuntimeLocatorTests { raw: "garbage", path: "/usr/local/bin/node", searchPaths: ["/usr/local/bin"])) - #expect(parseMsg.contains("Node >=22.19.0 <23 or >=23.11.0")) + #expect(parseMsg.contains("Node >=22.22.3 <23, >=24.15.0 <25, or >=25.9.0")) } @Test func `runtime version parses with leading V and metadata`() { diff --git a/extensions/browser/src/browser/chrome.graphics.ts b/extensions/browser/src/browser/chrome.graphics.ts index ea446e8ea428..680665fa4298 100644 --- a/extensions/browser/src/browser/chrome.graphics.ts +++ b/extensions/browser/src/browser/chrome.graphics.ts @@ -243,7 +243,9 @@ export async function getCachedChromeGraphicsDiagnostics( running.graphicsDiagnosticsPending ??= load(); try { const diagnostics = await running.graphicsDiagnosticsPending; - running.graphicsDiagnostics = diagnostics; + if (diagnostics.status === "available") { + running.graphicsDiagnostics = diagnostics; + } return diagnostics; } finally { running.graphicsDiagnosticsPending = undefined; diff --git a/extensions/browser/src/browser/routes/basic.existing-session.test.ts b/extensions/browser/src/browser/routes/basic.existing-session.test.ts index c22cce8432e8..d44d37423a28 100644 --- a/extensions/browser/src/browser/routes/basic.existing-session.test.ts +++ b/extensions/browser/src/browser/routes/basic.existing-session.test.ts @@ -339,6 +339,60 @@ describe("basic browser routes", () => { ); }); + it("retries unavailable graphics diagnostics and caches the first available result", async () => { + const unavailable = { + status: "unavailable", + observedAt: 123, + reason: "SystemInfo.getInfo timed out", + } as const; + const available = { + status: "available", + observedAt: 456, + acceleration: "hardware", + renderer: "ANGLE (Intel)", + vendor: "Intel", + version: "OpenGL ES 3.0", + backend: "(gl=angle,angle=metal)", + devices: [], + featureStatus: { webgl: "enabled" }, + disabledFeatures: [], + driverBugWorkarounds: [], + videoDecoding: [], + videoEncoding: [], + } as const; + inspectChromeGraphicsDiagnosticsMock + .mockResolvedValueOnce(unavailable) + .mockResolvedValue(available); + const state = createManagedProfileState( + {}, + { + isHttpReachable: async () => true, + isTransportAvailable: async () => true, + }, + ); + const profile = (state.forProfile() as { profile: unknown }).profile as never; + state.profiles.set("openclaw", { + profile, + running: { + pid: 222, + exe: { kind: "chromium", path: "/usr/bin/chromium" }, + userDataDir: "/tmp/openclaw-profile", + cdpPort: 18800, + startedAt: Date.now(), + proc: {} as never, + }, + }); + + const first = await callBasicRouteWithState({ query: { profile: "openclaw" }, state }); + const second = await callBasicRouteWithState({ query: { profile: "openclaw" }, state }); + const third = await callBasicRouteWithState({ query: { profile: "openclaw" }, state }); + + expect(responseBodyRecord(first).graphics).toEqual(unavailable); + expect(responseBodyRecord(second).graphics).toEqual(available); + expect(responseBodyRecord(third).graphics).toEqual(available); + expect(inspectChromeGraphicsDiagnosticsMock).toHaveBeenCalledTimes(2); + }); + it("does not inspect graphics while the managed process is pending reconcile", async () => { const state = createManagedProfileState( {}, diff --git a/src/media-understanding/local-audio.test.ts b/src/media-understanding/local-audio.test.ts index 7b1407e06141..7955de31945f 100644 --- a/src/media-understanding/local-audio.test.ts +++ b/src/media-understanding/local-audio.test.ts @@ -167,6 +167,39 @@ describe("local audio selection", () => { capableBackend: "metal", observedBackend: "metal", }); + + for (const failedBackend of ["Metal", "MTL0", "CUDA0"]) { + expect( + recordLocalAudioBackendObservation({ + command: "whisper-cli", + args: ["-m", modelPath, "-otxt", "-of", "{{OutputBase}}", "-nt", "{{MediaPath}}"], + output: [ + `whisper_backend_init_gpu: using ${failedBackend} backend`, + `whisper_backend_init_gpu: failed to initialize ${failedBackend} backend`, + ].join("\n"), + }), + ).toBe("cpu"); + const failedAccelerationSelection = await inspectLocalAudioSelection({ + env: { + WHISPER_CPP_MODEL: modelPath, + SHERPA_ONNX_MODEL_DIR: sherpaDir, + }, + platform: "darwin", + arch: "arm64", + resolveBinary: async (name) => + name === "whisper-cli" + ? "/opt/homebrew/bin/whisper-cli" + : name === "sherpa-onnx-offline" + ? "/usr/local/bin/sherpa-onnx-offline" + : null, + resolveRealpath: async () => "/opt/homebrew/Cellar/whisper-cpp/1.9.1/bin/whisper-cli", + inspectLinkedLibraries: async () => null, + }); + expect(failedAccelerationSelection.selected).toMatchObject({ id: "sherpa-onnx-offline" }); + expect( + failedAccelerationSelection.candidates.find((candidate) => candidate.id === "whisper-cli"), + ).toMatchObject({ observedBackend: "cpu" }); + } }); it("reports Parakeet as MLX-capable without treating capability as observation", async () => { diff --git a/src/media-understanding/local-audio.ts b/src/media-understanding/local-audio.ts index 2847cdf31c82..88c13d6fccff 100644 --- a/src/media-understanding/local-audio.ts +++ b/src/media-understanding/local-audio.ts @@ -91,13 +91,17 @@ export function recordLocalAudioBackendObservation(params: { if (commandId(params.command) !== "whisper-cli") { return undefined; } - const backend = /using\s+(?:MTL\d+|Metal)\s+backend/i.test(params.output) - ? "metal" - : /using\s+CUDA\d*\s+backend/i.test(params.output) - ? "cuda" - : /using\s+CPU\s+backend|no GPU found/i.test(params.output) - ? "cpu" - : undefined; + const acceleratorInitializationFailed = + /failed to initialize\s+(?:MTL\d+|Metal|CUDA\d*)\s+backend/i.test(params.output); + const backend = acceleratorInitializationFailed + ? "cpu" + : /using\s+(?:MTL\d+|Metal)\s+backend/i.test(params.output) + ? "metal" + : /using\s+CUDA\d*\s+backend/i.test(params.output) + ? "cuda" + : /using\s+CPU\s+backend|no GPU found/i.test(params.output) + ? "cpu" + : undefined; if (backend) { observedBackendCache.set(observationKey(params), backend); }