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 ### Changes
- Android: migrate node transport to the Gateway WebSocket protocol with TLS pinning support + gateway discovery naming. - 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. - Docs: refresh Android node discovery docs for the Gateway WS service type.
## 2026.1.19-1 ## 2026.1.19-1

View File

@@ -103,7 +103,7 @@ dependencies {
implementation("androidx.security:security-crypto:1.1.0") implementation("androidx.security:security-crypto:1.1.0")
implementation("androidx.exifinterface:exifinterface:1.4.2") 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) // CameraX (for node.invoke camera.* parity)
implementation("androidx.camera:camera-core:1.5.2") implementation("androidx.camera:camera-core:1.5.2")
@@ -113,7 +113,7 @@ dependencies {
implementation("androidx.camera:camera-view:1.5.2") implementation("androidx.camera:camera-view:1.5.2")
// Unicast DNS-SD (Wide-Area Bonjour) for tailnet discovery domains. // 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("junit:junit:4.13.2")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.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.ChatSessionEntry
import com.clawdbot.android.chat.OutgoingAttachment import com.clawdbot.android.chat.OutgoingAttachment
import com.clawdbot.android.gateway.DeviceIdentityStore import com.clawdbot.android.gateway.DeviceIdentityStore
import com.clawdbot.android.gateway.GatewayClientInfo
import com.clawdbot.android.gateway.GatewayConnectOptions import com.clawdbot.android.gateway.GatewayConnectOptions
import com.clawdbot.android.gateway.GatewayDiscovery import com.clawdbot.android.gateway.GatewayDiscovery
import com.clawdbot.android.gateway.GatewayEndpoint import com.clawdbot.android.gateway.GatewayEndpoint
@@ -208,7 +209,7 @@ class NodeRuntime(context: Context) {
}, },
) )
private val chat = private val chat: ChatController =
ChatController( ChatController(
scope = scope, scope = scope,
session = operatorSession, session = operatorSession,

View File

@@ -219,7 +219,7 @@ class GatewaySession(
} }
} }
fun awaitClose() = closedDeferred.await() suspend fun awaitClose() = closedDeferred.await()
fun closeQuietly() { fun closeQuietly() {
if (isClosed.compareAndSet(false, true)) { if (isClosed.compareAndSet(false, true)) {
@@ -315,41 +315,20 @@ class GatewaySession(
client.modelIdentifier?.let { put("modelIdentifier", JsonPrimitive(it)) } 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 authToken = token?.trim().orEmpty()
val authPassword = password?.trim().orEmpty() val authPassword = password?.trim().orEmpty()
if (authToken.isNotEmpty()) { val authJson =
params["auth"] = when {
buildJsonObject { authToken.isNotEmpty() ->
put("token", JsonPrimitive(authToken)) buildJsonObject {
} put("token", JsonPrimitive(authToken))
} else if (authPassword.isNotEmpty()) { }
params["auth"] = authPassword.isNotEmpty() ->
buildJsonObject { buildJsonObject {
put("password", JsonPrimitive(authPassword)) put("password", JsonPrimitive(authPassword))
} }
} else -> null
}
val identity = identityStore.loadOrCreate() val identity = identityStore.loadOrCreate()
val signedAtMs = System.currentTimeMillis() val signedAtMs = System.currentTimeMillis()
@@ -365,17 +344,40 @@ class GatewaySession(
) )
val signature = identityStore.signPayload(payload, identity) val signature = identityStore.signPayload(payload, identity)
val publicKey = identityStore.publicKeyBase64Url(identity) val publicKey = identityStore.publicKeyBase64Url(identity)
if (!signature.isNullOrBlank() && !publicKey.isNullOrBlank()) { val deviceJson =
params["device"] = if (!signature.isNullOrBlank() && !publicKey.isNullOrBlank()) {
buildJsonObject { buildJsonObject {
put("id", JsonPrimitive(identity.deviceId)) put("id", JsonPrimitive(identity.deviceId))
put("publicKey", JsonPrimitive(publicKey)) put("publicKey", JsonPrimitive(publicKey))
put("signature", JsonPrimitive(signature)) put("signature", JsonPrimitive(signature))
put("signedAt", JsonPrimitive(signedAtMs)) 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) { private suspend fun handleMessage(text: String) {

View File

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

View File

@@ -117,7 +117,7 @@ final class DevicePairingApprovalPrompter {
private func updatePendingCounts() { private func updatePendingCounts() {
self.pendingCount = self.queue.count 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() { private func presentNextIfNeeded() {
@@ -270,7 +270,8 @@ final class DevicePairingApprovalPrompter {
let req = try GatewayPayloadDecoding.decode(payload, as: PendingRequest.self) let req = try GatewayPayloadDecoding.decode(payload, as: PendingRequest.self)
self.enqueue(req) self.enqueue(req)
} catch { } 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": case let .event(evt) where evt.event == "device.pair.resolved":
guard let payload = evt.payload else { return } guard let payload = evt.payload else { return }
@@ -278,7 +279,9 @@ final class DevicePairingApprovalPrompter {
let resolved = try GatewayPayloadDecoding.decode(payload, as: PairingResolvedEvent.self) let resolved = try GatewayPayloadDecoding.decode(payload, as: PairingResolvedEvent.self)
self.handleResolved(resolved) self.handleResolved(resolved)
} catch { } 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: default:
break break
@@ -293,11 +296,15 @@ final class DevicePairingApprovalPrompter {
} }
private func handleResolved(_ resolved: PairingResolvedEvent) { 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 { if let activeRequestId, activeRequestId == resolved.requestId {
self.resolvedByRequestId.insert(resolved.requestId) self.resolvedByRequestId.insert(resolved.requestId)
self.endActiveAlert() 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 return
} }
self.queue.removeAll { $0.requestId == resolved.requestId } self.queue.removeAll { $0.requestId == resolved.requestId }
@@ -314,7 +321,7 @@ final class DevicePairingApprovalPrompter {
lines.append("Role: \(role)") lines.append("Role: \(role)")
} }
if let scopes = req.scopes, !scopes.isEmpty { if let scopes = req.scopes, !scopes.isEmpty {
lines.append("Scopes: \(scopes.joined(separator: \", \"))") lines.append("Scopes: \(scopes.joined(separator: ", "))")
} }
if let remoteIp = req.remoteIp { if let remoteIp = req.remoteIp {
lines.append("IP: \(remoteIp)") lines.append("IP: \(remoteIp)")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -81,8 +81,9 @@ struct SystemRunSettingsView: View {
.pickerStyle(.menu) .pickerStyle(.menu)
Text(self.model.isDefaultsScope 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." ? "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.") :
"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) .font(.footnote)
.foregroundStyle(.tertiary) .foregroundStyle(.tertiary)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)

View File

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