Managed Web browser activity #63

Open
albert wants to merge 11 commits from u/albert/10/web-browser into main
Owner

Fixes #10

Fixes #10
Step 2 of the web-browser activity design: the config-layer types,
validation, and RawEntry -> Entry round-trip. No host-side spawn wiring
yet (step 3), no profile management (step 4), no example entry (step 5).

The browser activity is a composition layer, not a new EntryKind: pair
[entries.browser] with kind = flatpak (com.google.Chrome) and an optional
[entries.firewall] block.

- schema: RawBrowserConfig (profile_id, mode, start_url, url_allowlist/
  blocklist, lockdown flags defaulting on, wipe_on_exit) + browser field
  on RawEntry.
- policy: validated BrowserPolicy + BrowserMode enum, convert_browser_config,
  wired through Entry::from_raw. Lives in shepherd-config like FirewallPolicy.
- validation: validate_browser + helpers. profile_id is restricted to a
  safe single path segment (it becomes an on-disk dir name); start_url must
  be http(s); URL patterns reject empty/whitespace.
- README: new Browser section documenting the schema.

cargo build/test/clippy/fmt all clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Step 3 of the web-browser activity design: the Linux host adapter now
turns a browser policy into the two documented Chrome controls before
spawning. Profile management (--user-data-dir, wipe_on_exit) stays for
step 4.

- shepherd-api: shared BrowserMode enum (Kiosk/App/Windowed), used by both
  config's BrowserPolicy and host-api's BrowserSpec (replaces the
  config-local enum from step 2).
- shepherd-host-api: BrowserSpec + SpawnOptions.browser, mirroring
  BrowserPolicy and kept out of the config crate like FirewallSpec.
- shepherd-host-linux/browser.rs: write the Chromium managed-policy JSON
  (URLAllowlist/Blocklist + lockdown switches) under the com.google.Chrome
  flatpak config dir, and derive --kiosk/--app/window launch flags. A
  non-empty allowlist injects a catch-all "*" blocklist so the allowlist is
  authoritative (a bare allowlist does not restrict in Chromium). 11 tests.
