Skip to content

Commit e53a687

Browse files
fix(mcp): keep normal browser windows chromed (#62)
Co-authored-by: Codex <codex@openai.com>
1 parent 3727df8 commit e53a687

7 files changed

Lines changed: 75 additions & 22 deletions

File tree

images/minimal-vnc-desktop/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ commands the probes need):
390390
| `APP_COMMAND` | `/usr/local/bin/run-chromium-managed` | GUI launcher. Warm pods start in kiosk mode and, after allocation, honor the GameServer's `popcorn.dev/browser-mode` annotation for that pod only. |
391391
| `BROWSER_KIOSK` | `true` | Passed to `start-chromium`; set to `false` by the managed launcher only for an allocated normal-view pod. |
392392
| `BROWSER_PROFILE_DIR` | `$HOME/user-data` | Chromium profile directory. The managed launcher uses `$HOME/user-data-normal` for a normal-view restart so kiosk fullscreen state cannot carry over. |
393+
| `BROWSER_MODE_FILE` | `/tmp/popcorn-browser-mode` | Shared kiosk/normal marker used by the CDP proxy; missing or invalid values default to kiosk. |
393394
| `APP_URL` | depends on `REPLACE_DEFAULT_PAGE` | Default startup URL for `start-chromium`. When unset, falls back to DuckDuckGo (`REPLACE_DEFAULT_PAGE=false`, default) or the Reclaim loading page (`REPLACE_DEFAULT_PAGE=true`). Set explicitly to override both. |
394395
| `POPCORN_BROWSER_STARTUP_URL` | empty | Compatibility alias used when `APP_URL` is unset. |
395396
| `CHROMIUM_STARTUP_URL` | empty | Compatibility alias used when `APP_URL` and `POPCORN_BROWSER_STARTUP_URL` are unset. |

images/minimal-vnc-desktop/extensions/proxy/background.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
// Popcorn Proxy Extension - Background Service Worker (MV3)
22
// Sets proxy only - auth handled via CDP Fetch
33

