Managed Web browser activity #63

Merged
albert merged 16 commits from u/albert/10/web-browser into main 2026-06-29 02:47:32 +00:00
Owner

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: any flatpak entry for com.google.Chrome can 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

  • Authoritative URL allowlisting, enforced by Chrome itself (no extensions). A non-empty url_allowlist blocks everything not matched; url_blocklist carves out exceptions after it.
  • Launch modes: kiosk (fullscreen, no chrome), app (single app window), or windowed.
  • Lockdown switches (all default on): disable dev tools, incognito, and extension installation.
  • Per-profile, on-disk user-data-dir keyed by profile_id — entries sharing an id share cookies/logins; unique ids are isolated.
  • wipe_on_exit clears the profile directory after the session ends (handled by shepherd, never trusted to Chrome).
  • Optional [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 in shepherd-config — typed, validated, and round-tripped from RawEntry to the validated Entry. The security-relevant validation is profile_id (must be a single safe path segment — it becomes an on-disk directory name); start_url and URL patterns get light checks with Chrome as the real authority. config.example.toml gains a fully-commented chrome-school entry 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 passes validate-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 /etc per 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 /etc never touched. As a consequence, browser support is now flatpak Chrome only — non-flatpak Chromium has the same root-owned /etc problem and was never actually functional.

A subtle Chromium detail handled here: URLAllowlist doesn't restrict on its own, it only carves exceptions out of URLBlocklist. 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

  • BrowserMode lives in shepherd-api as the single source of truth for the mode vocabulary, shared by config's BrowserPolicy and host-api's BrowserSpec.
  • New BrowserSpec + SpawnOptions.browser field in shepherd-host-api (kept independent of shepherd-config, like FirewallSpec).
  • New shepherd-host-linux/src/browser.rs: policy-file write, shim argv construction, the com.google.Chrome gate, user-data-dir resolution, and wipe_profile_dir.
  • The adapter writes the policy and rebuilds the argv on spawn (before the firewall block, so flags ride inside the firewall-wrapped argv); profile wipes are recorded against the activity pid and fire exactly once when the process monitor reaps the exit — covering both natural exits and stop.

Tests

  • Unit tests across schema parse/validation, policy JSON, Chrome flags, user-data-dir sanitization, flag ordering, and tempdir-backed wipe.
  • A stub-flatpak e2e test (shepherd-e2e/tests/browser.rs) asserting the full daemon wiring — policy written, injection argv, wipe — with no real Chrome.
  • A gated #[ignore] real-Chrome test that drives the actual injection: allowlisted origin renders, blocked origin hits the policy interstitial, host /etc/opt/chrome stays absent, profile created and wiped. Self-skips when the flatpak isn't installed; scripts/integration-tests/test-browser-flatpak.sh orchestrates it.

cargo build/test/clippy --workspace and cargo fmt --all clean. README sections added for the config and host-linux crates; step-by-step design notes are in docs/ai/history/2026-06-13 00{1..7}….

## 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`: any `flatpak` entry for `com.google.Chrome` can 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 - **Authoritative URL allowlisting**, enforced by Chrome itself (no extensions). A non-empty `url_allowlist` blocks everything not matched; `url_blocklist` carves out exceptions after it. - **Launch modes**: `kiosk` (fullscreen, no chrome), `app` (single app window), or `windowed`. - **Lockdown switches** (all default on): disable dev tools, incognito, and extension installation. - **Per-profile, on-disk user-data-dir** keyed by `profile_id` — entries sharing an id share cookies/logins; unique ids are isolated. - **`wipe_on_exit`** clears the profile directory after the session ends (handled by shepherd, never trusted to Chrome). - Optional `[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 in `shepherd-config` — typed, validated, and round-tripped from `RawEntry` to the validated `Entry`. The security-relevant validation is `profile_id` (must be a single safe path segment — it becomes an on-disk directory name); `start_url` and URL patterns get light checks with Chrome as the real authority. `config.example.toml` gains a fully-commented `chrome-school` entry 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 passes `validate-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 `/etc` per 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 `/etc` never touched. As a consequence, browser support is now **flatpak Chrome only** — non-flatpak Chromium has the same root-owned `/etc` problem and was never actually functional. A subtle Chromium detail handled here: `URLAllowlist` doesn't restrict on its own, it only carves exceptions out of `URLBlocklist`. 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 - `BrowserMode` lives in `shepherd-api` as the single source of truth for the mode vocabulary, shared by config's `BrowserPolicy` and host-api's `BrowserSpec`. - New `BrowserSpec` + `SpawnOptions.browser` field in `shepherd-host-api` (kept independent of `shepherd-config`, like `FirewallSpec`). - New `shepherd-host-linux/src/browser.rs`: policy-file write, shim argv construction, the `com.google.Chrome` gate, user-data-dir resolution, and `wipe_profile_dir`. - The adapter writes the policy and rebuilds the argv on spawn (before the firewall block, so flags ride inside the firewall-wrapped argv); profile wipes are recorded against the activity pid and fire exactly once when the process monitor reaps the exit — covering both natural exits and `stop`. ### Tests - Unit tests across schema parse/validation, policy JSON, Chrome flags, user-data-dir sanitization, flag ordering, and tempdir-backed wipe. - A stub-`flatpak` e2e test (`shepherd-e2e/tests/browser.rs`) asserting the full daemon wiring — policy written, injection argv, wipe — with no real Chrome. - A gated `#[ignore]` real-Chrome test that drives the actual injection: allowlisted origin renders, blocked origin hits the policy interstitial, host `/etc/opt/chrome` stays absent, profile created and wiped. Self-skips when the flatpak isn't installed; `scripts/integration-tests/test-browser-flatpak.sh` orchestrates it. `cargo build/test/clippy --workspace` and `cargo fmt --all` clean. README sections added for the config and host-linux crates; step-by-step design notes are in `docs/ai/history/2026-06-13 00{1..7}…`.
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
Merge branch 'main' into u/albert/10/web-browser
Some checks failed
CI / ShellCheck (pull_request) Successful in 7s
CI / CI image (pull_request) Successful in 24s
CI / Rustfmt (pull_request) Successful in 12s
CI / Clippy (pull_request) Failing after 2m31s
CI / Test (pull_request) Failing after 2m53s
CI / Android portability (shepherd-media-core) (pull_request) Successful in 27s
CI / Build (pull_request) Successful in 3m16s
CI / E2E (pull_request) Successful in 3m29s
CI / Firewall E2E (pull_request) Successful in 4m13s
621e9aa551
Fix test build: add missing browser field to Entry literal
All checks were successful
CI / ShellCheck (pull_request) Successful in 7s
CI / CI image (pull_request) Successful in 19s
CI / Rustfmt (pull_request) Successful in 9s
CI / Clippy (pull_request) Successful in 2m35s
CI / Android portability (shepherd-media-core) (pull_request) Successful in 29s
CI / Build (pull_request) Successful in 3m11s
CI / Test (pull_request) Successful in 3m10s
CI / E2E (pull_request) Successful in 3m30s
CI / Firewall E2E (pull_request) Successful in 4m12s
81677be953
The merge of main (621e9aa) brought in test_enable_override_bypasses_daily_quota
(PR #67), which constructs an Entry literal. This branch added the
browser: Option<BrowserPolicy> field to Entry, so the merged-in test failed to
compile with "missing field `browser`", breaking both the Test and Clippy CI
jobs (clippy --all-targets compiles tests too).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
Owner

code looks reasonable, pending testing

code looks reasonable, pending testing
Author
Owner

I tested the the example configuration -- Google Classroom with a strict allowlist -- and found two issues:

  • kiosk did not appear to be respected, and the full Chrome UI was visible
  • none of the URLs from the allowlist were actually allowed -- all were coming up "blocked by your organization" (which at least tells us that the allowlist functionality is doing something)
I tested the the example configuration -- Google Classroom with a strict allowlist -- and found two issues: * `kiosk` did not appear to be respected, and the full Chrome UI was visible * none of the URLs from the allowlist were actually allowed -- all were coming up "blocked by your organization" (which at least tells us that the allowlist functionality is doing something)
Fix browser kiosk presentation and URL-allowlist format (#10)
Some checks failed
CI / ShellCheck (pull_request) Successful in 8s
CI / CI image (pull_request) Successful in 21s
CI / Firewall E2E (pull_request) Waiting to run
CI / Android portability (shepherd-media-core) (pull_request) Waiting to run
CI / Rustfmt (pull_request) Has been cancelled
CI / Build (pull_request) Has been cancelled
CI / Test (pull_request) Has been cancelled
CI / Clippy (pull_request) Has been cancelled
CI / E2E (pull_request) Has been cancelled
4d60575f4c
Two 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_01QLcHRrgmhcWZrArd7BH6B1
Author
Owner

Both work great after the most recent fixes

Both work great after the most recent fixes
Merge branch 'main' into u/albert/10/web-browser
Some checks failed
CI / ShellCheck (pull_request) Successful in 7s
CI / CI image (pull_request) Successful in 19s
CI / Rustfmt (pull_request) Successful in 7s
CI / Test (pull_request) Successful in 2m48s
CI / Build (pull_request) Successful in 2m52s
CI / Android portability (shepherd-media-core) (pull_request) Successful in 21s
CI / E2E (pull_request) Failing after 3m18s
CI / Clippy (pull_request) Successful in 3m17s
CI / Firewall E2E (pull_request) Successful in 3m46s
12404072ce
Fix browser e2e config rejected by hardened URL validator
All checks were successful
CI / ShellCheck (pull_request) Successful in 7s
CI / CI image (pull_request) Successful in 19s
CI / Rustfmt (pull_request) Successful in 9s
CI / Test (pull_request) Successful in 2m55s
CI / Build (pull_request) Successful in 3m0s
CI / Android portability (shepherd-media-core) (pull_request) Successful in 24s
CI / E2E (pull_request) Successful in 3m22s
CI / Clippy (pull_request) Successful in 3m21s
CI / Firewall E2E (pull_request) Successful in 3m50s
da97b2f32f
The e2e BROWSER_CONFIG still used "https://*.google.com/*", which the new
validate_url_pattern now rejects (unsupported wildcard host + path glob). That
made shepherdd fail config validation at startup, so it never served
/api/v1/health and the test timed out — the only red job on PR #63's CI. Use a
valid plain-host pattern ("https://google.com"), which already matches all
subdomains.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLcHRrgmhcWZrArd7BH6B1
albert merged commit be24b95724 into main 2026-06-29 02:47:32 +00:00
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.