Files
computer-playbook/tasks
kevinveenbirkenbachandClaude Opus 4.7 cf09358408 fix(load_app): drop _load_app_id host fact to stop nested mutation
The include_role vars block referenced `_load_app_id`, a host fact
created by `set_fact` at the top of load_app.yml. Host facts live in
the non-persistent fact cache (vars/manager.py:405) and survive across
include frames, so every nested `include_tasks: utils/load_app.yml`
call overwrote `_load_app_id` for the whole play.

Inside the coturn deploy chain (web-svc-coturn -> sys-stk-full ->
sys-stk-backend -> sys-svc-compose), sys-svc-container.Setup Swapfile
triggers a nested load_app.yml(svc-opt-swapfile) BEFORE
sys-svc-compose/04_files.yml renders coturn's env.j2. The nested call
set_fact'd `_load_app_id='svc-opt-swapfile'` and left it as a host
fact. When env.j2 lazy-evaluated `COTURN_REALM -> domain ->
lookup('domain', application_id)`, the outer include_role's vars block
resolved `application_id` through `{{ _load_app_id }}` and read the
mutated value, yielding

    lookup('domain'): failed to resolve domain for 'svc-opt-swapfile':
      application_id 'svc-opt-swapfile' not found in domains mapping
    (job 77879571693 on run 26451828677, debian, play 3)

The fix is two-step:

1. Switch the include_role to reference `load_app_id` directly.
   `load_app_id` is the caller's `include_tasks vars:` argument,
   attached to the TaskInclude object as task-scoped vars (Level 414
   include params in vars/manager.py). It walks the `_parent` chain
   via `task.get_include_params()`, and `_ACTION_ALL_INCLUDES`
   (constants.py:101) covers both include_tasks and include_role, so
   the outer TaskInclude's vars stay reachable from any depth. Each
   nested include_tasks frame has its own TaskInclude.vars and cannot
   mutate the outer frame's `load_app_id`.

2. Remove the `set_fact: _load_app_id` step entirely. The host fact
   was the only mutation source for this variable in load_app.yml,
   and dropping it eliminates the bug class for the whole loader.
   `grep` across the repo confirms `_load_app_id` is unused elsewhere
   and no role does `set_fact: application_id|service_role|service_item
   |load_app_id`, so the include-params resolution chain stays immune
   to host-fact races.

The 7 current callers of load_app.yml are verified safe:
- 6 pass literal strings (sys-svc-webserver-core, web-app-bluesky,
  sys-stk-front-base, web-opt-rdr-www, sys-svc-container,
  web-app-dashboard)
- 1 (sys-service-loader/load_service.yml) passes
  `"{{ service_role }}"`, which resolves via outer-loop-scoped
  `service_item.role` and is itself only ever set via include-tasks
  vars or loop_control, never via set_fact.

Reuses the same defense-in-depth direction as commits f32266d53
(tls lookup defensive templating) and 7b9cb5617
(current_play_redirect_domains defensive templating): close the path
where a mutable side channel feeds into a load-bearing lookup.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 01:24:17 +02:00
..

Cross-role tasks

This folder contains the cross role tasks