4+
// Generated by start-chromium for this pod. The checked-in default is kiosk so
5+
// an older/custom launcher preserves the locked-down viewer behavior.
6+
try { importScripts('browser-mode.js'); } catch (_) {}
7+
const manageFullscreen = globalThis.POPCORN_BROWSER_KIOSK !== false;
8+
49
// ---- keep every browser window chromeless -----------------------------------
510
// The stream must never show Chromium's tab strip / omnibox. --kiosk boots the
611
// FIRST window chromeless, but that is all it guarantees: measured on this build,
@@ -28,7 +33,7 @@ function forceFullscreen(win) {
2833
function fullscreenAll() {
2934
try { chrome.windows.getAll().then((ws) => ws.forEach(forceFullscreen)).catch(() => {}); } catch (_) {}
3035
}
31-
try {
36+
if (manageFullscreen) try {
3237
chrome.windows.onCreated.addListener(forceFullscreen);
3338
// A window flipped to 'normal' after creation still needs correcting; focus
3439
// changes are the cheap, event-driven moment to re-check without polling.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Safe default. start-chromium rewrites this file to false for normal MCP pods.
2+
globalThis.POPCORN_BROWSER_KIOSK = true;

images/minimal-vnc-desktop/proxy/emulate.go

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,17 +1216,16 @@ func (e *emulator) session() error {
12161216
defer e.resetSessions()
12171217
defer e.resetDialogs()
12181218

1219-
// Kiosk fullscreen watchdog. Chrome's tab strip shows ONLY when a window is in
1219+
// Window-state watchdog. Chrome's tab strip shows ONLY when a window is in
12201220
// windowState 'normal' (measured: 'fullscreen' and 'maximized' are chromeless,
12211221
// 'normal' shows a 35px toolbar even under --kiosk). Our proxy never sets
12221222
// 'normal' — but Chromium can put a window there on its own (a window.open popup
12231223
// with size features opens normal+chromed), and the extension's onCreated fast
12241224
// path can miss a window that flips AFTER creation, since a single-window kiosk
12251225
// fires no focus-change to re-check on. This connection is always alive, so it is
12261226
// the reliable place to re-assert: each tick we ask for every tracked page's
1227-
// window, and the getWindowForTarget reply handler above forces it fullscreen.
1228-
// Idempotent on an already-fullscreen window (a no-op setWindowBounds), so the
1229-
// cost is one round-trip per page per tick.
1227+
// window, and the reply handler below applies the pod's current browser mode.
1228+
// The mode file is written by start-chromium and defaults to kiosk when absent.
12301229
watchdog := time.NewTicker(2 * time.Second)
12311230
defer watchdog.Stop()
12321231

@@ -1574,25 +1573,23 @@ func (e *emulator) session() error {
15741573
// Command responses (have an id).
15751574
if _, hasID := m["id"]; hasID {
15761575
if result, ok := m["result"].(map[string]any); ok {
1577-
// Browser.getWindowForTarget reply → fullscreen that window
1578-
// (browser-level, no session). This is how popups lose their
1579-
// location bar and fill the screen.
1576+
// Browser.getWindowForTarget reply → apply the requested pod
1577+
// mode (browser-level, no session). Kiosk popups lose their location
1578+
// bar; normal MCP windows explicitly retain browser chrome.
15801579
//
1581-
// FULLSCREEN IS THE ONLY STATE WE MAY EVER SET. Chromium's --kiosk
1582-
// suppresses the tab strip and omnibox only while the window is
1583-
// fullscreen: a window put into `normal` state draws its full chrome
1584-
// inside the same geometry, so the user gets a real URL bar in a
1585-
// session that is supposed to be a locked-down viewer. An earlier
1586-
// version of this handler resized the window with explicit normal
1587-
// bounds to cover a taller framebuffer, and that is exactly what it
1588-
// leaked — measured 796px of browser UI inside the window. Do not
1589-
// reintroduce a normal-state resize here; if a window must chase the
1590-
// screen, the sanctioned path is the X-level fit in window.go
1591-
// (requestWindowFit below), never un-fullscreening the kiosk.
1580+
// Kiosk mode may only set fullscreen: a normal-state resize would
1581+
// leak the tab strip and omnibox into a locked-down viewer. Normal
1582+
// MCP mode intentionally does the inverse and sets `normal` so those
1583+
// controls remain visible. Geometry in either mode is handled by the
1584+
// X-level fit in window.go (requestWindowFit below).
15921585
if wid, ok := result["windowId"]; ok {
1586+
desiredState := "fullscreen"
1587+
if !browserKioskMode() {
1588+
desiredState = "normal"
1589+
}
15931590
send("Browser.setWindowBounds", map[string]any{
15941591
"windowId": wid,
1595-
"bounds": map[string]any{"windowState": "fullscreen"},
1592+
"bounds": map[string]any{"windowState": desiredState},
15961593
}, "")
15971594
// A window seen OUTSIDE fullscreen is a real transition (a popup just
15981595
// opened normal+chromed), and the size openbox grants it may reflect a
@@ -1601,7 +1598,7 @@ func (e *emulator) session() error {
16011598
// watchdog tick for already-fullscreen windows, and spawning xdotool
16021599
// that often costs fd budget under Rosetta (see window.go).
16031600
if b, _ := result["bounds"].(map[string]any); b != nil {
1604-
if state, _ := b["windowState"].(string); state != "fullscreen" {
1601+
if state, _ := b["windowState"].(string); state != desiredState {
16051602
requestWindowFit(log.Printf)
16061603
}
16071604
}

images/minimal-vnc-desktop/proxy/main.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ const websocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
3131

3232
var websocketBridgeSequence atomic.Uint64
3333

34+
func browserKioskMode() bool {
35+
modeFile := envDefault("BROWSER_MODE_FILE", "/tmp/popcorn-browser-mode")
36+
mode, err := os.ReadFile(modeFile)
37+
if err != nil {
38+
return true
39+
}
40+
return strings.TrimSpace(string(mode)) != "normal"
41+
}
42+
3443
func main() {
3544
listen := flag.String("listen", envDefault("NOVNC_LISTEN", ":6080"), "HTTP listen address")
3645
vnc := flag.String("vnc", envDefault("VNC_ADDR", "127.0.0.1:5900"), "upstream VNC address")

images/minimal-vnc-desktop/proxy/main_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,27 @@ import (
1818
"github.com/flynn/noise"
1919
)
2020

21+
func TestBrowserKioskMode(t *testing.T) {
22+
modeFile := filepath.Join(t.TempDir(), "browser-mode")
23+
t.Setenv("BROWSER_MODE_FILE", modeFile)
24+
25+
if !browserKioskMode() {
26+
t.Fatal("missing mode file must default to kiosk")
27+
}
28+
if err := os.WriteFile(modeFile, []byte("normal\n"), 0o600); err != nil {
29+
t.Fatal(err)
30+
}
31+
if browserKioskMode() {
32+
t.Fatal("normal mode file must disable kiosk fullscreen management")
33+
}
34+
if err := os.WriteFile(modeFile, []byte("kiosk\n"), 0o600); err != nil {
35+
t.Fatal(err)
36+
}
37+
if !browserKioskMode() {
38+
t.Fatal("kiosk mode file must enable kiosk fullscreen management")
39+
}
40+
}
41+
2142
func TestCDPReadyGate(t *testing.T) {
2243
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2344
if r.URL.Path != "/json/version" {

images/minimal-vnc-desktop/start-chromium

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,27 @@ CDP_INTERNAL_PORT="${CDP_INTERNAL_PORT:-${CHROME_REMOTE_DEBUGGING_PORT:-9223}}"
2323
CHROME_REMOTE_DEBUGGING_ADDRESS="${CHROME_REMOTE_DEBUGGING_ADDRESS:-127.0.0.1}"
2424
PROXY_EXTENSION_DIR="${PROXY_EXTENSION_DIR:-/home/kernel/extensions/proxy}"
2525
ENABLE_PROXY_EXTENSION="${ENABLE_PROXY_EXTENSION:-true}"
26+
BROWSER_KIOSK="${BROWSER_KIOSK:-true}"
27+
BROWSER_MODE_FILE="${BROWSER_MODE_FILE:-/tmp/popcorn-browser-mode}"
2628
CHROMIUM_POLICY_DIR="${CHROMIUM_POLICY_DIR:-/etc/chromium/policies/managed}"
2729
CHROMIUM_POLICY_VARIANT_DIR="${CHROMIUM_POLICY_VARIANT_DIR:-/etc/chromium/policy-variants}"
2830

2931
BROWSER_PROFILE_DIR="${BROWSER_PROFILE_DIR:-$HOME/user-data}"
3032
mkdir -p "$BROWSER_PROFILE_DIR"
3133
rm -f "$BROWSER_PROFILE_DIR/SingletonLock" "$BROWSER_PROFILE_DIR/SingletonSocket" "$BROWSER_PROFILE_DIR/SingletonCookie"
3234

35+
# The noVNC/CDP proxy starts before allocation, so publish the selected mode in
36+
# a tiny shared file it can re-read while the managed launcher switches this pod.
37+
# Missing/invalid files fail closed to kiosk in the proxy.
38+
if [[ "$BROWSER_KIOSK" == "true" ]]; then
39+
BROWSER_MODE=kiosk
40+
BROWSER_KIOSK_JS=true
41+
else
42+
BROWSER_MODE=normal
43+
BROWSER_KIOSK_JS=false
44+
fi
45+
printf '%s\n' "$BROWSER_MODE" >"$BROWSER_MODE_FILE"
46+
3347
if [[ "$REPLACE_DEFAULT_PAGE" == "true" ]]; then
3448
reclaim_policy="${CHROMIUM_POLICY_VARIANT_DIR}/policy.reclaim-portal.json"
3549
managed_policy="${CHROMIUM_POLICY_DIR}/policy.json"
@@ -439,11 +453,15 @@ flags=(
439453
"${cloak_flags[@]}"
440454
)
441455

442-
if [[ "${BROWSER_KIOSK:-true}" == "true" ]]; then
456+
if [[ "$BROWSER_KIOSK" == "true" ]]; then
443457
flags+=("--kiosk")
444458
fi
445459

446460
if [[ "$ENABLE_PROXY_EXTENSION" == "true" && -f "${PROXY_EXTENSION_DIR}/manifest.json" ]]; then
461+
# The extension normally keeps kiosk popups chromeless. Give each isolated
462+
# pod's unpacked extension the same mode so normal MCP windows retain chrome.
463+
printf 'globalThis.POPCORN_BROWSER_KIOSK = %s;\n' "$BROWSER_KIOSK_JS" \
464+
>"${PROXY_EXTENSION_DIR}/browser-mode.js"
447465
flags+=(
448466
"--disable-extensions-except=${PROXY_EXTENSION_DIR}"
449467
"--load-extension=${PROXY_EXTENSION_DIR}"

0 commit comments

Comments
 (0)