iOS: allow Tailnet/MagicDNS canvas actions
This commit is contained in:
@@ -6,11 +6,6 @@ import WebKit
|
||||
@MainActor
|
||||
@Observable
|
||||
final class ScreenController {
|
||||
enum Mode: Sendable {
|
||||
case canvas
|
||||
case web
|
||||
}
|
||||
|
||||
let webView: WKWebView
|
||||
private let navigationDelegate: ScreenNavigationDelegate
|
||||
private let a2uiActionHandler: CanvasA2UIActionMessageHandler
|
||||
@@ -18,10 +13,6 @@ final class ScreenController {
|
||||
var urlString: String = ""
|
||||
var errorText: String?
|
||||
|
||||
var mode: Mode {
|
||||
self.urlString.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? .canvas : .web
|
||||
}
|
||||
|
||||
/// Callback invoked when a clawdis:// deep link is tapped in the canvas
|
||||
var onDeepLink: ((URL) -> Void)?
|
||||
|
||||
@@ -65,10 +56,15 @@ final class ScreenController {
|
||||
let trimmed = self.urlString.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if trimmed.isEmpty {
|
||||
guard let url = Self.canvasScaffoldURL else { return }
|
||||
self.errorText = nil
|
||||
self.webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
|
||||
return
|
||||
} else {
|
||||
guard let url = URL(string: trimmed) else { return }
|
||||
guard let url = URL(string: trimmed) else {
|
||||
self.errorText = "Invalid URL: \(trimmed)"
|
||||
return
|
||||
}
|
||||
self.errorText = nil
|
||||
if url.isFileURL {
|
||||
self.webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
|
||||
} else {
|
||||
@@ -221,6 +217,10 @@ final class ScreenController {
|
||||
}
|
||||
if host == "localhost" { return true }
|
||||
if host.hasSuffix(".local") { return true }
|
||||
if host.hasSuffix(".ts.net") { return true }
|
||||
if host.hasSuffix(".tailscale.net") { return true }
|
||||
// Allow MagicDNS / LAN hostnames like "peters-mac-studio-1".
|
||||
if !host.contains("."), !host.contains(":") { return true }
|
||||
if let ipv4 = Self.parseIPv4(host) {
|
||||
return Self.isLocalNetworkIPv4(ipv4)
|
||||
}
|
||||
@@ -306,6 +306,22 @@ private final class ScreenNavigationDelegate: NSObject, WKNavigationDelegate {
|
||||
|
||||
decisionHandler(.allow)
|
||||
}
|
||||
|
||||
func webView(
|
||||
_: WKWebView,
|
||||
didFailProvisionalNavigation _: WKNavigation?,
|
||||
withError error: any Error)
|
||||
{
|
||||
Task { @MainActor in
|
||||
self.controller?.errorText = error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
func webView(_: WKWebView, didFail _: WKNavigation?, withError error: any Error) {
|
||||
Task { @MainActor in
|
||||
self.controller?.errorText = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler {
|
||||
|
||||
@@ -21,5 +21,5 @@ struct ScreenTab: View {
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation/mode selection is agent-driven; no local controls here.
|
||||
// Navigation is agent-driven; no local URL bar here.
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import WebKit
|
||||
@Test @MainActor func canvasModeConfiguresWebViewForTouch() {
|
||||
let screen = ScreenController()
|
||||
|
||||
#expect(screen.mode == .canvas)
|
||||
#expect(screen.webView.isOpaque == true)
|
||||
#expect(screen.webView.backgroundColor == .black)
|
||||
|
||||
@@ -17,11 +16,11 @@ import WebKit
|
||||
#expect(scrollView.bounces == false)
|
||||
}
|
||||
|
||||
@Test @MainActor func navigateDefaultsToWebMode() {
|
||||
@Test @MainActor func navigateSlashShowsDefaultCanvas() {
|
||||
let screen = ScreenController()
|
||||
screen.navigate(to: "not a url")
|
||||
screen.navigate(to: "/")
|
||||
|
||||
#expect(screen.mode == .web)
|
||||
#expect(screen.urlString.isEmpty)
|
||||
}
|
||||
|
||||
@Test @MainActor func evalExecutesJavaScript() async throws {
|
||||
@@ -46,6 +45,8 @@ import WebKit
|
||||
let screen = ScreenController()
|
||||
#expect(screen.isLocalNetworkCanvasURL(URL(string: "http://localhost:18793/")!) == true)
|
||||
#expect(screen.isLocalNetworkCanvasURL(URL(string: "http://clawd.local:18793/")!) == true)
|
||||
#expect(screen.isLocalNetworkCanvasURL(URL(string: "http://peters-mac-studio-1:18793/")!) == true)
|
||||
#expect(screen.isLocalNetworkCanvasURL(URL(string: "https://peters-mac-studio-1.ts.net:18793/")!) == true)
|
||||
#expect(screen.isLocalNetworkCanvasURL(URL(string: "http://192.168.0.10:18793/")!) == true)
|
||||
#expect(screen.isLocalNetworkCanvasURL(URL(string: "http://10.0.0.10:18793/")!) == true)
|
||||
#expect(screen.isLocalNetworkCanvasURL(URL(string: "http://100.123.224.76:18793/")!) == true) // Tailscale CGNAT
|
||||
|
||||
@@ -151,7 +151,7 @@ When iOS is backgrounded:
|
||||
### App structure
|
||||
- Single fullscreen Canvas surface (WKWebView).
|
||||
- One settings entry point: a **gear button** that opens a settings sheet.
|
||||
- All navigation/mode selection is **agent-driven** (no local URL bar).
|
||||
- All navigation is **agent-driven** (no local URL bar).
|
||||
|
||||
### Components
|
||||
- `BridgeDiscovery`: Bonjour browse + resolve (Network.framework `NWBrowser`)
|
||||
@@ -232,4 +232,4 @@ open Clawdis.xcodeproj
|
||||
## Open questions
|
||||
- Should `connect.params.client.mode` be `"node"` with `platform="ios ..."` or a distinct mode `"ios-node"`? (Presence filtering currently excludes `"cli"` only.)
|
||||
- Do we want a “permissions” model per node (voice only vs voice+canvas) at pairing time?
|
||||
- Should “website mode” allow arbitrary https, or enforce an allowlist to reduce risk?
|
||||
- Should loading arbitrary websites via `canvas.navigate` allow any https URL, or enforce an allowlist to reduce risk?
|
||||
|
||||
Reference in New Issue
Block a user