Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion app/src/main/java/com/secrm/ui/dashboard/DashboardScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ private fun ConnectionFormSheet(
var fallback by remember { mutableStateOf(initial?.fallbackVia) }
var canExec by remember { mutableStateOf(initial?.allowMcpExec ?: false) }
var canRead by remember { mutableStateOf(initial?.allowMcpRead ?: false) }
var canRediscover by remember { mutableStateOf(initial?.allowMcpRediscover ?: false) }
var forgetKey by remember { mutableStateOf(false) }

val savedKeys = remember { app.keys.list() }
Expand Down Expand Up @@ -489,6 +490,12 @@ private fun ConnectionFormSheet(
trailingContent = { Switch(canRead, { canRead = it }) },
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
)
ListItem(
headlineContent = { Text("Find host if its IP changed") },
supportingContent = { Text("When Claude connects, search this phone's networks for the same host key") },
trailingContent = { Switch(canRediscover, { canRediscover = it }, enabled = canExec || canRead) },
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
)

if (initial?.hostKey != null) {
PillButton(
Expand Down Expand Up @@ -518,7 +525,8 @@ private fun ConnectionFormSheet(
// Kept only for the same address: a new address is a new host and needs its key approved again.
hostKey = if (forgetKey || latest == null || latest.host != host.trim() || latest.port != (portNum ?: 22) || latest.via != via) null else latest.hostKey,
allowMcpExec = canExec,
allowMcpRead = canRead
allowMcpRead = canRead,
allowMcpRediscover = canRediscover && (canExec || canRead),
))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,15 @@ class McpSessionPool(
evict(e)
}
val client = connectNew(conn, auth) ?: return null
return Entry(conn, auth, client).also { it.users = 1; entries[conn.name] = it }
// A rediscovered host was saved under its new address; store that, so the next call reuses this session.
val current = saved.find(conn.name)?.takeIf { it.copy(host = conn.host, routeByNetwork = conn.routeByNetwork) == conn } ?: conn
return Entry(current, auth, client).also { it.users = 1; entries[conn.name] = it }
}

/** Caller holds [lock]. Connects a new client for [conn], or records why it could not. */
private suspend fun connectNew(conn: SavedConnection, auth: SshAuth): SshClientManager? {
val client = newClient()
if (!client.connectSaved(conn, auth, saved, allowRediscover = false)) {
if (!client.connectSaved(conn, auth, saved, allowRediscover = conn.allowMcpRediscover)) {
keyChanged[conn.name] = client.state.value is SshState.HostKeyChanged
client.shutdown()
return null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ data class SavedConnection(
val hostKey: String? = null,
/** Opt-in: a connection is not reachable over MCP until its owner enables it. */
val allowMcpExec: Boolean = false,
val allowMcpRead: Boolean = false
val allowMcpRead: Boolean = false,
/** Opt-in: lets an MCP connect search the phone's networks for this host if its address changed (pinned key only). */
val allowMcpRediscover: Boolean = false,
)

interface SavedConnections {
Expand Down Expand Up @@ -64,7 +66,7 @@ suspend fun SshClientManager.connectSaved(
conn: SavedConnection,
auth: SshAuth,
saved: SavedConnections,
/** On for the owner's app and plugin; MCP passes false, so an agent never triggers a network probe. */
/** On for the owner's app and plugin; MCP passes the connection's [SavedConnection.allowMcpRediscover], off by default. */
allowRediscover: Boolean = true,
approvedKey: String? = null,
): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,8 @@ object SshTools {
} ?: if (pool.hostKeyChanged(conn.name)) ToolResult.error("host_key_changed",
"The server's host key changed. The owner must trust the new key in the app before MCP can connect.",
"Host key for '${conn.name}' has changed.")
else ToolResult.error("connect_failed", "Check host reachability and credentials.",
else ToolResult.error("connect_failed", if (conn.allowMcpRediscover) "Check host reachability and credentials."
else "Check host reachability and credentials. If its IP may have changed, the owner can turn on 'Find host if its IP changed' for this connection.",
"SSH connect failed to ${conn.host}:${conn.port}")
} catch (d: Denied) {
d.result
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,8 @@ class McpToolsTest {
put("connection", "box")
}).errorCode)
}

@Test fun mcpRediscoveryIsOffUnlessTheOwnerTurnsItOn() {
assertFalse(box(exec = true).allowMcpRediscover)
}
}
Loading