- adapter.rs: materialize before the firewall block (so process-kind flags
  ride inside the firewall helper's argv); flatpak/process only, others warn.
- shepherdd main.rs + shepherd-http sessions.rs: convert Entry.browser ->
  SpawnOptions.browser at both spawn sites.

The managed-policy path is the design's documented location, isolated in a
const for easy adjustment after on-device testing.

cargo build/test/clippy/fmt all clean across the workspace.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Step 4 of the web-browser activity design: profile management in the Linux
host adapter.

- browser.rs: user_data_dir() resolves the per-profile dir
  ~/.var/app/com.google.Chrome/config/google-chrome/<profile_id>/ (path is
  identical inside/outside the flatpak sandbox; profile_id sanitized).
  chrome_flags() now prepends --user-data-dir. wipe_profile_dir() removes the
  tree (tolerant of a missing dir), in the adapter, never in Chrome.
- adapter.rs: new profile_wipes map keyed by pid. spawn() resolves the
  user-data-dir, passes it to chrome_flags, and records it for wiping when
  wipe_on_exit is set. The process monitor wipes it after the pid exits, so
  for flatpak the wipe waits until the Chrome instance is gone; remove-from-map
  makes it fire exactly once across natural exit and stop().
- Tests: user-data-dir path/sanitization, --user-data-dir ordering, and a
  tempdir-backed recursive-wipe test. README updated.

cargo build/test/clippy/fmt all clean across the workspace.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Step 5 (final) of the web-browser activity design: a chrome-school entry in
config.example.toml composing all three layers in one place --
kind = flatpak (com.google.Chrome), [entries.browser] (kiosk + Google
Workspace url_allowlist + lockdown + profile), and a [entries.firewall]
block.

The firewall block is commented out: a default-deny IP firewall needs the
current Google/Workspace CIDRs, which drift and can't be hardcoded honestly,
so the browser url_allowlist is the authoritative host control (works out of
the box) and the firewall is documented as opt-in coarse defense-in-depth
with a pointer to the gstatic ipranges source.

Verified with `validate-config config.example.toml` -> valid.

Schema/host docs were already added alongside their implementation (config
and host-linux READMEs); the screenshot-driven top-level README is left for a
real on-device capture.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make the browser materialization automatically testable end to end, without
needing real Chrome.

0. Injectable browser root: write_managed_policy/user_data_dir now take a
   root path instead of calling dirs::home_dir() directly (so tests can't
   pollute the real ~/.var/app/...). LinuxHost resolves browser_root once from
   SHEPHERD_BROWSER_ROOT (test knob, mirrors SHEPHERD_FIREWALL_HELPER) or home.
   user_data_dir is now infallible.
1. Golden snapshot test of the managed-policy JSON, locking the key/value
   mapping incl. the URLBlocklist ["*"] injection.
2. Adapter wiring tests (process kind, fake chrome): assert the policy file is
   written and --user-data-dir/--kiosk/start-url are appended to the real argv;
   and that wipe_on_exit removes the profile dir via the process monitor.
3. e2e boot test (shepherd-e2e/tests/browser.rs, #[ignore], mirrors
   firewall.rs): real sway+shepherdd, launches a browser entry through the HTTP
   API, asserts policy + flags + wipe through the full stack. Verified green on
   a configured host.

Still out of scope: verifying Chrome's own behavior against the policy
(suggestion item 4, a gated headless --dump-dom check) -- the path consts
remain the single knobs.

cargo build/test/clippy/fmt all clean across the workspace.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Item 4 of the testability plan: verify the two assumptions only real Chrome
can confirm -- that Flatpak Chrome reads the managed policy at the path we
write it to (so the URL allow/blocklist is enforced), and that --user-data-dir
lands where we wipe.

- browser.rs: #[ignore] unit test real_flatpak_chrome_enforces_policy_and_
  user_data_dir. In-crate so it can use the private path consts; no harness
  needed (talks to Chrome directly). HOME is redirected to a tempdir so the
  real ~/.var/app/com.google.Chrome is never touched; XDG_DATA_HOME stays real
  so flatpak finds the install. Two loopback servers serve unique markers; only
  one origin is allowlisted, so the blocked origin rendering its marker (or not)
  is an unambiguous signal of whether the policy path is correct. Also asserts
  Chrome created --user-data-dir at our path, then wipes it. Failure messages
  name the offending const. Self-skips when the flatpak isn't installed.
- scripts/integration-tests/test-browser-flatpak.sh: orchestrator mirroring
  test-firewall-flatpak.sh.

Skip path verified here (Chrome not installed); the real assertions run via the
orchestrator on a host with com.google.Chrome, like the firewall flatpak test.

cargo test/clippy/fmt clean; normal `cargo test` skips the ignored test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Document scheme-qualified browser allowlist entries (#10)
All checks were successful
CI / ShellCheck (pull_request) Successful in 7s
CI / CI image (pull_request) Successful in 21s
CI / Rustfmt (pull_request) Successful in 8s
CI / Build (pull_request) Successful in 2m29s
CI / Test (pull_request) Successful in 2m46s
CI / Android portability (shepherd-media-core) (pull_request) Successful in 23s
CI / E2E (pull_request) Successful in 2m58s
CI / Clippy (pull_request) Successful in 3m39s
CI / Firewall E2E (pull_request) Successful in 4m0s
1a8e2a43ec
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>
Merge branch 'main' into u/albert/10/web-browser
All checks were successful
CI / ShellCheck (pull_request) Successful in 7s
CI / CI image (pull_request) Successful in 10m56s
CI / Rustfmt (pull_request) Successful in 13s
CI / Clippy (pull_request) Successful in 2m44s
CI / Test (pull_request) Successful in 3m15s
CI / Android portability (shepherd-media-core) (pull_request) Successful in 34s
CI / Build (pull_request) Successful in 3m26s
CI / E2E (pull_request) Successful in 3m36s
CI / Firewall E2E (pull_request) Successful in 4m24s
0a3b11d58b
Merge branch 'main' into u/albert/10/web-browser
All checks were successful
CI / ShellCheck (pull_request) Successful in 7s
CI / CI image (pull_request) Successful in 20s
CI / Rustfmt (pull_request) Successful in 8s
CI / Clippy (pull_request) Successful in 2m33s
CI / Build (pull_request) Successful in 3m3s
CI / Test (pull_request) Successful in 3m5s
CI / Android portability (shepherd-media-core) (pull_request) Successful in 29s
CI / E2E (pull_request) Successful in 3m30s
CI / Firewall E2E (pull_request) Successful in 4m10s
2b4e0a58db
Merge branch 'main' into u/albert/10/web-browser
Some checks failed
CI / ShellCheck (pull_request) Successful in 15s
CI / CI image (pull_request) Successful in 38s
CI / Rustfmt (pull_request) Successful in 10s
CI / Clippy (pull_request) Failing after 2m1s
CI / Test (pull_request) Failing after 2m27s
CI / Android portability (shepherd-media-core) (pull_request) Successful in 26s
CI / Build (pull_request) Successful in 2m52s
CI / E2E (pull_request) Successful in 3m8s
CI / Firewall E2E (pull_request) Successful in 3m16s
f0cf34140a
Some checks failed
CI / ShellCheck (pull_request) Successful in 15s
Required
Details
CI / CI image (pull_request) Successful in 38s
Required
Details
CI / Rustfmt (pull_request) Successful in 10s
Required
Details
CI / Clippy (pull_request) Failing after 2m1s
Required
Details
CI / Test (pull_request) Failing after 2m27s
Required
Details
CI / Android portability (shepherd-media-core) (pull_request) Successful in 26s
Required
Details
CI / Build (pull_request) Successful in 2m52s
Required
Details
CI / E2E (pull_request) Successful in 3m8s
Required
Details
CI / Firewall E2E (pull_request) Successful in 3m16s
Required
Details
Some required checks were not successful.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin u/albert/10/web-browser:u/albert/10/web-browser
git switch u/albert/10/web-browser
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
albert/shepherd-launcher!63
No description provided.