fix: pass android lint and swiftformat

This commit is contained in:
Peter Steinberger
2026-01-19 11:14:27 +00:00
parent e6a4cf01ee
commit b826bd668c
15 changed files with 118 additions and 94 deletions

View File

@@ -6,6 +6,7 @@ Docs: https://docs.clawd.bot
### Changes
- Android: migrate node transport to the Gateway WebSocket protocol with TLS pinning support + gateway discovery naming.
- Android: bump okhttp + dnsjava to satisfy lint dependency checks.
- Docs: refresh Android node discovery docs for the Gateway WS service type.
## 2026.1.19-1

View File

@@ -103,7 +103,7 @@ dependencies {
implementation("androidx.security:security-crypto:1.1.0")
implementation("androidx.exifinterface:exifinterface:1.4.2")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:okhttp:5.3.2")
// CameraX (for node.invoke camera.* parity)
implementation("androidx.camera:camera-core:1.5.2")
@@ -113,7 +113,7 @@ dependencies {
implementation("androidx.camera:camera-view:1.5.2")
// Unicast DNS-SD (Wide-Area Bonjour) for tailnet discovery domains.
implementation("dnsjava:dnsjava:3.6.3")
implementation("dnsjava:dnsjava:3.6.4")
testImplementation("junit:junit:4.13.2")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")

View File

@@ -13,6 +13,7 @@ import com.clawdbot.android.chat.ChatPendingToolCall
import com.clawdbot.android.chat.ChatSessionEntry
import com.clawdbot.android.chat.OutgoingAttachment
import com.clawdbot.android.gateway.DeviceIdentityStore
import com.clawdbot.android.gateway.GatewayClientInfo
import com.clawdbot.android.gateway.GatewayConnectOptions
import com.clawdbot.android.gateway.GatewayDiscovery
import com.clawdbot.android.gateway.GatewayEndpoint
@@ -208,7 +209,7 @@ class NodeRuntime(context: Context) {
},
)
private val chat =
private val chat: ChatController =
ChatController(
scope = scope,
session = operatorSession,

View File

@@ -219,7 +219,7 @@ class GatewaySession(
}
}
fun awaitClose() = closedDeferred.await()
suspend fun awaitClose() = closedDeferred.await()
fun closeQuietly() {
if (isClosed.compareAndSet(false, true)) {
@@ -315,41 +315,20 @@ class GatewaySession(
client.modelIdentifier?.let { put("modelIdentifier", JsonPrimitive(it)) }
}
val params =
buildJsonObject {
put("minProtocol", JsonPrimitive(GATEWAY_PROTOCOL_VERSION))
put("maxProtocol", JsonPrimitive(GATEWAY_PROTOCOL_VERSION))
put("client", clientObj)
if (options.caps.isNotEmpty()) put("caps", JsonArray(options.caps.map(::JsonPrimitive)))
if (options.commands.isNotEmpty()) put("commands", JsonArray(options.commands.map(::JsonPrimitive)))
if (options.permissions.isNotEmpty()) {
put(
"permissions",
buildJsonObject {
options.permissions.forEach { (key, value) ->
put(key, JsonPrimitive(value))
}
},
)
}
put("role", JsonPrimitive(options.role))
if (options.scopes.isNotEmpty()) put("scopes", JsonArray(options.scopes.map(::JsonPrimitive)))
put("locale", JsonPrimitive(locale))
}
val authToken = token?.trim().orEmpty()
val authPassword = password?.trim().orEmpty()
if (authToken.isNotEmpty()) {
params["auth"] =
buildJsonObject {
put("token", JsonPrimitive(authToken))
}
} else if (authPassword.isNotEmpty()) {
params["auth"] =
buildJsonObject {
put("password", JsonPrimitive(authPassword))
}
}
val authJson =
when {
authToken.isNotEmpty() ->
buildJsonObject {
put("token", JsonPrimitive(authToken))
}
authPassword.isNotEmpty() ->
buildJsonObject {
put("password", JsonPrimitive(authPassword))
}
else -> null
}
val identity = identityStore.loadOrCreate()
val signedAtMs = System.currentTimeMillis()
@@ -365,17 +344,40 @@ class GatewaySession(
)
val signature = identityStore.signPayload(payload, identity)
val publicKey = identityStore.publicKeyBase64Url(identity)
if (!signature.isNullOrBlank() && !publicKey.isNullOrBlank()) {
params["device"] =
val deviceJson =
if (!signature.isNullOrBlank() && !publicKey.isNullOrBlank()) {
buildJsonObject {
put("id", JsonPrimitive(identity.deviceId))
put("publicKey", JsonPrimitive(publicKey))
put("signature", JsonPrimitive(signature))
put("signedAt", JsonPrimitive(signedAtMs))
}
}
} else {
null
}
return params
return buildJsonObject {
put("minProtocol", JsonPrimitive(GATEWAY_PROTOCOL_VERSION))
put("maxProtocol", JsonPrimitive(GATEWAY_PROTOCOL_VERSION))
put("client", clientObj)
if (options.caps.isNotEmpty()) put("caps", JsonArray(options.caps.map(::JsonPrimitive)))
if (options.commands.isNotEmpty()) put("commands", JsonArray(options.commands.map(::JsonPrimitive)))
if (options.permissions.isNotEmpty()) {
put(
"permissions",
buildJsonObject {
options.permissions.forEach { (key, value) ->
put(key, JsonPrimitive(value))
}
},
)
}
put("role", JsonPrimitive(options.role))
if (options.scopes.isNotEmpty()) put("scopes", JsonArray(options.scopes.map(::JsonPrimitive)))
authJson?.let { put("auth", it) }
deviceJson?.let { put("device", it) }
put("locale", JsonPrimitive(locale))
}
}
private suspend fun handleMessage(text: String) {

View File

@@ -309,11 +309,10 @@ fun SettingsSheet(viewModel: MainViewModel) {
add("IP: ${gateway.host}:${gateway.port}")
gateway.lanHost?.let { add("LAN: $it") }
gateway.tailnetDns?.let { add("Tailnet: $it") }
if (gateway.gatewayPort != null || gateway.bridgePort != null || gateway.canvasPort != null) {
val gw = gateway.gatewayPort?.toString() ?: ""
val br = (gateway.bridgePort ?: gateway.port).toString()
if (gateway.gatewayPort != null || gateway.canvasPort != null) {
val gw = (gateway.gatewayPort ?: gateway.port).toString()
val canvas = gateway.canvasPort?.toString() ?: ""
add("Ports: gw $gw · bridge $br · canvas $canvas")
add("Ports: gw $gw · canvas $canvas")
}
}
ListItem(

View File

@@ -117,7 +117,7 @@ final class DevicePairingApprovalPrompter {
private func updatePendingCounts() {
self.pendingCount = self.queue.count
self.pendingRepairCount = self.queue.filter { $0.isRepair == true }.count
self.pendingRepairCount = self.queue.count(where: { $0.isRepair == true })
}
private func presentNextIfNeeded() {
@@ -270,7 +270,8 @@ final class DevicePairingApprovalPrompter {
let req = try GatewayPayloadDecoding.decode(payload, as: PendingRequest.self)
self.enqueue(req)
} catch {
self.logger.error("failed to decode device pairing request: \(error.localizedDescription, privacy: .public)")
self.logger
.error("failed to decode device pairing request: \(error.localizedDescription, privacy: .public)")
}
case let .event(evt) where evt.event == "device.pair.resolved":
guard let payload = evt.payload else { return }
@@ -278,7 +279,9 @@ final class DevicePairingApprovalPrompter {
let resolved = try GatewayPayloadDecoding.decode(payload, as: PairingResolvedEvent.self)
self.handleResolved(resolved)
} catch {
self.logger.error("failed to decode device pairing resolution: \(error.localizedDescription, privacy: .public)")
self.logger
.error(
"failed to decode device pairing resolution: \(error.localizedDescription, privacy: .public)")
}
default:
break
@@ -293,11 +296,15 @@ final class DevicePairingApprovalPrompter {
}
private func handleResolved(_ resolved: PairingResolvedEvent) {
let resolution = resolved.decision == PairingResolution.approved.rawValue ? PairingResolution.approved : .rejected
let resolution = resolved.decision == PairingResolution.approved.rawValue ? PairingResolution
.approved : .rejected
if let activeRequestId, activeRequestId == resolved.requestId {
self.resolvedByRequestId.insert(resolved.requestId)
self.endActiveAlert()
self.logger.info("device pairing resolved while active requestId=\(resolved.requestId, privacy: .public) decision=\(resolution.rawValue, privacy: .public)")
let decision = resolution.rawValue
self.logger.info(
"device pairing resolved while active requestId=\(resolved.requestId, privacy: .public) " +
"decision=\(decision, privacy: .public)")
return
}
self.queue.removeAll { $0.requestId == resolved.requestId }
@@ -314,7 +321,7 @@ final class DevicePairingApprovalPrompter {
lines.append("Role: \(role)")
}
if let scopes = req.scopes, !scopes.isEmpty {
lines.append("Scopes: \(scopes.joined(separator: \", \"))")
lines.append("Scopes: \(scopes.joined(separator: ", "))")
}
if let remoteIp = req.remoteIp {
lines.append("IP: \(remoteIp)")

View File

@@ -53,11 +53,11 @@ enum ExecApprovalQuickMode: String, CaseIterable, Identifiable {
static func from(security: ExecSecurity, ask: ExecAsk) -> ExecApprovalQuickMode {
switch security {
case .deny:
return .deny
.deny
case .full:
return .allow
.allow
case .allowlist:
return .ask
.ask
}
}
}
@@ -106,7 +106,8 @@ struct ExecApprovalsAgent: Codable {
var allowlist: [ExecAllowlistEntry]?
var isEmpty: Bool {
security == nil && ask == nil && askFallback == nil && autoAllowSkills == nil && (allowlist?.isEmpty ?? true)
self.security == nil && self.ask == nil && self.askFallback == nil && self
.autoAllowSkills == nil && (self.allowlist?.isEmpty ?? true)
}
}
@@ -467,8 +468,8 @@ struct ExecCommandResolution: Sendable {
command: [String],
rawCommand: String?,
cwd: String?,
env: [String: String]?
) -> ExecCommandResolution? {
env: [String: String]?) -> ExecCommandResolution?
{
let trimmedRaw = rawCommand?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !trimmedRaw.isEmpty, let token = self.parseFirstToken(trimmedRaw) {
return self.resolveExecutable(rawExecutable: token, cwd: cwd, env: env)
@@ -486,8 +487,8 @@ struct ExecCommandResolution: Sendable {
private static func resolveExecutable(
rawExecutable: String,
cwd: String?,
env: [String: String]?
) -> ExecCommandResolution? {
env: [String: String]?) -> ExecCommandResolution?
{
let expanded = rawExecutable.hasPrefix("~") ? (rawExecutable as NSString).expandingTildeInPath : rawExecutable
let hasPathSeparator = expanded.contains("/") || expanded.contains("\\")
let resolvedPath: String? = {
@@ -503,7 +504,11 @@ struct ExecCommandResolution: Sendable {
return CommandResolver.findExecutable(named: expanded, searchPaths: searchPaths)
}()
let name = resolvedPath.map { URL(fileURLWithPath: $0).lastPathComponent } ?? expanded
return ExecCommandResolution(rawExecutable: expanded, resolvedPath: resolvedPath, executableName: name, cwd: cwd)
return ExecCommandResolution(
rawExecutable: expanded,
resolvedPath: resolvedPath,
executableName: name,
cwd: cwd)
}
private static func parseFirstToken(_ command: String) -> String? {
@@ -624,7 +629,7 @@ struct ExecEventPayload: Codable, Sendable {
var output: String?
var reason: String?
static func truncateOutput(_ raw: String, maxChars: Int = 20_000) -> String? {
static func truncateOutput(_ raw: String, maxChars: Int = 20000) -> String? {
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
if trimmed.count <= maxChars { return trimmed }

View File

@@ -51,7 +51,7 @@ final class ExecApprovalsGatewayPrompter {
"id": AnyCodable(request.id),
"decision": AnyCodable(decision.rawValue),
],
timeoutMs: 10_000)
timeoutMs: 10000)
} catch {
self.logger.error("exec approval handling failed \(error.localizedDescription, privacy: .public)")
}

View File

@@ -28,7 +28,7 @@ private struct ExecApprovalSocketDecision: Codable {
var decision: ExecApprovalDecision
}
fileprivate struct ExecHostSocketRequest: Codable {
private struct ExecHostSocketRequest: Codable {
var type: String
var id: String
var nonce: String
@@ -37,7 +37,7 @@ fileprivate struct ExecHostSocketRequest: Codable {
var requestJson: String
}
fileprivate struct ExecHostRequest: Codable {
private struct ExecHostRequest: Codable {
var command: [String]
var rawCommand: String?
var cwd: String?
@@ -48,7 +48,7 @@ fileprivate struct ExecHostRequest: Codable {
var sessionKey: String?
}
fileprivate struct ExecHostRunResult: Codable {
private struct ExecHostRunResult: Codable {
var exitCode: Int?
var timedOut: Bool
var success: Bool
@@ -57,13 +57,13 @@ fileprivate struct ExecHostRunResult: Codable {
var error: String?
}
fileprivate struct ExecHostError: Codable {
private struct ExecHostError: Codable {
var code: String
var message: String
var reason: String?
}
fileprivate struct ExecHostResponse: Codable {
private struct ExecHostResponse: Codable {
var type: String
var id: String
var ok: Bool
@@ -74,14 +74,14 @@ fileprivate struct ExecHostResponse: Codable {
enum ExecApprovalsSocketClient {
private struct TimeoutError: LocalizedError {
var message: String
var errorDescription: String? { message }
var errorDescription: String? { self.message }
}
static func requestDecision(
socketPath: String,
token: String,
request: ExecApprovalPromptRequest,
timeoutMs: Int = 15_000) async -> ExecApprovalDecision?
timeoutMs: Int = 15000) async -> ExecApprovalDecision?
{
let trimmedPath = socketPath.trimmingCharacters(in: .whitespacesAndNewlines)
let trimmedToken = token.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -254,7 +254,7 @@ enum ExecApprovalsPromptPresenter {
}
@MainActor
fileprivate enum ExecHostExecutor {
private enum ExecHostExecutor {
private static let blockedEnvKeys: Set<String> = [
"PATH",
"NODE_OPTIONS",
@@ -313,12 +313,15 @@ fileprivate enum ExecHostExecutor {
id: UUID().uuidString,
ok: false,
payload: nil,
error: ExecHostError(code: "UNAVAILABLE", message: "SYSTEM_RUN_DISABLED: security=deny", reason: "security=deny"))
error: ExecHostError(
code: "UNAVAILABLE",
message: "SYSTEM_RUN_DISABLED: security=deny",
reason: "security=deny"))
}
let requiresAsk: Bool = {
if ask == .always { return true }
if ask == .onMiss && security == .allowlist && allowlistMatch == nil && !skillAllow { return true }
if ask == .onMiss, security == .allowlist, allowlistMatch == nil, !skillAllow { return true }
return false
}()
@@ -341,7 +344,10 @@ fileprivate enum ExecHostExecutor {
id: UUID().uuidString,
ok: false,
payload: nil,
error: ExecHostError(code: "UNAVAILABLE", message: "SYSTEM_RUN_DENIED: user denied", reason: "user-denied"))
error: ExecHostError(
code: "UNAVAILABLE",
message: "SYSTEM_RUN_DENIED: user denied",
reason: "user-denied"))
case .allowAlways:
approvedByAsk = true
if security == .allowlist {
@@ -355,13 +361,16 @@ fileprivate enum ExecHostExecutor {
}
}
if security == .allowlist && allowlistMatch == nil && !skillAllow && !approvedByAsk {
if security == .allowlist, allowlistMatch == nil, !skillAllow, !approvedByAsk {
return ExecHostResponse(
type: "exec-res",
id: UUID().uuidString,
ok: false,
payload: nil,
error: ExecHostError(code: "UNAVAILABLE", message: "SYSTEM_RUN_DENIED: allowlist miss", reason: "allowlist-miss"))
error: ExecHostError(
code: "UNAVAILABLE",
message: "SYSTEM_RUN_DENIED: allowlist miss",
reason: "allowlist-miss"))
}
if let match = allowlistMatch {
@@ -381,7 +390,10 @@ fileprivate enum ExecHostExecutor {
id: UUID().uuidString,
ok: false,
payload: nil,
error: ExecHostError(code: "UNAVAILABLE", message: "PERMISSION_MISSING: screenRecording", reason: "permission:screenRecording"))
error: ExecHostError(
code: "UNAVAILABLE",
message: "PERMISSION_MISSING: screenRecording",
reason: "permission:screenRecording"))
}
}
@@ -621,7 +633,7 @@ private final class ExecApprovalsSocketServer: @unchecked Sendable {
private func handleExecRequest(_ request: ExecHostSocketRequest) async -> ExecHostResponse {
let nowMs = Int(Date().timeIntervalSince1970 * 1000)
if abs(nowMs - request.ts) > 10_000 {
if abs(nowMs - request.ts) > 10000 {
return ExecHostResponse(
type: "exec-res",
id: request.id,

View File

@@ -120,8 +120,8 @@ enum GatewayEnvironment {
kind: .missingNode,
nodeVersion: nil,
gatewayVersion: nil,
requiredGateway: expectedString,
message: RuntimeLocator.describeFailure(err))
requiredGateway: expectedString,
message: RuntimeLocator.describeFailure(err))
case let .success(runtime):
let gatewayBin = CommandResolver.clawdbotExecutable()
@@ -237,11 +237,10 @@ enum GatewayEnvironment {
static func installGlobal(versionString: String?, statusHandler: @escaping @Sendable (String) -> Void) async {
let preferred = CommandResolver.preferredPaths().joined(separator: ":")
let trimmed = versionString?.trimmingCharacters(in: .whitespacesAndNewlines)
let target: String
if let trimmed, !trimmed.isEmpty {
target = trimmed
let target: String = if let trimmed, !trimmed.isEmpty {
trimmed
} else {
target = "latest"
"latest"
}
let npm = CommandResolver.findExecutable(named: "npm")
let pnpm = CommandResolver.findExecutable(named: "pnpm")

View File

@@ -482,12 +482,12 @@ actor MacNodeRuntime {
let requiresAsk: Bool = {
if ask == .always { return true }
if ask == .onMiss && security == .allowlist && allowlistMatch == nil && !skillAllow { return true }
if ask == .onMiss, security == .allowlist, allowlistMatch == nil, !skillAllow { return true }
return false
}()
let approvedByAsk = params.approved == true
if requiresAsk && !approvedByAsk {
if requiresAsk, !approvedByAsk {
await self.emitExecEvent(
"exec.denied",
payload: ExecEventPayload(
@@ -502,7 +502,7 @@ actor MacNodeRuntime {
message: "SYSTEM_RUN_DENIED: approval required")
}
if security == .allowlist && allowlistMatch == nil && !skillAllow && !approvedByAsk {
if security == .allowlist, allowlistMatch == nil, !skillAllow, !approvedByAsk {
await self.emitExecEvent(
"exec.denied",
payload: ExecEventPayload(
@@ -558,7 +558,7 @@ actor MacNodeRuntime {
env: env,
timeout: timeoutSec)
let combined = [result.stdout, result.stderr, result.errorMessage]
.compactMap { $0 }
.compactMap(\.self)
.filter { !$0.isEmpty }
.joined(separator: "\n")
await self.emitExecEvent(
@@ -668,7 +668,7 @@ actor MacNodeRuntime {
let resolvedPath = (socketPath?.isEmpty == false)
? socketPath!
: current.socket?.path?.trimmingCharacters(in: .whitespacesAndNewlines) ??
ExecApprovalsStore.socketPath()
ExecApprovalsStore.socketPath()
let resolvedToken = (token?.isEmpty == false)
? token!
: current.socket?.token?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""

View File

@@ -57,5 +57,4 @@ final class LiveMacNodeRuntimeMainActorServices: MacNodeRuntimeMainActorServices
maxAgeMs: maxAgeMs,
timeoutMs: timeoutMs)
}
}

View File

@@ -168,7 +168,6 @@ struct SessionMenuPreviewView: View {
.font(.caption)
.foregroundStyle(self.primaryColor)
}
}
enum SessionMenuPreviewLoader {
@@ -182,7 +181,7 @@ enum SessionMenuPreviewLoader {
static func load(sessionKey: String, maxItems: Int) async -> SessionMenuPreviewSnapshot {
if let cached = await SessionPreviewCache.shared.cachedItems(for: sessionKey, maxAge: cacheMaxAgeSeconds) {
return Self.snapshot(from: cached)
return self.snapshot(from: cached)
}
do {

View File

@@ -81,8 +81,9 @@ struct SystemRunSettingsView: View {
.pickerStyle(.menu)
Text(self.model.isDefaultsScope
? "Defaults apply when an agent has no overrides. Ask controls prompt behavior; fallback is used when no companion UI is reachable."
: "Security controls whether system.run can execute on this Mac when paired as a node. Ask controls prompt behavior; fallback is used when no companion UI is reachable.")
? "Defaults apply when an agent has no overrides. Ask controls prompt behavior; fallback is used when no companion UI is reachable."
:
"Security controls whether system.run can execute on this Mac when paired as a node. Ask controls prompt behavior; fallback is used when no companion UI is reachable.")
.font(.footnote)
.foregroundStyle(.tertiary)
.fixedSize(horizontal: false, vertical: true)

View File

@@ -51,7 +51,6 @@ struct UsageMenuLabelView: View {
.padding(.leading, 2)
}
}
}
.padding(.vertical, 10)
.padding(.leading, self.paddingLeading)