Skip to content

feat(ansible): provision the fleet browser ladder via a standalone fleet_browsers profile - #35

Merged
undeemed merged 3 commits into
mainfrom
fm/host-fleet-browser-b1
Oct 2, 2026
Merged

undeemed merged 3 commits into
mainfrom
fm/host-fleet-browser-b1

Conversation

@undeemed

@undeemed undeemed commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

Intent

Drive his browser yourself when work needs something only his session can reach - a credential, a console setting, a third-party sign-in, a dashboard toggle. Do not hand him click-by-click instructions.

Context: that is the captain's standing order, and fleet rule says every browser use goes through the shared fleet-browser ladder (obscura CDP tier on 127.0.0.1:9222 by default, Chrome tier, noVNC tier where a human must see or drive it; one canonical cookie jar synced across tiers). His newer rule also requires before/after screenshots for every UI change. Since the fleet moved to this host on 2026-10-01 the ladder does not exist here: no fleet-browser* systemd user units, nothing listening on :9222, so a worker on 2026-10-02 could not take screenshots. Code-Factory's ansible/tasks/fleet-browsers.yml (imported from ansible/site.yml under the fleet tag when the fleet_guards profile is on) owns the ladder: the obscura tier, sync and gc timers, and env injection.

Applied on the fleet host (Ubuntu 26.04)

This branch's recipe was applied on the host the fleet moved to on 2026-10-01, using that host's .local/host.yml plus fleet_browsers: true, narrowed to the ladder:

ansible-playbook -i ansible/inventory.yml ansible/site.yml --extra-vars @<host.yml + fleet_browsers: true> \
  --tags account,fleet_browsers --skip-tags journald

account only resolves the account facts the ladder needs (no change). Changed: ~/oss-fleet/browsers/ (scripts, latest Obscura release, symlinks), one env.sh source line in ~/.profile and ~/.bashrc, 7 fleet-browser-* user units + 3 enable links, then obscura and the sync/gc timers started. No packages, nothing under /etc, no Herdr/tailscale/ssh/docker change. A second apply reported changed=0.

Evidence:

  • fleet-browser-obscura.service active, listening on 127.0.0.1:9222; curl /json/version returns "webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/browser".
  • chrome-devtools-axi open https://example.com + screenshot from a fresh shell (CHROME_DEVTOOLS_AXI_BROWSER_URL=http://127.0.0.1:9222 from env.sh) produced a screenshot through obscura.
  • fleet-browser-sync.timer (2 min) and fleet-browser-gc.timer (5 min) active; first fleet-browser-sync.service run Result=success (cookie-sync: canonical 0 cookies), which needed the bun-on-PATH fix.

After merge, that host's .local/host.yml needs profiles.fleet_browsers: true (the current main schema rejects the key, so it is set after merge). Not yet applied there: the Herdr unit's ladder environment (needs a run with the herdr tag, which restarts Herdr) and the host-wide journald cap (journald tag, restarts systemd-journald).

What Changed

  • Added a profiles.fleet_browsers flag (default false, added to config/default.yml and schemas/factory.schema.json). factory_fleet_browsers_enabled is true when either fleet_browsers or fleet_guards is on. It now gates the import of tasks/fleet-browsers.yml in site.yml (new fleet_browsers tag) and the Obscura release resolution, so the ladder no longer needs the Supabase CLI. The fleet-browser unit lists moved from factory_fleet_units / factory_fleet_enabled_units into factory_fleet_browser_units / factory_fleet_browser_enabled_units. fleet-browsers.yml now loops over those lists and creates the default.target.wants and timers.target.wants directories itself. verify.yml now checks the browser scripts, units and enable symlinks.
  • herdr.service.j2 now sets the ladder environment when the ladder is enabled: the fleet browsers dir on PATH, CHROME_DEVTOOLS_AXI_BROWSER_URL=http://127.0.0.1:9222 and FLEET_BROWSER_TIER=obscura. This replaces the lineinfile edit of the installed herdr.service. fleet/browsers/fleet-browser now defaults BUN to bun from PATH instead of $HOME/.bun/bin/bun. The journald rate-limit tasks are tagged journald.
  • test_configuration.py now checks that obscura is resolved when either profile is on and supabase only for fleet_guards. CONTRIBUTING.md and the docs (configuration, dependencies, fleet-guards, capacity, agent-host-move) are updated for the new profile.

Risk Assessment

✅ Low: The change is a bounded Ansible and config refactor that adds a fleet_browsers profile. Prior-round decisions are implemented consistently, fleet_guards hosts keep the ladder, and I found no concrete failing path.

Testing

I applied the real playbook with the ladder tags into a disposable home, using a fake-sudo shim and the real Obscura 0.2.3 download. I checked all three profile combinations, idempotency, the verify assertions, and a live Obscura tier driven over CDP, then removed the sandbox. All scenarios passed. The one side effect on the operator's real Obscura unit is listed in the last scenario below.

  • Live validation: ✅ go - 10 of 10 scenarios driven live against the product
