Managed Web browser activity #63
No reviewers
Labels
No labels
bug
duplicate
enhancement
future
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
albert/shepherd-launcher!63
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "u/albert/10/web-browser"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Add a managed web browser activity (#63)
Closes #10.
Adds a browser activity type that launches Chrome under enterprise managed-policy controls — a locked-down, kiosk-style web session suitable for things like a school-mode launcher entry. It composes on top of the existing entry/firewall machinery rather than introducing a new
EntryKind: anyflatpakentry forcom.google.Chromecan carry a[entries.browser]block.shepherd-launcher only steers Chrome through documented controls (enterprise policy JSON + command-line flags). It does not patch the browser or circumvent any protections.
What you get
url_allowlistblocks everything not matched;url_blocklistcarves out exceptions after it.kiosk(fullscreen, no chrome),app(single app window), orwindowed.profile_id— entries sharing an id share cookies/logins; unique ids are isolated.wipe_on_exitclears the profile directory after the session ends (handled by shepherd, never trusted to Chrome).[entries.firewall]for coarse IP-layer defense-in-depth (issue #4), kept separate because hostname filtering is Chrome's job.Configuration
New
[entries.browser]schema inshepherd-config— typed, validated, and round-tripped fromRawEntryto the validatedEntry. The security-relevant validation isprofile_id(must be a single safe path segment — it becomes an on-disk directory name);start_urland URL patterns get light checks with Chrome as the real authority.config.example.tomlgains a fully-commentedchrome-schoolentry composing all three layers (browser active, firewall shown commented-out because a default-deny IP list needs current Google/Workspace CIDRs that drift over time), and it passesvalidate-config.How the policy is delivered (the interesting part)
The original design assumed Chrome would read managed policy from its per-user flatpak config dir. The gated real-Chrome test falsified that: the Flathub Chrome wrapper reads managed policy only from the machine-wide host
/etc/opt/chrome/policies/— which would require root and would hijack Chrome for every user on the machine.The fix injects policy into the sandbox's own ephemeral
/etcper launch. Chrome is launched through a small bash shim that symlinks our per-user policy file into the sandbox's/etc/opt/chrome/policies/managed/and then execs the normal launcher. Per-user, per-launch, no root, host/etcnever touched. As a consequence, browser support is now flatpak Chrome only — non-flatpak Chromium has the same root-owned/etcproblem and was never actually functional.A subtle Chromium detail handled here:
URLAllowlistdoesn't restrict on its own, it only carves exceptions out ofURLBlocklist. So when an allowlist is non-empty, a catch-all"*"is injected into the blocklist ahead of user entries — otherwise a kiosk allowlist would be a no-op.Wiring
BrowserModelives inshepherd-apias the single source of truth for the mode vocabulary, shared by config'sBrowserPolicyand host-api'sBrowserSpec.BrowserSpec+SpawnOptions.browserfield inshepherd-host-api(kept independent ofshepherd-config, likeFirewallSpec).shepherd-host-linux/src/browser.rs: policy-file write, shim argv construction, thecom.google.Chromegate, user-data-dir resolution, andwipe_profile_dir.stop.Tests
flatpake2e test (shepherd-e2e/tests/browser.rs) asserting the full daemon wiring — policy written, injection argv, wipe — with no real Chrome.#[ignore]real-Chrome test that drives the actual injection: allowlisted origin renders, blocked origin hits the policy interstitial, host/etc/opt/chromestays absent, profile created and wiped. Self-skips when the flatpak isn't installed;scripts/integration-tests/test-browser-flatpak.shorchestrates it.cargo build/test/clippy --workspaceandcargo fmt --allclean. README sections added for the config and host-linux crates; step-by-step design notes are indocs/ai/history/2026-06-13 00{1..7}….Running the gated real-Chrome test (after installing com.google.Chrome) falsified the design's policy-path assumption: the Flathub Chrome wrapper ignores the per-user config dir and reads managed policy only from the machine-wide, root-owned /etc/opt/chrome/policies/. Writing there would need root and hijack Chrome for every user on the box. Fix: inject our policy into the *sandbox's own* /etc (ephemeral, per-launch, per-user) instead of the host's. Launch Chrome through a shim: flatpak run --command=bash --env=SHEPHERD_POLICY=<file> com.google.Chrome \ -c 'ln -sf "$SHEPHERD_POLICY" /etc/opt/chrome/policies/managed/shepherd.json; exec /app/bin/chrome "$@"' bash <chrome flags> No root, host /etc untouched, policy scoped to that launch. Verified end to end against real Chrome (allow renders, block hits the enterprise interstitial, host /etc/opt/chrome stays absent, profile created + wiped) in 1.12s. - browser.rs: write_policy_file (now under config/shepherd-policies/), chrome_flatpak_argv (the shim), is_supported_browser_flatpak. Support is flatpak Chrome only; the process-kind generalization is dropped (non-flatpak Chromium has the same root-/etc problem and was never functional). - adapter.rs: browser block rebuilds the argv via chrome_flatpak_argv; other kinds warn + ignore. - tests: unit tests for the argv builder + gate; e2e now uses a stub flatpak to assert the full wiring with no real Chrome; gated test drives the real injection. Two real-Chrome gotchas handled in the test: a single-threaded marker server wedged on Chrome preconnect sockets (now thread-per-conn + read timeout), and DeveloperToolsAvailability=2 breaks --dump-dom because headless drives Chrome over DevTools (omitted in the headless probe only; correct for the real kiosk window). - README updated. cargo test --workspace / clippy / fmt all clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Chrome's URL-filter format needs allowlist entries to be scheme-qualified ("https://host/..."); a bare "host"/"host:port" isn't reliably matched and gets caught by the authoritative catch-all block (confirmed against real Chrome). Note this in config.example.toml and the shepherd-config README. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>code looks reasonable, pending testing
I tested the the example configuration -- Google Classroom with a strict allowlist -- and found two issues:
kioskdid not appear to be respected, and the full Chrome UI was visibleTwo bugs found while trying the browser activity via the example config: - Kiosk not respected: shepherd's sway compositor force-disables client fullscreen for non-launcher windows (to keep the HUD visible), so Chrome's --kiosk fell back to a toolbar'd window. Kiosk and app modes now both open a chromeless --app window that sway maximizes around the HUD. - All URLs blocked: the example used Chrome URL-filter patterns ("https://*.google.com/*") that match nothing — Chrome has no "*.host" wildcard and treats the path as a literal prefix, not a glob — so everything was caught by the authoritative catch-all blocklist. Corrected the example to plain scheme+host entries (a plain host already matches all subdomains) and hardened validate_url_pattern to reject the unsupported "*.host" and path-glob forms at config load. Updated unit/e2e tests, schema docs, and config comments accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QLcHRrgmhcWZrArd7BH6B1Both work great after the most recent fixes