From 8080a8baa2909e0006845f35c402910b9f967674 Mon Sep 17 00:00:00 2001 From: Aron Prins Date: Fri, 19 Jun 2026 11:52:13 +0200 Subject: [PATCH 1/4] Fix same-origin View run popups --- src/connection/window-policy.ts | 19 +++++++++++++++++++ src/main.ts | 6 +++++- test/window-policy.test.mjs | 31 +++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/connection/window-policy.ts b/src/connection/window-policy.ts index 85da0ba2..d124caf2 100644 --- a/src/connection/window-policy.ts +++ b/src/connection/window-policy.ts @@ -30,6 +30,25 @@ export function shouldOpenExternally(targetUrl: string, allowedOrigin: string): } } +export type NewWindowPolicyAction = "navigate-in-app" | "open-externally" | "deny"; + +export function newWindowPolicyAction(targetUrl: string, allowedOrigin: string): NewWindowPolicyAction { + try { + const parsed = new URL(targetUrl); + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + return "deny"; + } + + if (parsed.origin === allowedOrigin) { + return "navigate-in-app"; + } + + return "open-externally"; + } catch { + return "deny"; + } +} + export function remotePartitionForProfile(profileId: string): string { return `persist:paperclip-remote-${profileId}`; } diff --git a/src/main.ts b/src/main.ts index d59a6ea3..6c36a2b1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -38,6 +38,7 @@ import { import { isNavigationAllowed, localPartition, + newWindowPolicyAction, remotePartitionForProfile, shouldOpenExternally, } from "./connection/window-policy"; @@ -892,7 +893,10 @@ function applyWindowPolicy(win: BrowserWindow, allowedOrigin: string): void { }); win.webContents.setWindowOpenHandler(({ url }) => { - if (shouldOpenExternally(url, allowedOrigin)) { + const action = newWindowPolicyAction(url, allowedOrigin); + if (action === "navigate-in-app") { + void win.webContents.loadURL(url); + } else if (action === "open-externally") { void shell.openExternal(url); } return { action: "deny" }; diff --git a/test/window-policy.test.mjs b/test/window-policy.test.mjs index b4aec8e1..11f6bfa9 100644 --- a/test/window-policy.test.mjs +++ b/test/window-policy.test.mjs @@ -5,6 +5,7 @@ import { createRequire } from "node:module"; const require = createRequire(import.meta.url); const { isNavigationAllowed, + newWindowPolicyAction, shouldOpenExternally, remotePartitionForProfile, } = require("../dist/connection/window-policy.js"); @@ -22,6 +23,36 @@ test("window policy opens external http links outside the allowed origin", () => assert.equal(shouldOpenExternally("mailto:test@example.com", allowedOrigin), false); }); +test("new-window policy navigates same-origin http links in the app", () => { + const allowedOrigin = "http://localhost:3100"; + assert.equal( + newWindowPolicyAction( + "http://localhost:3100/agents/agent_123/runs/run_456", + allowedOrigin, + ), + "navigate-in-app", + ); +}); + +test("new-window policy opens different-origin http links externally", () => { + const allowedOrigin = "https://paperclip-host.tailnet.ts.net"; + assert.equal( + newWindowPolicyAction("https://docs.paperclip.ing/reference", allowedOrigin), + "open-externally", + ); + assert.equal( + newWindowPolicyAction("http://example.com", allowedOrigin), + "open-externally", + ); +}); + +test("new-window policy denies unsupported and malformed urls", () => { + const allowedOrigin = "http://localhost:3100"; + assert.equal(newWindowPolicyAction("mailto:test@example.com", allowedOrigin), "deny"); + assert.equal(newWindowPolicyAction("file:///tmp/index.html", allowedOrigin), "deny"); + assert.equal(newWindowPolicyAction("not a url", allowedOrigin), "deny"); +}); + test("remote partitions are isolated per profile", () => { assert.equal(remotePartitionForProfile("abc123"), "persist:paperclip-remote-abc123"); }); From c1ceab06e7b511f8d8f07e3273bb21de6e80851c Mon Sep 17 00:00:00 2001 From: Aron Prins Date: Fri, 19 Jun 2026 12:14:47 +0200 Subject: [PATCH 2/4] Handle failed popup navigation --- src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 6c36a2b1..f5b22a7d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -895,7 +895,7 @@ function applyWindowPolicy(win: BrowserWindow, allowedOrigin: string): void { win.webContents.setWindowOpenHandler(({ url }) => { const action = newWindowPolicyAction(url, allowedOrigin); if (action === "navigate-in-app") { - void win.webContents.loadURL(url); + void win.webContents.loadURL(url).catch(() => undefined); } else if (action === "open-externally") { void shell.openExternal(url); } From d0e4f777ebe1dd57392e7dbb98744fe14225b155 Mon Sep 17 00:00:00 2001 From: Aron Prins Date: Fri, 19 Jun 2026 12:15:27 +0200 Subject: [PATCH 3/4] Deny credentialed window policy URLs --- src/connection/window-policy.ts | 16 ++++++++++++++++ test/window-policy.test.mjs | 7 +++++++ 2 files changed, 23 insertions(+) diff --git a/src/connection/window-policy.ts b/src/connection/window-policy.ts index d124caf2..4572994b 100644 --- a/src/connection/window-policy.ts +++ b/src/connection/window-policy.ts @@ -11,6 +11,10 @@ export interface WindowPolicy { export function isNavigationAllowed(targetUrl: string, allowedOrigin: string): boolean { try { const parsed = new URL(targetUrl); + if (hasEmbeddedCredentials(parsed)) { + return false; + } + return parsed.origin === allowedOrigin; } catch { return false; @@ -20,6 +24,10 @@ export function isNavigationAllowed(targetUrl: string, allowedOrigin: string): b export function shouldOpenExternally(targetUrl: string, allowedOrigin: string): boolean { try { const parsed = new URL(targetUrl); + if (hasEmbeddedCredentials(parsed)) { + return false; + } + if (parsed.origin === allowedOrigin) { return false; } @@ -35,6 +43,10 @@ export type NewWindowPolicyAction = "navigate-in-app" | "open-externally" | "den export function newWindowPolicyAction(targetUrl: string, allowedOrigin: string): NewWindowPolicyAction { try { const parsed = new URL(targetUrl); + if (hasEmbeddedCredentials(parsed)) { + return "deny"; + } + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { return "deny"; } @@ -49,6 +61,10 @@ export function newWindowPolicyAction(targetUrl: string, allowedOrigin: string): } } +function hasEmbeddedCredentials(parsed: URL): boolean { + return parsed.username !== "" || parsed.password !== ""; +} + export function remotePartitionForProfile(profileId: string): string { return `persist:paperclip-remote-${profileId}`; } diff --git a/test/window-policy.test.mjs b/test/window-policy.test.mjs index 11f6bfa9..df4876f3 100644 --- a/test/window-policy.test.mjs +++ b/test/window-policy.test.mjs @@ -14,12 +14,17 @@ test("window policy only allows same-origin navigation", () => { const allowedOrigin = "https://paperclip-host.tailnet.ts.net"; assert.equal(isNavigationAllowed("https://paperclip-host.tailnet.ts.net/dashboard", allowedOrigin), true); assert.equal(isNavigationAllowed("https://example.com", allowedOrigin), false); + assert.equal( + isNavigationAllowed("https://user:pass@paperclip-host.tailnet.ts.net/dashboard", allowedOrigin), + false, + ); }); test("window policy opens external http links outside the allowed origin", () => { const allowedOrigin = "http://localhost:3100"; assert.equal(shouldOpenExternally("https://docs.paperclip.ing", allowedOrigin), true); assert.equal(shouldOpenExternally("http://localhost:3100/settings", allowedOrigin), false); + assert.equal(shouldOpenExternally("https://user:pass@docs.paperclip.ing", allowedOrigin), false); assert.equal(shouldOpenExternally("mailto:test@example.com", allowedOrigin), false); }); @@ -48,6 +53,8 @@ test("new-window policy opens different-origin http links externally", () => { test("new-window policy denies unsupported and malformed urls", () => { const allowedOrigin = "http://localhost:3100"; + assert.equal(newWindowPolicyAction("http://user:pass@localhost:3100/settings", allowedOrigin), "deny"); + assert.equal(newWindowPolicyAction("https://user:pass@example.com", allowedOrigin), "deny"); assert.equal(newWindowPolicyAction("mailto:test@example.com", allowedOrigin), "deny"); assert.equal(newWindowPolicyAction("file:///tmp/index.html", allowedOrigin), "deny"); assert.equal(newWindowPolicyAction("not a url", allowedOrigin), "deny"); From 8baec1fcddddd76b06143db235478332d82ed71b Mon Sep 17 00:00:00 2001 From: Aron Prins Date: Fri, 19 Jun 2026 12:16:03 +0200 Subject: [PATCH 4/4] Handle failed external link opens --- src/main.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main.ts b/src/main.ts index f5b22a7d..22a1e991 100644 --- a/src/main.ts +++ b/src/main.ts @@ -877,7 +877,7 @@ function applyWindowPolicy(win: BrowserWindow, allowedOrigin: string): void { event.preventDefault(); if (shouldOpenExternally(targetUrl, allowedOrigin)) { - void shell.openExternal(targetUrl); + void shell.openExternal(targetUrl).catch(() => undefined); } }); @@ -888,7 +888,7 @@ function applyWindowPolicy(win: BrowserWindow, allowedOrigin: string): void { event.preventDefault(); if (shouldOpenExternally(targetUrl, allowedOrigin)) { - void shell.openExternal(targetUrl); + void shell.openExternal(targetUrl).catch(() => undefined); } }); @@ -897,7 +897,7 @@ function applyWindowPolicy(win: BrowserWindow, allowedOrigin: string): void { if (action === "navigate-in-app") { void win.webContents.loadURL(url).catch(() => undefined); } else if (action === "open-externally") { - void shell.openExternal(url); + void shell.openExternal(url).catch(() => undefined); } return { action: "deny" }; });