Scenario Result Live Evidence
Applying the playbook with profiles.fleet_browsers on (fleet_guards off) provisions the ladder: scripts, Obscura release and symlinks, shell-profile env, 7 units, static enables for the Obscura servic… ✅ pass live apply-fleet_browsers-run1.log; profile-matrix.txt
An existing fleet_guards-only host (no fleet_browsers key) still gets the full ladder, with CHROME_DEVTOOLS_AXI_BROWSER_URL, FLEET_BROWSER_TIER and the browsers PATH entry in herdr.service ✅ pass live apply-fleet_guards-only.log; profile-matrix.txt
Adversarial: with neither profile on, nothing of the ladder is installed and the Herdr unit carries no browser env or PATH entry ✅ pass live apply-neither-profile.log; profile-matrix.txt
A second unchanged apply reports changed=0 ✅ pass live apply-fleet_browsers-run2.log (changed=0)
A ladder-only run (--tags fleet_browsers) leaves herdr.service untouched; --tags herdr,fleet_browsers renders it with the ladder env (matches the new docs/fleet-guards.md text) ✅ pass live herdr.service absent after the fleet_browsers-only run; present with the ladder env after the herdr+fleet_browsers run
--skip-tags journald applies the ladder without touching /etc/systemd/journald.conf.d ✅ pass live All applies ran with --skip-tags journald; no journald tasks ran
verify.yml checks the ladder outputs and fails naming a deleted ladder output ✅ pass live verify-missing-ladder-outputs.txt lists env.sh and fleet-browser-gc.timer. Other missing items in that run (herdr, btop-bin, sentrux, and .profile on the no-profile host) are not ladder outputs: the s…
Starting the Obscura tier from the rendered unit's ExecStart puts CDP on 127.0.0.1:9222. fleet-browser status goes from down to up, and a worker navigates and takes a screenshot (the failure the int… ✅ pass live live-obscura-ladder-transcript.txt; ladder-obscura-screenshot.png
fleet-browser resolves bun from PATH (the BUN=bun change) when the home has no ~/.bun ✅ pass live live-fleet-browser-sync-bun-from-path.txt: fleet-browser sync exited 0 with bun found at ~/.local/bin/bun
Side effect on the operator's real Obscura unit ✅ pass live The unshare -Urn namespace isolates the network but not the user systemd bus. My fleet-browser sync therefore restarted the operator's real fleet-browser-obscura.service once (ActiveEnterTimesta…

Screenshot taken through the CDP tier on 127.0.0.1:9222, started from the playbook-rendered unit

Evidence: Live Obscura ladder transcript: :9222 down before and up after, env.sh values, CDP navigate, evaluate and screenshot
--- listeners before:
(nothing on :9222)
--- fleet-browser status BEFORE obscura:
TIER     PORT   STATE     CLIENTS  NOTE
obscura  9222   down      0        default
chrome   9522   down      0        fallback: pixel-critical evidence (fleet-browser up chrome)
vnc      9523   down      0        last resort: human eyes/hands; noVNC http://127.0.0.1:6909/vnc.html (fleet-browser up vnc)
session: 0 cookies in canonical jar ($S/home/.fleet-browser/cookies.json); last sync: 
--- ExecStart from rendered unit: $S/home/oss-fleet/browsers/obscura serve --host 127.0.0.1 --port 9222 --allow-private-network --storage-dir $S/home/.fleet-browser/obscura
--- listeners after:
LISTEN 0      128        127.0.0.1:9222      0.0.0.0:*   
LISTEN 0      5          127.0.0.1:8099      0.0.0.0:*   
--- env.sh in a login shell:
http://127.0.0.1:9222 obscura
--- fleet-browser status AFTER:
TIER     PORT   STATE     CLIENTS  NOTE
obscura  9222   up        0        default
chrome   9522   down      0        fallback: pixel-critical evidence (fleet-browser up chrome)
vnc      9523   down      0        last resort: human eyes/hands; noVNC http://127.0.0.1:6909/vnc.html (fleet-browser up vnc)
session: 0 cookies in canonical jar ($S/home/.fleet-browser/cookies.json); last sync: 
--- CDP drive:
version {"Browser":"Chrome/145.0.0.0","Protocol-Version":"1.3","User-Agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36","V8-Version":"14.5.0.0","WebKit-Version":"537.36","webSocketDebuggerUrl":"ws://127.0.0.1:9222/devtools/browser"}
createTarget {"id":1,"result":{"targetId":"page-1"}}
navigate {"id":3,"result":{"frameId":"page-1","loaderId":"loader-26d5dfe4-0cf0-4900-8ec9-72191ca5a45e"},"sessionId":"page-1-session-1"}
title-text {"result":{"type":"string","description":"fleet ladder live","objectId":"{\"injectedScriptId\":1,\"id\":1}","value":"fleet ladder live"}}
screenshot bytes 9123
Evidence: Apply run 1 and run 2 with fleet_browsers only (run 2 reports changed=0)

PLAY [Code Factory native provisioning] ****************************************

TASK [Gathering Facts] *********************************************************
ok: [localhost]

TASK [Assert the configuration document matches the contract] ******************
ok: [localhost]

TASK [Assert the host is a supported Ubuntu release] ***************************
[WARNING]: Deprecation warnings can be disabled by setting `deprecation_warnings=False` in ansible.cfg.
[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24.
Origin: ~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/ansible/tasks/preflight.yml:30:15

28       - ansible_distribution == 'Ubuntu'
29       - ansible_distribution_version in factory_supported_ubuntu_releases
30     fail_msg: >-
                 ^ column 15

Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead.

[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24.
Origin: ~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/ansible/tasks/preflight.yml:28:9

26   ansible.builtin.assert:
27     that:
28       - ansible_distribution == 'Ubuntu'
           ^ column 9

Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead.

[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24.
Origin: ~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/ansible/tasks/preflight.yml:29:9

27     that:
28       - ansible_distribution == 'Ubuntu'
29       - ansible_distribution_version in factory_supported_ubuntu_releases
           ^ column 9

Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead.

ok: [localhost]

TASK [Assert the CPU architecture has toolchain assets] ************************
[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24.
Origin: ~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/ansible/tasks/preflight.yml:42:15

40       - factory_platform | length > 0
41       - factory_deb_arch | length > 0
42     fail_msg: >-
                 ^ column 15

Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead.

[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24.
Origin: ~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/ansible/group_vars/all.yml:37:19

35   aarch64: linux-aarch64
36   arm64: linux-aarch64
37 factory_platform: "{{ factory_platform_map[ansible_architecture] | default('') }}"
                     ^ column 19

Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead.

[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24.
Origin: ~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/ansible/group_vars/all.yml:43:19

41   aarch64: arm64
42   arm64: arm64
43 factory_deb_arch: "{{ factory_deb_arch_map[ansible_architecture] | default('') }}"
                     ^ column 19

Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead.

ok: [localhost]

TASK [Build the list of Main-owned inputs this run requires] *******************
ok: [localhost]

TASK [Stat every required input] ***********************************************
ok: [localhost] => (item=scripts/install_tools.py)
ok: [localhost] => (item=maintenance/herdr-spaces.py)
ok: [localhost] => (item=patches/koncreet/ubuntu-26.04.patch)

TASK [Fail when a contract input is missing] ***********************************
ok: [localhost]

TASK [Resolve the latest tool releases] ****************************************
ok: [localhost]

TASK [Record the resolved releases] ********************************************
ok: [localhost]

TASK [Warn about optional tools skipped by the lookup] *************************
skipping: [localhost]

TASK [Show the resolved provisioning plan] *************************************
[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24.
Origin: ~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/ansible/tasks/preflight.yml:115:9

113     msg:
114       - "account            : {{ factory_cfg.user }} (home {{ factory_cfg.home }}, workspace {{ factory_cfg.works...
115       - "platform           : {{ ansible_distribution }} {{ ansible_distribution_version }} {{ ansible_architectu...
            ^ column 9

Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead.

ok: [localhost] => {
    "msg": [
        "account            : administrator (home ~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/.livetest/home, workspace ~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/.livetest/home/Dev)",
        "platform           : Ubuntu 26.04 x86_64 -> linux-x86_64 / amd64",
        "herdr              : 0.9.3 (latest release) at ~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/.livetest/home/.local/share/code-factory/tools/herdr/0.9.3/linux-x86_64/herdr",
        "omp                : not installed (latest release)",
        "profiles           : fleet_browsers",
        "services           : start_services=False linger=False",
        "pruner runtime     : disabled",
        "check mode         : False"
    ]
}

TASK [Ensure the Herdr configuration directory] ********************************
ok: [localhost]

TASK [Render the Herdr static configuration] ***********************************
ok: [localhost]

TASK [Render the Herdr user unit] **********************************************
ok: [localhost]

TASK [Ensure the user default.target.wants directory] **************************
ok: [localhost]

TASK [Statically enable the Herdr user unit] ***********************************
ok: [localhost]

TASK [Enable and start the Herdr user service] *********************************
[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24.
Origin: ~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/ansible/group_vars/all.yml:113:24

111
112 # Escalate to the target account only when we are not already that account.
113 factory_become_target: "{{ (ansible_user_id | default('')) != factory_cfg.user }}"
                           ^ column 24

Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead.

skipping: [localhost]

TASK [Report the deferred service start] ***************************************
ok: [localhost] => {
    "msg": "start_services is false: herdr.service is written and statically enabled at ~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/.livetest/home/.config/systemd/user/herdr.service but not started. It comes up on the first boot of a host that has lingering enabled for administrator."
}

TASK [Install the Herdr Spaces reporter] ***************************************
ok: [localhost]

TASK [Install the Herdr sidebar client helper] *********************************
ok: [localhost]

TASK [Install the btop launcher] ***********************************************
ok: [localhost]

TASK [Ensure the btop configuration directory] *********************************
ok: [localhost]

TASK [Install the btop configuration] ******************************************
ok: [localhost]

TASK [Render the Herdr Spaces reporter unit and timer] *************************
ok: [localhost] => (item=herdr-spaces.service)
ok: [localhost] => (item=herdr-spaces.timer)

TASK [Ensure the user timers.target.wants directory] ***************************
ok: [localhost]

TASK [Statically enable the Herdr Spaces reporter timer] ***********************
ok: [localhost]

TASK [Enable and start the Herdr Spaces reporter timer] ************************
skipping: [localhost]

TASK [Ensure the omp extensions directory] *************************************
skipping: [localhost]

TASK [Checksum the Herdr omp integration before install] ***********************
skipping: [localhost]

TASK [Run herdr integration install omp] ***************************************
skipping: [localhost]

TASK [Checksum the Herdr omp integration after install] ************************
skipping: [localhost]

TASK [Ensure fleet browser directories] ****************************************
ok: [localhost] => (item=~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/.livetest/home/oss-fleet)
ok: [localhost] => (item=~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/.livetest/home/oss-fleet/browsers)
ok: [localhost] => (item=~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/.livetest/home/.fleet-browser)
ok: [localhost] => (item=~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/.livetest/home/.config/systemd/user/default.target.wants)
ok: [localhost] => (item=~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/.livetest/home/.config/systemd/user/timers.target.wants)

TASK [Install the fleet browser scripts] ***************************************
ok: [localhost] => (item={'src': 'browsers/fleet-browser', 'dest': '~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/.livetest/home/oss-fleet/browsers/fleet-browser', 'mode': '0755'})
ok: [localhost] => (item={'src': 'browsers/cookie-sync.ts', 'dest': '~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/.livetest/home/oss-fleet/browsers/cookie-sync.ts', 'mode': '0644'})
ok: [localhost] => (item={'src': 'browsers/env.sh', 'dest': '~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/.livetest/home/oss-fleet/browsers/env.sh', 'mode': '0644'})

TASK [Stat the resolved Obscura release] ***************************************
ok: [localhost]

TASK [Create the Obscura release directory] ************************************
skipping: [localhost]

TASK [Fetch the obscura tarball] ***********************************************
skipping: [localhost]

TASK [Extract obscura binaries] ************************************************
skipping: [localhost]

TASK [Clean up the obscura tarball] ********************************************
skipping: [localhost]

TASK [Point the fleet browser at the resolved Obscura release] *****************
ok: [localhost] => (item=obscura)
ok: [localhost] => (item=obscura-worker)

TASK [Inject the shared-browser env into shell profiles] ***********************
ok: [localhost] => (item=.profile)
ok: [localhost] => (item=.bashrc)

TASK [Render fleet browser units] **********************************************
ok: [localhost] => (item=fleet-browser-obscura.service)
ok: [localhost] => (item=fleet-browser-chrome.service)
ok: [localhost] => (item=fleet-browser-vnc.service)
ok: [localhost] => (item=fleet-browser-sync.service)
ok: [localhost] => (item=fleet-browser-sync.timer)
ok: [localhost] => (item=fleet-browser-gc.service)
ok: [localhost] => (item=fleet-browser-gc.timer)

TASK [Statically enable fleet browser units] ***********************************
ok: [localhost] => (item=fleet-browser-obscura.service)
ok: [localhost] => (item=fleet-browser-sync.timer)
ok: [localhost] => (item=fleet-browser-gc.timer)

TASK [Enable and start fleet browser units] ************************************
skipping: [localhost] => (item=fleet-browser-obscura.service) 
skipping: [localhost] => (item=fleet-browser-sync.timer) 
skipping: [localhost] => (item=fleet-browser-gc.timer) 
skipping: [localhost]

TASK [Report the deferred fleet browser start] *********************************
ok: [localhost] => {
    "msg": "start_services is false: obscura + sync/gc units are written and statically enabled but not started here."
}

TASK [Apply pending unit reloads and restarts before verification] *************

PLAY RECAP *********************************************************************
localhost                  : ok=32   changed=0    unreachable=0    failed=0    skipped=12   rescued=0    ignored=0   
Evidence: Apply with fleet_guards only (existing host, no fleet_browsers key)

PLAY [Code Factory native provisioning] ****************************************

TASK [Gathering Facts] *********************************************************
ok: [localhost]

TASK [Assert the configuration document matches the contract] ******************
ok: [localhost]

TASK [Assert the host is a supported Ubuntu release] ***************************
[WARNING]: Deprecation warnings can be disabled by setting `deprecation_warnings=False` in ansible.cfg.
[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24.
Origin: ~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/ansible/tasks/preflight.yml:30:15

28       - ansible_distribution == 'Ubuntu'
29       - ansible_distribution_version in factory_supported_ubuntu_releases
30     fail_msg: >-
                 ^ column 15

Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead.

[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24.
Origin: ~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/ansible/tasks/preflight.yml:28:9

26   ansible.builtin.assert:
27     that:
28       - ansible_distribution == 'Ubuntu'
           ^ column 9

Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead.

[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24.
Origin: ~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/ansible/tasks/preflight.yml:29:9

27     that:
28       - ansible_distribution == 'Ubuntu'
29       - ansible_distribution_version in factory_supported_ubuntu_releases
           ^ column 9

Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead.

ok: [localhost]

TASK [Assert the CPU architecture has toolchain assets] ************************
[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24.
Origin: ~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/ansible/tasks/preflight.yml:42:15

40       - factory_platform | length > 0
41       - factory_deb_arch | length > 0
42     fail_msg: >-
                 ^ column 15

Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead.

[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24.
Origin: ~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/ansible/group_vars/all.yml:37:19

35   aarch64: linux-aarch64
36   arm64: linux-aarch64
37 factory_platform: "{{ factory_platform_map[ansible_architecture] | default('') }}"
                     ^ column 19

Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead.

[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24.
Origin: ~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/ansible/group_vars/all.yml:43:19

41   aarch64: arm64
42   arm64: arm64
43 factory_deb_arch: "{{ factory_deb_arch_map[ansible_architecture] | default('') }}"
                     ^ column 19

Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead.

ok: [localhost]

TASK [Build the list of Main-owned inputs this run requires] *******************
ok: [localhost]

TASK [Stat every required input] ***********************************************
ok: [localhost] => (item=scripts/install_tools.py)
ok: [localhost] => (item=maintenance/herdr-spaces.py)
ok: [localhost] => (item=patches/koncreet/ubuntu-26.04.patch)

TASK [Fail when a contract input is missing] ***********************************
ok: [localhost]

TASK [Resolve the latest tool releases] ****************************************
ok: [localhost]

TASK [Record the resolved releases] ********************************************
ok: [localhost]

TASK [Warn about optional tools skipped by the lookup] *************************
skipping: [localhost]

TASK [Show the resolved provisioning plan] *************************************
[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24.
Origin: ~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/ansible/tasks/preflight.yml:115:9

113     msg:
114       - "account            : {{ factory_cfg.user }} (home {{ factory_cfg.home }}, workspace {{ factory_cfg.works...
115       - "platform           : {{ ansible_distribution }} {{ ansible_distribution_version }} {{ ansible_architectu...
            ^ column 9

Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead.

ok: [localhost] => {
    "msg": [
        "account            : administrator (home ~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/.livetest/home-guards, workspace ~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/.livetest/home-guards/Dev)",
        "platform           : Ubuntu 26.04 x86_64 -> linux-x86_64 / amd64",
        "herdr              : 0.9.3 (latest release) at ~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/.livetest/home-guards/.local/share/code-factory/tools/herdr/0.9.3/linux-x86_64/herdr",
        "omp                : not installed (latest release)",
        "profiles           : fleet_guards",
        "services           : start_services=False linger=False",
        "pruner runtime     : disabled",
        "check mode         : False"
    ]
}

TASK [Ensure the Herdr configuration directory] ********************************
changed: [localhost]

TASK [Render the Herdr static configuration] ***********************************
changed: [localhost]

TASK [Render the Herdr user unit] **********************************************
changed: [localhost]

TASK [Ensure the user default.target.wants directory] **************************
changed: [localhost]

TASK [Statically enable the Herdr user unit] ***********************************
changed: [localhost]

TASK [Enable and start the Herdr user service] *********************************
[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24.
Origin: ~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/ansible/group_vars/all.yml:113:24

111
112 # Escalate to the target account only when we are not already that account.
113 factory_become_target: "{{ (ansible_user_id | default('')) != factory_cfg.user }}"
                           ^ column 24

Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead.

skipping: [localhost]

TASK [Report the deferred service start] ***************************************
ok: [localhost] => {
    "msg": "start_services is false: herdr.service is written and statically enabled at ~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/.livetest/home-guards/.config/systemd/user/herdr.service but not started. It comes up on the first boot of a host that has lingering enabled for administrator."
}

TASK [Install the Herdr Spaces reporter] ***************************************
changed: [localhost]

TASK [Install the Herdr sidebar client helper] *********************************
changed: [localhost]

TASK [Install the btop launcher] ***********************************************
changed: [localhost]

TASK [Ensure the btop configuration directory] *********************************
changed: [localhost]

TASK [Install the btop configuration] ******************************************
changed: [localhost]

TASK [Render the Herdr Spaces reporter unit and timer] *************************
changed: [localhost] => (item=herdr-spaces.service)
changed: [localhost] => (item=herdr-spaces.timer)

TASK [Ensure the user timers.target.wants directory] ***************************
changed: [localhost]

TASK [Statically enable the Herdr Spaces reporter timer] ***********************
changed: [localhost]

TASK [Enable and start the Herdr Spaces reporter timer] ************************
skipping: [localhost]

TASK [Ensure the omp extensions directory] *************************************
skipping: [localhost]

TASK [Checksum the Herdr omp integration before install] ***********************
skipping: [localhost]

TASK [Run herdr integration install omp] ***************************************
skipping: [localhost]

TASK [Checksum the Herdr omp integration after install] ************************
skipping: [localhost]

TASK [Ensure fleet browser directories] ****************************************
changed: [localhost] => (item=~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/.livetest/home-guards/oss-fleet)
changed: [localhost] => (item=~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/.livetest/home-guards/oss-fleet/browsers)
changed: [localhost] => (item=~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/.livetest/home-guards/.fleet-browser)
ok: [localhost] => (item=~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/.livetest/home-guards/.config/systemd/user/default.target.wants)
ok: [localhost] => (item=~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/.livetest/home-guards/.config/systemd/user/timers.target.wants)

TASK [Install the fleet browser scripts] ***************************************
changed: [localhost] => (item={'src': 'browsers/fleet-browser', 'dest': '~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/.livetest/home-guards/oss-fleet/browsers/fleet-browser', 'mode': '0755'})
changed: [localhost] => (item={'src': 'browsers/cookie-sync.ts', 'dest': '~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/.livetest/home-guards/oss-fleet/browsers/cookie-sync.ts', 'mode': '0644'})
changed: [localhost] => (item={'src': 'browsers/env.sh', 'dest': '~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/.livetest/home-guards/oss-fleet/browsers/env.sh', 'mode': '0644'})

TASK [Stat the resolved Obscura release] ***************************************
ok: [localhost]

TASK [Create the Obscura release directory] ************************************
changed: [localhost]

TASK [Fetch the obscura tarball] ***********************************************
changed: [localhost]

TASK [Extract obscura binaries] ************************************************
changed: [localhost]

TASK [Clean up the obscura tarball] ********************************************
changed: [localhost]

TASK [Point the fleet browser at the resolved Obscura release] *****************
changed: [localhost] => (item=obscura)
changed: [localhost] => (item=obscura-worker)

TASK [Inject the shared-browser env into shell profiles] ***********************
changed: [localhost] => (item=.profile)
changed: [localhost] => (item=.bashrc)

TASK [Render fleet browser units] **********************************************
changed: [localhost] => (item=fleet-browser-obscura.service)
changed: [localhost] => (item=fleet-browser-chrome.service)
changed: [localhost] => (item=fleet-browser-vnc.service)
changed: [localhost] => (item=fleet-browser-sync.service)
changed: [localhost] => (item=fleet-browser-sync.timer)
changed: [localhost] => (item=fleet-browser-gc.service)
changed: [localhost] => (item=fleet-browser-gc.timer)

TASK [Statically enable fleet browser units] ***********************************
changed: [localhost] => (item=fleet-browser-obscura.service)
changed: [localhost] => (item=fleet-browser-sync.timer)
changed: [localhost] => (item=fleet-browser-gc.timer)

TASK [Enable and start fleet browser units] ************************************
skipping: [localhost] => (item=fleet-browser-obscura.service) 
skipping: [localhost] => (item=fleet-browser-sync.timer) 
skipping: [localhost] => (item=fleet-browser-gc.timer) 
skipping: [localhost]

TASK [Report the deferred fleet browser start] *********************************
ok: [localhost] => {
    "msg": "start_services is false: obscura + sync/gc units are written and statically enabled but not started here."
}

RUNNING HANDLER [Reload the user systemd manager] ******************************
skipping: [localhost]

RUNNING HANDLER [Restart the Herdr user service] *******************************
skipping: [localhost]

RUNNING HANDLER [Restart fleet browser obscura] ********************************
skipping: [localhost]

RUNNING HANDLER [Restart fleet browser timers] *********************************
skipping: [localhost] => (item=fleet-browser-sync.timer) 
skipping: [localhost] => (item=fleet-browser-gc.timer) 
skipping: [localhost]

TASK [Apply pending unit reloads and restarts before verification] *************

PLAY RECAP *********************************************************************
localhost                  : ok=36   changed=23   unreachable=0    failed=0    skipped=12   rescued=0    ignored=0   
  • Evidence: Apply with neither profile (local file: ~/.no-mistakes/evidence/01M3X2T219BX49BYET2918N27W/apply-neither-profile.log)
Evidence: Per-profile ladder files, units and Herdr unit environment
--- home
cookie-sync.ts env.sh fleet-browser obscura obscura-0.2.3 obscura-worker 
fleet-browser-chrome.service fleet-browser-gc.service fleet-browser-gc.timer fleet-browser-obscura.service fleet-browser-sync.service fleet-browser-sync.timer fleet-browser-vnc.service 
Environment=PATH=$S/home/oss-fleet/browsers:%h/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Environment=CHROME_DEVTOOLS_AXI_BROWSER_URL=http://127.0.0.1:9222
Environment=FLEET_BROWSER_TIER=obscura
--- home-guards
cookie-sync.ts env.sh fleet-browser obscura obscura-0.2.3 obscura-worker 
fleet-browser-chrome.service fleet-browser-gc.service fleet-browser-gc.timer fleet-browser-obscura.service fleet-browser-sync.service fleet-browser-sync.timer fleet-browser-vnc.service 
Environment=PATH=$S/home-guards/oss-fleet/browsers:%h/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Environment=CHROME_DEVTOOLS_AXI_BROWSER_URL=http://127.0.0.1:9222
Environment=FLEET_BROWSER_TIER=obscura
--- home-none
ls: cannot access '~/.no-mistakes/worktrees/0e7271d93ec6/01M3X2T219BX49BYET2918N27W/.livetest/home-none/oss-fleet/browsers': No such file or directory 

Environment=PATH=%h/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Evidence: verify names the deleted ladder outputs (env.sh, fleet-browser-gc.timer)
fatal: [localhost]: FAILED! => {"assertion": "factory_output_stat.results | rejectattr('stat.exists') | list | length == 0"
  "changed": false
  "evaluated_to": false
  "msg": "Provisioning finished but these outputs are missing: .local/share/code-factory/tools/herdr/0.9.3/linux-x86_64/herdr
  .local/bin/herdr
  .local/bin/btop-bin
  .local/bin/sentrux
  oss-fleet/browsers/env.sh
  .config/systemd/user/fleet-browser-gc.timer."}
Evidence: fleet-browser sync with bun from PATH
which bun: ~/.local/bin/bun
cookie-sync: canonical 0 cookies; edits 0 deletions 0; obscura: set 0 del 0
obscura reloaded session (no lanes connected)
sync rc=0
[2026-10-02T01:36:04Z] obscura restarted to load merged session
$S/ns2.sh: 9: kill: No such process

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed (2) ✅
  • ⚠️ ansible/site.yml:105 - The change gates the ladder on a new profiles.fleet_browsers flag (default false) instead of the existing fleet_guards gate. The intent says ansible/tasks/fleet-browsers.yml is imported from site.yml "when the fleet_guards profile is on". No requirement in the intent needs a second profile, so this is an unrequired component. It also has two side effects.

(1) A host that already has fleet_guards: true and no fleet_browsers key stops provisioning the ladder on the next apply (site.yml:105). herdr.service.j2:15-20 re-renders the Herdr unit without CHROME_DEVTOOLS_AXI_BROWSER_URL, FLEET_BROWSER_TIER and the browsers PATH entry. The restart herdr notify in herdr.yml:36-37 then restarts Herdr, so agents lose the ladder environment. verify.yml:93-105 also stops checking the browser outputs on that host.

(2) The diff changes no host config. .local/host.yml is local, so the ladder stays absent on the moved host until someone edits it by hand. docs/agent-host-move.md:21 only documents that edit.

The smallest honest remedy is to remove the new profile and keep the fleet_guards gate, with the Obscura/Supabase installer split and the unit-list refactor undone. If a separate profile is wanted, the author must say so. The remedy then needs an explicit decision on migrating existing fleet_guards hosts and on enabling the flag for this host. The fleet_browsers and journald tags in site.yml:106 and fleet-browsers.yml:11,26 are also not required by the intent.

Sites tied to the new profile: ansible/group_vars/all.yml:212-213 (installer_also split), config/default.yml:16, schemas/factory.schema.json:57, ansible/tasks/verify.yml:94-105, ansible/templates/herdr.service.j2:15-20, tests/test_configuration.py (test_each_fleet_profile_resolves_only_its_own_release), and the docs edits.

  • ℹ️ docs/dependencies.md:81 - The docs now say fleet_browsers "Needs no other profile" (docs/configuration.md:84). docs/dependencies.md:81 says only the desktop profile installs iproute2 (ss), and that without ss every tier reports 0 clients and gc can stop a tier in use. A fleet_browsers-only host therefore relies on iproute2 being present in the base image. Only the always-on obscura tier is provisioned there, so impact is low. Consider stating the iproute2 dependency next to the fleet_browsers entry, or installing it with the profile. This only applies if F1's separate profile is kept.

🔧 Fix applied.
1 warning still open:

  • ⚠️ ansible/templates/herdr.service.j2:15 - The fix round moved the ladder's Herdr environment (CHROME_DEVTOOLS_AXI_BROWSER_URL, FLEET_BROWSER_TIER, the browsers PATH entry) into herdr.service.j2. Before, fleet-browsers.yml injected it with lineinfile, so a run with --tags fleet carried it. Now the template is rendered only by tasks/herdr.yml, which site.yml:56 tags [herdr].

An operator on the new host who runs ansible-playbook --tags fleet_browsers gets the units and env.sh, but the Herdr unit is not re-rendered and restart herdr is not notified. Herdr agents then still lack the ladder environment.

That is the runbook docs/fleet-guards.md advertises ("The playbook tag fleet_browsers narrows a run to the ladder", in a paragraph that also says the ladder puts its environment in the Herdr unit).

--tags fleet has regressed in the same way for existing fleet_guards hosts, and --tags fleet --skip-tags journald is the other path the docs imply.

Smallest fix: document that the Herdr unit needs the herdr tag too (--tags herdr,fleet_browsers), or add herdr to the tags on the ladder's import in site.yml:106. This is not a defect in the template move itself. Keeping the template is correct, because the old lineinfile was reverted by the template on every apply.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 10 of 10 scenarios driven live against the product
Scenario Result Live Evidence
Applying the playbook with profiles.fleet_browsers on (fleet_guards off) provisions the ladder: scripts, Obscura release and symlinks, shell-profile env, 7 units, static enables for the Obscura servic… ✅ pass live apply-fleet_browsers-run1.log; profile-matrix.txt
An existing fleet_guards-only host (no fleet_browsers key) still gets the full ladder, with CHROME_DEVTOOLS_AXI_BROWSER_URL, FLEET_BROWSER_TIER and the browsers PATH entry in herdr.service ✅ pass live apply-fleet_guards-only.log; profile-matrix.txt
Adversarial: with neither profile on, nothing of the ladder is installed and the Herdr unit carries no browser env or PATH entry ✅ pass live apply-neither-profile.log; profile-matrix.txt
A second unchanged apply reports changed=0 ✅ pass live apply-fleet_browsers-run2.log (changed=0)
A ladder-only run (--tags fleet_browsers) leaves herdr.service untouched; --tags herdr,fleet_browsers renders it with the ladder env (matches the new docs/fleet-guards.md text) ✅ pass live herdr.service absent after the fleet_browsers-only run; present with the ladder env after the herdr+fleet_browsers run
--skip-tags journald applies the ladder without touching /etc/systemd/journald.conf.d ✅ pass live All applies ran with --skip-tags journald; no journald tasks ran
verify.yml checks the ladder outputs and fails naming a deleted ladder output ✅ pass live verify-missing-ladder-outputs.txt lists env.sh and fleet-browser-gc.timer. Other missing items in that run (herdr, btop-bin, sentrux, and .profile on the no-profile host) are not ladder outputs: the s…
Starting the Obscura tier from the rendered unit's ExecStart puts CDP on 127.0.0.1:9222. fleet-browser status goes from down to up, and a worker navigates and takes a screenshot (the failure the int… ✅ pass live live-obscura-ladder-transcript.txt; ladder-obscura-screenshot.png
fleet-browser resolves bun from PATH (the BUN=bun change) when the home has no ~/.bun ✅ pass live live-fleet-browser-sync-bun-from-path.txt: fleet-browser sync exited 0 with bun found at ~/.local/bin/bun
Side effect on the operator's real Obscura unit ✅ pass live The unshare -Urn namespace isolates the network but not the user systemd bus. My fleet-browser sync therefore restarted the operator's real fleet-browser-obscura.service once (ActiveEnterTimesta…
  • uv run pytest tests/test_configuration.py -q (56 passed, includes test_each_fleet_profile_resolves_only_its_own_release)
  • uv run ansible-playbook -i ansible/inventory.yml ansible/site.yml --syntax-check
  • Real ansible-playbook ansible/site.yml --tags herdr,fleet_browsers --skip-tags journald with fleet_browsers=true, fleet_guards=false, sandbox home, start_services=false; run twice
  • Same playbook with fleet_guards=true, fleet_browsers=false
  • Same playbook with both profiles off
  • --tags verify on the ladder host after deleting env.sh and fleet-browser-gc.timer
  • Rendered fleet-browser-obscura.service ExecStart run in a unshare -Urn network namespace, then fleet-browser status, env.sh sourced in a shell, and a Bun CDP client navigating to a local page and calling Page.captureScreenshot
  • fleet-browser sync with bun found on PATH and no ~/.bun in the sandbox home
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

The ladder only ran under fleet_guards, whose shared-Supabase play refuses
to proceed without the fixture volume, so a host without that fixture
could not get the obscura tier at all.

- profiles.fleet_browsers (default off) gates tasks/fleet-browsers.yml,
  tagged fleet_browsers; obscura resolves only for it, supabase only for
  fleet_guards; ladder units leave factory_fleet_units for their own lists,
  checked by verify under the new profile.
- The ladder env moves into herdr.service.j2: the old lineinfile edit was
  overwritten by the herdr template on every apply, restarting Herdr, and
  its PATH line was shadowed by the template's own.
- journald cap tasks are tagged journald so a ladder-only run can skip it.
- fleet-browser finds bun on PATH; Code Factory installs it in
  ~/.local/bin, not ~/.bun/bin, so cookie sync never ran.
@undeemed
undeemed merged commit 1073f11 into main Oct 2, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant