Commit Graph
1264 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5.5 50b4fa38b2 perf(ci): skip the local stack image build the deploy jobs never run
On real GitHub Actions INFINITO_BUILD passed through from default.env as 1, so every deploy job built the infinito image locally (~5 min) and then pulled and ran the published ci image anyway (pull_policy always). An env handler now defaults INFINITO_BUILD to 0 there; act and jobs that set it themselves keep building.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:03:37 +01:00
kevinveenbirkenbach 98d5966f50 Merge branch 'feature/web-svc-logout/server-side-logout-everywhere' into feature/store-i18n 2026-10-31 13:27:49 +01:00
kevinveenbirkenbach 5cb5acae6d Merge branch 'fix/lint/resource-budget-honours-pins' into feature/store-i18n 2026-10-31 13:27:42 +01:00
kevinveenbirkenbachandClaude Opus 5.5 b4ede26bab docs(requirements): add 066 logout everywhere per network and device
Specifies two logout choices. 'Log out on this device' keeps the front-channel conductor sweep and adds per-app back-channel clients plus an oauth2-proxy cookie_refresh, so one browser's sessions end on clearnet and onion alike. 'Log out everywhere' runs in the dashboard with the user's own keycloak-js token: revoke offline consents, DELETE /account/sessions, then keycloak.logout(). No service account, realm-management role, admin REST API, stored events or SPI provider is used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 13:11:00 +01:00
kevinveenbirkenbachandClaude Opus 5.5 76fc6992c4 fix(variants): apply a variant pin only while its carrier is in the round
Pins of every role applied at every round index, so a role the round did not deploy could pull a partner into a heavier variant (web-app-discourse#1 measured 35.46 GB against the 32 GB reservation cap). resolve_round now starts every role on its clamped index and recomputes the include and the pins in force until the assignment settles, raising VariantPinError on a cycle. The planner resolves each round once (39.1 s to 16.0 s for all rounds), affected judges every (role, index) with the round the planner deploys for it, and the resource budget lint measures the pin-aware rounds. The stale web-app-matomo pin above the primary-only docs entry is dropped, and test_variant_pin_fixed_point.py guards every planned round against unsettled pins and pins above a disabled entry. docs/contributing/artefact/files/role/variants.md explains the settle loop with diagrams.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 12:45:53 +01:00
kevinveenbirkenbachandClaude Opus 5.5 79e88c5ea4 fix(mirror): refresh a mirrored tag that upstream re-pushed
Fork CI run 41147980556 pgAdmin#0 failed in sys-ctl-hlth-runtime with
"stalwart_webmail: PHP 8.4.26 is below the security floor 8.4.27".
The deploy ran the fork's GHCR mirror of roundcube/roundcubemail:1.7.5-
apache, copied on 10-19 with PHP 8.4.26, while upstream re-pushed the
same tag on 10-27 with PHP 8.4.27. Branch CI runs the sync with
--only-missing, which skipped the tag because it existed ("destination
exists, skipping") without comparing digests, and the full refresh only
runs on the fork's default branch, which still pins 1.7.4.

- utils/docker/registry.py: manifest_digest() reads the
  Docker-Content-Digest of a reference with a HEAD request, which does
  not count against Docker Hub's pull allowance.
- GHCRProvider.is_stale() reports a mirrored tag as stale only when both
  registries answer with a digest and the two differ.
- Under --only-missing an existing tag is re-copied when it is stale; a
  failed re-copy is reported on stderr with the image and keeps the
  existing copy, so one refresh cannot block every deploy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 09:50:14 +01:00
kevinveenbirkenbach 166da8f8d2 Merge branch 'feature/shared-stack-caches' into feature/store-i18n 2026-10-30 23:31:00 +01:00
kevinveenbirkenbachandClaude Opus 5.5 34506554f7 fix(cache): download pacman packages per node and publish them into the shared cache
The shared pacman cache was the first writable CacheDir on every arch runner and lab node. Our arch image sets DisableSandbox, so libalpm downloads straight into that directory (util.c _alpm_download_dir_setup only makes a per-process temp dir with the sandbox on), and the XferCommand writes <pkg>.part there with curl -C -o %o; two nodes fetching the same package appended to the same .part file, and the second rename failed or left a corrupt package in the shared cache.

pacman.sh now gives each node its own download directory <shared>/pacman/node-XXXXXX/ (created once with mktemp, reused from pacman.conf on later runs) as the CacheDir between the read-only host layer and <shared>/pacman/, and writes a PostTransaction hook to pacman.d/hooks/infinito-shared-cache.hook that renames every finished package and signature into <shared>/pacman/; .part files stay for the next resume. Both directories sit in one bind mount, so each publish is an atomic rename(2). pacman.sh fails when pacman.conf sets HookDir, since pacman would then skip the hook silently. Requirement 065 and cache.md describe the three layers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:43:01 +01:00
kevinveenbirkenbachandClaude Opus 5.5 74d99201e0 feat(cache): share package and tool caches across stacks and release swarm clusters after rescue
Parallel compose slots, swarm lab clusters and GUI lanes each kept their own pip, npm and pacman downloads, and a failed swarm run left its cluster (about 25 GB of node docker data plus the rescue bundle in the act container) on the root filesystem until someone ran make swarm-down. INFINITO_CACHE_SHARED_HOST_PATH (default <DIR_CACHE>/shared) is now mounted read-write at the same path into every compose slot and every swarm lab node, with PIP_CACHE_DIR and npm_config_cache below it; empty, the renders stay byte-identical to before. On a host whose os-release ID is exactly arch running an arch stack, INFINITO_CACHE_HOST_PACMAN_PATH mounts the host pacman cache read-only and scripts/docker/cache/pacman.sh makes it the first CacheDir with <shared>/pacman/ second, so new downloads land in the shared folder and the host cache stays untouched; on Manjaro and every other host pacman uses the shared folder alone.

make swarm-zombie without keep=true now copies the act job's rescue diagnostics and Playwright artefacts to the host and then releases the lab nodes, the lab network, the NFS export volume and the act outer container, on success and on failure; when the copy fails the nodes still go, the act container stays and the run exits naming make swarm-down. make roundtrip passes keep to every swarm step, make clean-cache removes the shared folder, and the disc-space cleanup skips pacman cache directories it cannot write. apt and dnf keep their per-container caches behind the package-cache proxy, and no docker data root is shared between daemons. Requirement 065.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:04:38 +01:00
kevinveenbirkenbachandClaude Opus 5.5 f287322065 fix(objstore): serve public bucket files same-origin on every vhost of the consumer
WordPress put objstore.public_url, the clearnet api.seaweedfs.s3 host, into S3_UPLOADS_BUCKET_URL, so the onion pass of WordPress#0 (CI run 40979252521) loaded the site logo from clearnet and the leak spec failed; the /objstore rebase only fired on onion-only consumers and never on a multi-network node. lookup('objstore', app, 'relative_url') now returns /_infinito/objstore/<bucket> for a shared consumer with a public bucket and an API domain, and locations.conf.j2 serves that path on every vhost of the consumer, clearnet and onion alike, by proxying over the loopback to the provider's own vhost.

The route passes GET and HEAD only, answers 404 for a path ending in / (no ListObjects), drops the query string (no S3 subresources or presigned queries) and the Authorization and Cookie headers, so only the public bucket's anonymous read applies, and adds nosniff plus a sandbox CSP; private and embedded buckets get no route. Only web-app-wordpress switches to relative_url: mastodon, pixelfed, baserow, bookwyrm, snipe-it, listmonk, mediawiki, shopware and peertube need absolute URLs for federation, mail or server-side rendering, and decidim and funkwhale sign URLs, which the route deliberately refuses. The prefix moves from /objstore to /_infinito/objstore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 21:52:35 +01:00
kevinveenbirkenbach c242359ddf Merge branch 'feature/registry-cache-hub-auth' into integration/store-i18n-merge 2026-10-30 19:33:43 +01:00
kevinveenbirkenbach c5a0e5445e Merge branch 'feature/037-onion-network-aligned-config' into integration/store-i18n-merge 2026-10-30 19:33:27 +01:00
kevinveenbirkenbachandClaude Opus 5.5 f12d328fee fix(sys-front-tls): strip Secure from upstream cookies on every plain-HTTP vhost
On an onion vhost the browser talks plain HTTP, so an upstream that marks its session cookie Secure (the app still believes in its canonical https origin) loses that cookie and the login loops. n8n carried its own proxy_cookie_flags n8n-auth nosecure for this; every other app on the onion pass was exposed to the same failure.

The plain-HTTP branch of nginx_ssl_header.j2 now emits proxy_cookie_flags ~ nosecure, which matches every cookie; TLS vhosts are unchanged. n8n's per-cookie line goes. Taiga and Semaphore keep their tested location-level flags, which replace the server rule for their location, and network_modes.md says so. test_nginx_ssl_header.py asserts the rule on a plain-HTTP vhost and its absence on a TLS one; without the template line the plain-HTTP case fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 18:10:54 +01:00
kevinveenbirkenbachandClaude Opus 5.5 41ea801fd9 feat(network): point logout and peer allowlists at every network a vhost serves
CI run 40979252521 Nextcloud#0: on the onion pass the biber OIDC logout cleared only the clearnet session, because Keycloak holds one front-channel URL per client and the injected logout sent no post-logout target for the onion network; and Collabora's aliasgroup1 listed only the clearnet WOPI host.

On a vhost outside the application's canonical network (and without an external issuer) the injected logout one-liner now adds client_id and post_logout_redirect_uri set to that network's logout conductor, and Keycloak registers every conductor sibling as post-logout redirect URI next to '+'. Collabora's aliasgroup1 lists the Nextcloud WOPI host of every network (one instance, union allowlist). network_modes.md gains a Network-Aligned Values table, and test_network_allowlist_sinks.py fails when a template names an allowlist sink without the sibling union.

Rendering is covered by 14 tests; the full gate and the runtime acceptance of the '##' post-logout list by Keycloak and the comma aliasgroup by Collabora still need a gate on store-i18n and an onion deploy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 18:00:24 +01:00
kevinveenbirkenbachandClaude Opus 5.5 624ef48e8e feat(cache): let the registry cache pull from Docker Hub with an operator account
Two GUI lanes died on Docker Hub "toomanyrequests" on 2026-10-30 while the registry cache pulled anonymously from the shared host IPv4 bucket. compose/cache.override.yml hardcoded AUTH_REGISTRIES to "" (the image's Dockerfile sets placeholder accounts, so the empty value must stay explicit).

AUTH_REGISTRIES now reads INFINITO_CACHE_REGISTRY_AUTH, a static passthrough key whose default.env value is empty, so an unset or empty value renders the cache byte-identically to before and recreates no container. The operator sets auth.docker.io:<user>:<token> in custom.env; rpardini/docker-registry-proxy 0.6.5 adds the credentials to the token requests for auth.docker.io only. cache.md documents the Repo Public Read-only token, rotation via make dotenv and make compose-up, the log check, and where the value is visible. test_registry_cache_auth.py renders the real compose files with a temporary custom.env and asserts the value reaches registry-cache only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 17:52:08 +01:00
kevinveenbirkenbachandClaude Opus 5.5 470d970a9a docs(requirements): cover clearnet-rendered configuration on onion vhosts in 037
CI run 40979252521 (Nextcloud#0) and earlier onion passes (joomla,
mobilizon, mini-qr, wazuh, n8n) fail on values rendered once per role
that resolve to the clearnet host. 037 already asks for per-vhost URLs,
one instance per network and redirect URIs for both siblings; it now
also asks for:

- network-aligned once-rendered templates and a lint for them
- per-network web app manifests and no Secure cookie on http onion vhosts
- Collabora accepting and being embedded by every Nextcloud sibling
- post-logout redirect URIs per sibling and a logout that ends the
  session on every network
- the Mobilizon onion leak pass, and no handover, clearnet-only marker
  or skipped assertion as a fix for this class

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 14:16:46 +01:00
kevinveenbirkenbachandClaude Opus 5.5 46962b5110 Merge branch 'feature/updater' into feature/store-i18n
Brings in feature/updater 8c14873225: lockdown-probe with the Bluesky
broker key-rotation fix (0a6b6f9130) and the occ-only trusted_proxies
(406998f43a), the ERPNext server_scripts switch, and main with the
hotfixes #1290 (BBB apps-akka READ_COMMITTED), #1291 (FusionDirectory
PHP_MEMORY_LIMIT) and #1293 (Matrix variant 1 force_verification false).

Conflict resolutions:
- web-app-erpnext: create_site.yml takes updater's include of
  restart_frappe.yml; that file's compose/swarm gates use
  IS_COMPOSE_MODE / IS_SWARM_MODE instead of DEPLOYMENT_MODE literals.
- web-app-mailu, web-app-stalwart vars: store-i18n's RUF from
  users.platform_administrator plus the new RUA vars; dmarc.rua_user is
  platform_administrator (on store-i18n `administrator` is the host
  account), so RUA and RUF share the app mailbox. READMEs follow.
- web-app-stalwart 09_dns-records.yml: store-i18n's block form with the
  alias-domain MX task, plus rua=mailto in the DMARC record.
- web-app-pretix: store-i18n's source build stays; the version bump
  lands as source_version v2026.9.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 10:16:23 +01:00
kevinveenbirkenbachandClaude Opus 5.5 33591ab688 fix(roundtrip): pin the swarm step to clearnet when Tor is disabled
The workspace swarm roundtrip for svc-db-postgres failed on every host
with "The lookup plugin 'tls' failed: NETWORK_MODE 'multi' needs
svc-net-tor with a node onion on this node" (CI run 40979252521, job
Workspace debian:13). roundtrip.sh (b2469ff948) pinned the swarm step to
multi whenever the validator accepts it and never looked at disable=,
while 03_roundtrip.sh passes a disable list that removes tor. The
hard-fail is the documented contract (network_modes.md: tor or multi on
a node without the provider fails the deploy); the request was wrong.

roundtrip.sh now pins clearnet when disable= holds the Tor key (read
from utils.github.variant.network.TOR_SERVICE), else multi when the role
takes it, else the rotation. 02_zombie.sh, which also disables tor,
passes network=clearnet instead of relying on the rotation drawing it.
TestRoundtripSwarmNetwork runs the real script against stubs; the old
pin logic fails three of its cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 08:49:50 +01:00
kevinveenbirkenbachandClaude Opus 5.5 a6b9548a37 docs(requirements): tick the signaling endpoint and recording criteria of 061
The compose full_cycle reinstall of web-app-nextcloud passed test-talk-admin-settings.js (the expected signaling URL under /standalone-signaling/, the pinned signaling version as "Running version", chat-relay not listed as missing) and test-talk-recording.js, which asserts the stored recording file is not empty, in both deploy passes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-30 08:33:09 +01:00
kevinveenbirkenbachandClaude Opus 5.5 5e8890d208 Merge branch 'feature/updater' into feature/store-i18n
Brings in feature/updater accb22da75, which carries main 5995766f58.

Conflict resolutions:
- web-app-fider: main's tenant bootstrap (01_tenant.yml) runs first on
  the stack host, OIDC setup moves to 02_setup_oidc.yml, design to
  03_design.yml; flat includes so main's gate test reads them.
- sys-front-tls test_nginx_ssl_header: the tls fake maps the domain term
  store-i18n passes back to its app.
- group_vars 04_webserver.yml, sys-svc-proxy CSP header: store-i18n's
  webserver_resource lookup plus #1256's WEBSERVER_SECURITY_HEADERS and
  the security_headers.lua include after the CSP block.
- web-app-nextcloud: updater's decoupled Talk stack (Janus, signaling,
  NATS, recording builds) replaces aio-talk; store-i18n's per-network
  Talk backends move from the wrapper script into signaling.conf.j2 urls
  (NEXTCLOUD_HPB_SIGNALING_BACKEND_URLS from network_urls, canonical
  first); IS_COMPOSE_MODE/IS_SWARM_MODE booleans kept; the integration
  OpenProject digest stays delegated to STACK_HOST.
- users lookup docs, token-store write_many, cache README, meta info
  lint: both sides combined.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 00:40:05 +01:00
kevinveenbirkenbachandClaude Opus 5.5 f131f57978 docs(web-app-nextcloud): describe the decoupled Talk backend
README and Administration list the four Talk services, their sources, how they connect, how a version update reaches them and how the first deploy replaces nextcloud/aio-talk. Requirement 061 is added. Its criteria for the code shape are checked, and those for the four services running as their own services from their pinned or role-built images with role-rendered configs are checked from a compose deploy, where all four were healthy and each started with the config file the role mounted. The criteria for the Talk specs, swarm, the updater and the migration of an existing installation stay open.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-29 23:56:01 +01:00
kevinveenbirkenbachandClaude Opus 5.5 c855efc30a refactor(web-app-dashboard): hide a tile through services.dashboard.enabled instead of display
services.dashboard.enabled already says whether a role takes part in the dashboard: roles_with_service lists only roles with enabled and shared as tile-reachability targets, the CSP filter adds the dashboard to frame-ancestors only when it is set, and docker_cards uses it as the card's iframe flag. A separate display key duplicated that switch and left a role with enabled false still rendering a card. docker_cards now skips the card when services.dashboard.enabled is false (a role without a dashboard binding keeps its card, and a value that is not a boolean fails loudly), so display, its validator exception and its roles_with_service filter go again; base.md documents enabled as the per-host switch.

An inventory hides a tile with applications.<role>.services.dashboard.enabled: false, which also drops the dashboard framing of that role. Unit tests cover false, true, a missing binding, a rendered string and a non-boolean; forcing the helper to always show the card fails four of them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-29 19:00:46 +01:00
kevinveenbirkenbachandClaude Opus 5.5 3ef937a9d8 feat(web-app-dashboard): move the tile switch to services.dashboard.display
The dashboard read `display` from the role's meta/info.yml, which no inventory can override, so a host could not hide a tile. docker_cards now skips a card when `services.dashboard.display` in the merged applications is false, and absent still means shown; a non-bool value fails loudly. roles_with_service drops the same role from the dashboard tile-reachability targets so the Playwright spec does not wait for a hidden tile, and the inventory validator accepts `services.dashboard.display` for every role that declares `services.dashboard`, since no role default carries it. `display` leaves the meta/info.yml allowlist, web-app-jellyfin drops its redundant `display: true`, and base.md and layout.md document the new switch.

Real lookups with web-app-gitea set to display false render the Dashboard and Jellyfin cards and only jellyfin as a tile target, and all three without the override; the nine tests for the new behaviour fail against the old code.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-29 17:46:03 +01:00
kevinveenbirkenbachandClaude Opus 5.5 96ebefb9db perf(spot): re-render only the users SPOT after a token store write
A token store write re-rendered the whole applications SPOT: merged_applications_cache_key carried the token store digest, so the applications_carrier park in sys-token-store/tasks/write_many.yml missed its key and rendered all 269 applications again (stalwart compose roundtrip 18.3 s and 10.2 s per pass, swarm pass 1 12.2 s and 12.6 s).

Rendered from the real roles tree under an empty, a realistic and an exhaustive token store (298 users x 269 applications), the applications tree differs only under <app>.users.<key>.tokens (28783 leaves in 46 applications, 0 elsewhere); the 69 password leaves that also differ change between two renders of the same store. No code reads a token through the applications tree: every reader goes through lookup('users'), lookup('config', <app>, 'users.<key>...') (served from get_merged_users), lookup('email') or the sys_token_store_users fact, and the users carrier keys on the store.

The token digest therefore moves from merged_applications_cache_key into merged_users_cache_key, whose value stays the same. A store write keeps the applications carrier and the write_many park re-renders the users alone; the task is renamed to match. applications.md now says that user records inside the payload can predate every store write and that application config must not derive a value from a token.

Real ansible-playbook harness on 2.20.6 with the real roles, group_vars and write_many.yml: the re-park after a first mint and after a rotation drops from 4.0 s to 0.55 s on one host (applications renders 1 -> 0), and every token probe (users, config users path, email password, stalwart_users) equals the old code in compose and swarm mode. Dropping the digest from the users key as well leaves those probes stale, which the harness and two unit tests catch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-29 15:56:08 +01:00
kevinveenbirkenbachandClaude Opus 5.5 91f29d332e perf(spot): carry the rendered users next to the applications
Ansible forks a fresh worker per task, so every lookup('users') in a forked worker re-rendered the whole user catalogue (298 users, 0.3 to 0.5 s locally and about 1 s on the runner, 1 s locally once a users password is vaulted): _MERGED_USERS_CACHE lives in the process and had no carrier fact.

applications_carrier now also parks lookup('users', carrier=True) in _INFINITO_USERS_RENDERED at the same park points, and clear drops both facts. get_merged_users serves the fact to templated reads when its key matches: the applications carrier key (roles dir, applications, users, DOMAIN_PRIMARY, SYSTEM_EMAIL_DOMAIN, group_names, token store digest) plus inventory_hostname, so a carrier a run_once park broadcasts never answers another host with its become password. A park whose users carrier still matches leaves it in place instead of returning 119 KB again, so parks cost what they cost before.

Real ansible-playbook harness on 2.20.6 and 2.21.5 with the real roles tree: users renders per users-reading task after a park 1 -> 0, task 0.56 -> 0.08 s (vaulted 1.21 -> 0.17 s); a rotated token, a rotated inventory password, a cleared carrier and another host's carrier all re-render; 32 of 32 probe digests equal HEAD's. Removing the carrier check fails three unit tests. The stalwart swarm roundtrip pass 1 and its DR drill ran green with it on all nodes, with no 401 from JMAP, msmtp or LDAP.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-29 15:53:34 +01:00
kevinveenbirkenbachandClaude Opus 5.5 21e05b9d42 feat(env): switch the GPU-only tests with INFINITO_TEST_GPU
The two tests that only answer on a GPU host, the logout catalogue translation check and the complete-catalogs integration test, were gated on accelerated() alone: a contributor with an NVIDIA runtime had no way to keep them out of a local gate while the GPU translation run is paused, so every gate on such a host stayed red on them.

INFINITO_TEST_GPU is a new env key. Its handler derives the default from INFINITO_GPU_COUNT: true where the compose services reserve a GPU, false for 0 or none. custom.env is applied after every handler, so `INFINITO_TEST_GPU=false` there switches the tests off on a GPU host; an exported value wins as well. tests/utils/gpu.py reads the flag for both tests, fails on any value other than true or false, and falls back to the same GPU detection only where no .env is loaded, such as a direct pytest run.

The handler joins gpu_count in utils/env/handlers/infinito/gpu/ (count.py, only_tests.py), because two handlers sharing the gpu prefix are a folder by the prefix-grouping lint. The new module avoids a test* name so unittest discovery never collects it. customize.md lists the key among the common overrides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 11:50:26 +01:00
kevinveenbirkenbachandClaude Opus 5.5 c9e7f950d9 Merge branch 'feature/updater' into feature/store-i18n
68 conflicts, resolved per hunk; resolutions agreed with the Updater
session were applied as agreed:

- svc-db-postgres (superuser password script and core) and
  sys-svc-rdbms realign_dedicated_db: updater's argv/stdin redesign with
  expand_argument_vars: false, nested into store-i18n's block structure.
- svc-db-redis 01_init: store-i18n's ACL password on stdin.
- sys-svc-proxy forwarded.conf.j2: store-i18n's handover-aware file,
  without the forwarded_base include (Host and X-Forwarded-Proto would
  be sent twice); forwarded_base serves the inline consumers, e.g.
  web-app-seaweedfs nginx.conf.j2.
- basic.conf.j2 and web-app-openproject proxy.conf.j2: issuer redirect,
  then handover, else updater's location/sso_acl.conf.j2; the
  server-level webserver_extra_configuration hook stays.
- web-app-keycloak: updater's admin-REST settings step and renumbered
  update steps; update/01_client.yml removed; store-i18n's design step
  and LDAP block kept; a73435cc9d arrived through rename detection.
- web-app-nextcloud OpenProject provisioning: updater's rewrite, all
  credentials on the Ruby runner's stdin.
- web-app-semaphore: store-i18n's canonical web root (a1d7835863).
- Role READMEs: no generated Cosmos/Architecture/Quick Setup/Credits
  sections; desk-* paths resolved to the dsk-* renames.

Integration fixes for what merged cleanly but did not fit together:

- web-app-matrix ansible vars: drop updater's second
  matrix_client_element_configuration_extension_json, which silently
  overrode store-i18n's design-aware value.
- url.origin instead of a stripped url.base in 19 places store-i18n
  added since 44aaa68924, same lookup targets (the double-slash lint).
- Package batch (18ea75049e, cb4d340de9): installs the deploy-start
  batch covers are removed (caffeine, cursor, blender, npm in dsk-gnt-*,
  with shared references), the rest carry batch: false and a nocheck
  with the verified reason (PyPI fallback, before the batch, vendor
  repository, configured flavor only, addon gate); dev-npm references
  the root npm id.
- sys-token-store write_many.yml: store-i18n's change-gated block and
  IS_SWARM_MODE.
- web-app-openproject: IS_COMPOSE_MODE in 02_migrate/run.yml; the
  seeded-admin lock (ccbf8f168d) ported into 04_admin/provision.yml and
  covered by the admin-state digest.
- Bucket policy (3b4ac40df5 and follow-ups) moved to web-svc-seaweedfs,
  where store-i18n's object store lives; the S3 admin secret travels on
  stdin instead of a task-level environment.
- act workflow.sh: no Exception-labelled comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 02:28:16 +01:00
kevinveenbirkenbach da78db5662 Merge remote-tracking branch 'origin/main' into feature/updater-lockdown-probe
# Conflicts:
#	roles/web-app-matrix/templates/flavor/ansible/vars.yml.j2
2026-10-29 01:48:55 +01:00
kevinveenbirkenbachandClaude Opus 5.5 86e7c844f3 fix(web-app-stalwart): recognise existing v1.0 accounts and patch only their password
Stalwart v1.0 refuses an existing account with primaryKeyViolation; the batched provisioning from 1fbd4758e3 only knew alreadyExists, so every update deploy skipped the password sync and the listed-id fallback hid the refusal (msmtp then failed with 535). An account now counts as existing when the refusal's objectId is the listed account of that name, any other refusal fails the assert, and the update patches credentials/<k>/secret instead of replacing the whole credentials list, which would have dropped every app password, API key and second factor. sys-token-store gains write_many.yml, so all bot tokens land in one store write and one applications re-render; write.yml delegates a one-entry batch to it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-29 01:37:35 +01:00
kevinveenbirkenbachandClaude Opus 5.5 86dee1edc5 feat(web-app-openbao): record every request through a declared stdout audit device
OpenBao keeps no record of who read which secret without an audit device,
and 2.7 refuses to enable one over the API, so the server config declares a
file device writing to stdout, which cannot block and lands in the container
log with secret values only as salted HMACs. Every deploy fails unless the
device is active, a probe secret read through the AppRole session shows up
in the log with path, operation, display_name and time, and the probe value
does not appear in plaintext. openbao becomes a regular shared provider so
consumers such as the landing page role can declare services.openbao.
Requirement 047; taken over from the feature/openbao-audit worktree.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-29 00:07:17 +01:00
kevinveenbirkenbachandClaude Opus 5.5 d92375922d feat(roles): pull custom roles from inventory-declared sources
An inventory group web-app-cst-<name> with applications.<id>.custom: true and
a pinned source {url, version, path} is pulled into roles/ before the play,
read raw (Jinja, Ansible tags and credential URLs refused), placed through a
staging directory under an exclusive lock, and owned by a marker that names
its own directory, so a pull or roles-clean never replaces or removes a
directory it did not place and a failed pull leaves roles/ as it was.
Directory inventories, host lists and non-UTF-8 files behave as Ansible
reads them; generated test deploys pass --skip-pull. make roles-pull,
roles-clean and roles list, INFINITO_EXTRA_MOUNTS for local sources, the
web-app-cst category, the reserved prefix lint and a central
repository_files() that also yields the gitignored custom roles let every
lint scan them like in-tree roles. Supersedes the uncommitted 045 External
Roles draft and reuses its fetch, sync and mount design.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-29 00:02:58 +01:00
kevinveenbirkenbachandClaude Opus 5.5 b2c2fe7703 docs(github): match branch protection docs to the live ruleset
The contributor docs described repository settings that are not in
place. security.md marked "Require status checks", "Require branch to
be up to date" and "Require signed commits" as enforced, but the active
ruleset `Main` has no required_status_checks and no required_signatures
rule, and the separate `Signed` ruleset is disabled. repository.md
listed Dependabot alerts, security and version updates, secret scanning
and push protection as disabled although all of them are on, and its
summary repeated the two rules that are not enforced. branch.md claimed
branch protection enforces the branch naming rules, while only the PR
validation run checks the prefix. Three pages still described a hadolint
SARIF job in call-lint.yml that no longer exists, which hid that nothing
uploads the Hadolint results the ruleset asks for.

The pages now state what the ruleset and the repository settings
enforce today, name the policy pages for rules that are policy only,
and record that no workflow uploads Hadolint results.

Scorecard alerts 472, 504 and 505 depend on repository settings and
process and stay open; 507 and 539 are no longer reported on main.

Refs #853

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 22:39:16 +01:00
kevinveenbirkenbachandClaude Opus 5.5 4b5739a0fb perf(cache): key the token store by its tokens, not by its source
The carrier key switched from the tokens file's path and hash to the
sys_token_store_users fingerprint once the token store loaded, so the
stage-wide park re-rendered the applications SPOT (about 11 s) even when the
tokens were identical. Both sources now sign as a digest over exactly the
tokens the users hydration reads; the store helpers move to users/store.py
to keep the package under the file length cap.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-28 22:18:15 +01:00
kevinveenbirkenbachandClaude Opus 5.5 1b9c6f9d12 feat(test-e2e-playwright): ship @axe-core/playwright in the shared runner
Specs can run WCAG checks through AxeBuilder without a role-local package.
playwright.axe_version pins it with an npm update source, and the shared
install stamp carries both versions so a host with an older stamp reinstalls.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-28 22:12:12 +01:00
kevinveenbirkenbachandClaude Opus 5.5 88182e8d1f docs(requirements): add 046 custom roles pulled from inventory-declared sources
A custom role is an ordinary inventory group web-app-cst-<name> marked with
custom: true and a pinned source; core pulls it into roles/ before the play
and scans it like any in-tree role. It supersedes the uncommitted 045
External Roles draft and keeps its credential, atomic-sync and mount rules.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-28 22:05:20 +01:00
kevinveenbirkenbachandClaude Opus 5.5 5586a31d82 chore(lint): drop the three lints that parsed the MCP requirement audit
test_mcp_audit_completeness, test_mcp_audit_classification and
test_mcp_blocked_reasons held roles against the audit lists and tables of
requirement 035, so every new role, a custom web-app-cst-* role included,
had to be written into a requirement document. No lint pins the content of
a requirement; the audit stays as a review record.

Requirements 025 and 035 no longer claim that a lint enforces the audit,
and 035 drops its demand for such a test. Same removal as 0e9210c36a on
feature/store-i18n, where the audit moved into ADR 0003.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-28 21:36:47 +01:00
kevinveenbirkenbachandClaude Opus 5.5 621a492ec3 feat(ci): name the deploy chunks as numbered batches and star the priority ones
The chunk jobs read `🧱 Chunk N` and every priority row carried its own
trailing ⭐, so a priority sweep repeated the star down the whole job
list while the chunk that held those rows looked like any other.

- The four chunk jobs are named `0️⃣ Batch` to `3️⃣ Batch`, with ` ⭐`
  when the batch holds priority rows, and their discover job reads
  `🔍 Batch N ⭐` through the existing `marker` input.
- call-validation counts those batches once per run
  (scripts/github/resolve/priority_batches.sh, which runs
  `cli.meta.ci.matrix --priority-chunks`) and exposes the count as the
  `starred` output. chunks.plan cuts the priority chunks from the
  priority rows alone, so offset and the regular line cannot move them;
  the orchestrator hands validation the same sweep and chunk size it
  hands the chunk jobs.
- The row label drops its own star. report_failures and the job-name
  parsers still accept a starred title from an older run.
- The glossary names the unit `batch` (`#️⃣`) instead of `chunk` (`📦`),
  `ci plan` prints the batch column as the keycap the job name leads
  with, and the name lint accepts a leading keycap emoji.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 15:46:38 +01:00
kevinveenbirkenbachandClaude Opus 5.5 fdf1a33c5f feat(backup): decide stop_required for every service with a backed-up volume
baudolo stops every container on a volume before the authoritative copy
unless its image is listed via --images-no-stop-required. Leaving the
marker out silently picked "stop": the Stalwart webmail start that the
backup cycle triggered got SIGKILLed mid `composer update` (CI run
40625679270), and 71 services that mount a named volume had never
decided either way.

- The marker is now `backup.stop_required` (true = stop first, false =
  copy live), replacing the double-negated `no_stop_required`; the 50
  existing declarations are inverted unchanged. backup_image takes the
  marker and the wanted value explicitly:
  lookup('backup_image', 'stop_required', false) still feeds
  --images-no-stop-required, lookup('backup_image', 'disabled', true)
  the skip list.
- The 71 undecided services carry researched values (repo files plus
  upstream backup guidance: GitLab, Jenkins, Elastic, RabbitMQ, Typesense,
  Qdrant, OpenLDAP, Prometheus, Moodle, ClamAV, Alertmanager, ...): engine
  files, git repositories, SQLite, mnesia and multi-file state stop; image
  code, write-once blob stores with metadata in the database and
  atomically rewritten files copy live. Stalwart's main service stays
  stopped until /var/lib/stalwart is verified to hold only logs.
- Re-obtainable data is no longer backed up at all (`backup: false` on
  the volume): model weights, ClamAV signatures, SearXNG config/cache,
  transient RabbitMQ queues, Funkwhale's Typesense index, the meta clone,
  the agent-broker socket dir, coturn's unused data dir, and volumes no
  image reads (Decidim's /app paths outside /code, BookWyrm's /app/data
  which v0.9.3 never reads, FusionDirectory's measured-empty dir).
- The backup lookup only matches what actually runs once the role
  declares it: Bluesky's broker (`custom: bluesky_broker`) and Decidim
  (`custom: true`) declare their custom build, BookWyrm's worker/migrate,
  EspoCRM's daemon/websocket, Moodle's cron and OpenProject's proxy use
  their owner's image, and the templates drop their render-time
  overrides. Decidim deploys decidim_custom:0.31.6 and BookWyrm
  bookwyrm_custom:3.11-slim from now on; a BookWyrm ref bump still
  rebuilds through the build-args digest.
- test_backup_stop_decision_declared requires the boolean on every
  service with its own image that mounts a backed-up named volume;
  test_stop_required replaces the old integrity test.

Measured against HEAD: 43 -> 78 services copied live, none moved the
other way; 14 -> 30 volumes skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 15:10:24 +01:00
kevinveenbirkenbachandClaude Opus 5.5 ffe77cdba9 feat(ci): mirror the source repository's main into a fork on every push
bf25eb2fc7 removed the sync-main job because the fork had no token that
may push .github/workflows/**: GITHUB_TOKEN cannot be granted the
workflows permission, and the bot App is installed on infinito-nexus/core
only. Since then a fork's main only moves when its owner syncs it by
hand, and the fork's main has fallen 368 commits behind.

The job is back with a token of its own: CI_SYNC_MAIN_TOKEN, a
fine-grained personal access token limited to the fork, with Contents
and Workflows write. main_from_repository.sh resolves the source through
baseline_remote.sh, so the normalization and the off switch exist once:
in infinito-nexus/core itself and with CI_SYNC_MAIN_SOURCE_REPOSITORY
disabled the job ends green without reading the secret. Everywhere else
a missing secret fails the job before any fetch or push and names the
page that explains how to create it. The token reaches git as an HTTP
header through GIT_CONFIG_* variables, never through argv or
.git/config; checkout runs without persisted credentials so its own
header does not compete.

The job runs beside run-policy, not before it: CI keeps fetching its
diff baseline from the source repository read-only, so a failed sync
turns the run red without holding CI back.

fix/ci-mirror-token tried the bot App instead (019fb572a7, 756b180f1d).
That needs the App installed on every fork and granted Workflows write,
which secrets.md rules out for the App, so the token here is a
per-fork PAT.

Checked: the script skips as infinito-nexus/core and with the sync
disabled, and fails before fetching as a fork without the secret.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-28 13:12:22 +01:00
kevinveenbirkenbachandClaude Opus 5.5 8e3a013b1b fix(web-app-docs): build the replay image on the stack daemon when it cannot be pulled
The README replay of a `docker run`-shaped guide starts INFINITO_IMAGE on
the stack daemon (machine.sh replay_over_ssh). In matrix CI that image is
a pullable ghcr ci-<sha> ref; in Workspace jobs and local worktree stacks
it is a locally built image (`infinito-<distro>`, pull policy never) that
exists only on the outer daemon, so the replay died with "pull access
denied for infinito-ubuntu" (CI run 40625679270, Workspace ubuntu,
web-svc-cdn guide).

With INFINITO_PULL_POLICY=never the replay now builds the checkout at
DOCS_TEST_SRC_DIR via scripts/image/build.sh on the stack daemon, tagged
with the image the block runs, before replaying. The policy reaches the
container through compose.yml, the development inventory and
test.env.j2; an inventory not yet regenerated renders it empty and keeps
today's behaviour, and matrix CI (always) never builds. Not verified
locally: the worktree stack has no inner dockerd before a deploy and the
replayed guide is picked at random, so the Workspace jobs of the next run
are the evidence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:59:48 +01:00
kevinveenbirkenbachandClaude Opus 5.5 99123762df Merge branch 'feature/updater' into feature/store-i18n
16 conflicts, resolved per hunk:

- sys-stk-front-proxy: tasks/main.yml keeps the network-sibling loop;
  the single stack_host_contents write of http.conf plus vhost and the
  front_proxy_vhost_changed gate move into tasks/vhost.yml, with the
  http.conf decided per sibling domain so an onion vhost gets no HTTPS
  redirect.
- sys-front-tls: 01_http.yml is gone as the updater intends; the
  cert-renew include in main.yml gates on the vhost domain's TLS.
- sys-svc-compose: the updater's resolve_host_cid without the remote
  echo and the prepared pending flag in up.yml, written with
  IS_SWARM_MODE and the repeated-when block grouping.
- web-app-nextcloud: the updater's post-upgrade gate and pending-only
  plugin configuration, which already carry the pinned-plugin handling.
- web-app-xwiki: one XWIKI_CORE_VERSION for the readiness probe and
  style.css.j2; OIDC/LDAP follow xwiki_extensions_ready at bootstrap,
  the S3 blob store stays off there as TestBlobstoreRenderOrder demands.
- roles_with_service: onion swap and dashboard card key combined, the
  canonical domain is the canonical URL's host in both branches.
- sys-ctl-hlth-csp: --clearnet-timeout and --concurrency together;
  run_checker forwards its options to build_docker_cmd, keeping the
  script under the 500-line limit.
- web-app-matrix: Element branding and web_client_location side by side.
- web-app-stalwart/README.md: the generated Cosmos section is dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 23:47:07 +01:00
kevinveenbirkenbachandClaude Opus 5.5 afb82f57a1 feat(web-app-dashboard): let a role name the canonical domain its card opens
The dashboard card of every app opened its primary domain, so Matrix led to the Synapse status page and Stalwart to its admin UI. A role can now declare services.dashboard.card: <key of meta/domains.yml canonical>; docker_cards builds the card URL with lookup('canonical_url', app, key), the Apps menu reuses it, and roles_with_service('dashboard') hands the same domain to the tile e2e spec. Matrix sets card: element, Stalwart card: webmail. Without card nothing changes, and the prometheus/matomo target lists are untouched. A lint requires the key to resolve in the role's own domains.canonical.

The Matrix ansible flavor also sets Synapse's web_client_location to Element through matrix_synapse_configuration_extension_yaml, as the compose flavor already does, so matrix.<domain>/ redirects there. Merge note: feature/iam-profile-rbac defines the same extension variable inside the OIDC block; the next merge must combine both into one block (agreed with that session).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:01:06 +01:00
kevinveenbirkenbach db17bd5d4a Merge branch 'feature/updater' into feature/store-i18n 2026-10-24 21:52:15 +02:00
kevinveenbirkenbachandClaude Opus 5.5 8e35d053a2 fix(security): stop seeding demo data on every deploy, gate it on MODE_DEMO
Snipe-IT ran `php artisan db:seed --force` on every deploy. Its upstream
DatabaseSeeder (v8.8.3) truncates users, assets, licenses, orders and
settings, deletes uploads and creates about 2100 users, among them the
superusers `admin` and `snipe`, all with the factory password `password`,
plus a settings row against the public demo LDAP ldap.forumsys.com. Every
redeploy wiped real data and planted known superuser logins.

Decidim ran `rails db:seed` on every deploy with failed_when: false. The
generated seeds call Decidim.seed!, whose participatory-space seeds run in
production too: from the second deploy on they add demo processes and,
once a non-admin user exists, confirmed users with admin, moderator and
valuator roles and the password `decidim123456789`.

Snipe-IT now only creates its settings row when none exists, through
files/php/ensure_settings.php and SNIPE_IT_INITIAL_SETTINGS, and records
what already-seeded installs keep in TODO.md (taken verbatim from
d12b57be90 on feature/store-i18n). Decidim's seed task is gone; the role
creates its organization, pages and admin itself.

MODE_DEMO (group_vars/all/01_modes.yml, default false) brings both demo
seeds back on purpose; Decidim's then runs after the organization exists
and fails loudly. tests/lint/ansible/tasks/test_no_default_database_seeder.py
rejects Laravel's default seeder and Rails db:seed/seed:replant/setup/
reset/prepare unless the task is gated on MODE_DEMO; a Rails task whose
seeds are production-safe carries `# nocheck: default-seeder <reason>`,
which Mastodon does (find_or_create seeds, development-only admin seed).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 20:56:00 +02:00
kevinveenbirkenbachandClaude Opus 5.5 dba6aa7407 docs(requirements): add 045 stack service
Records the operator-confirmed design for replacing the sys-stk-full,
sys-stk-backend and sys-stk-front-proxy includes (115 call sites in 94
roles) with one shared `stack` service that sys-service-loader runs once
before the application roles and that processes every application of
the play in one batch: per-role literal `stack` and `openresty` keys,
front proxy and database derived from enabled services, per-domain vhost
data in meta without exceptions, all roles in one change.

The document carries the verified codebase facts with file:line on
origin/main 6997dd3dbb, the ordering and binding constraints, the cost
measured on the echoserver full-6 and full-7 deploys, a target design,
grouped acceptance criteria, validation commands and an implementation
order, so another agent can implement it without this conversation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 19:48:43 +02:00
kevinveenbirkenbachandClaude Opus 5.5 7ce22ee62d fix(design): render the per-app sheet and the webmail logo network-neutral
sys-stk-front-proxy renders one vhost per network sibling, and each pass ran sys-front-inj-design, which composes app.css.j2 with lookup('cdn') aligned to that pass's network and writes the result to the single path roles/<app>/latest/css/style.css. The onion pass runs last, so the clearnet page loaded the onion logo URLs from the clearnet CDN (fork run 40042690056, job 120723739575: both renders changed the file, onion at log line 161228). That failed the clearnet design checks of docs (18 jobs), prometheus (12), baserow, hermes, mattermost, openproject and pretix, and the clearnet network-leak checks of docs, pretix and mattermost. 8187db557c already gated who writes the file; the content still depended on the rendering pass.

The composed sheet now drops the CDN scheme and host, the way cfc0c337ab did inside Stalwart's own sheet. The file is byte-identical in every network and its root-relative URLs resolve against the CDN vhost the page loaded it from; the custom properties all sit inside url().

Roundcube's skin_logo is an <img src> in the document, where a relative URL would resolve against the webmail host, so webmail-design.inc.php.j2 renders a map from each webmail sibling host to the CDN origin of the same network and picks it by HTTP_HOST, falling back to the canonical CDN for any other host. That fixes stalwart+tor test-design.js:892 (nine onion passes showed the clearnet logo).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 15:24:57 +02:00
kevinveenbirkenbach a04f2c9895 Merge branch 'feature/updater' into feature/store-i18n 2026-10-23 17:32:32 +02:00
kevinveenbirkenbachandClaude Opus 5.5 285ce910d2 feat(inventory): reject unread keys and a missing Cloudflare token before the deploy
The Keycloak stage on echoserver ran ten minutes before sys-svc-dns stopped
it with "API.cloudflare.api_token must be set": the inventory still kept the
token under the flat name CLOUDFLARE_API_TOKEN, which nothing reads any
more. The validator that runs before every deploy now catches that class.

- api.py reports a host that selects DNS_PROVIDER: cloudflare without
  API.cloudflare.api_token, with group_vars/all merged under the host_vars
- variables.py reports every top-level key of host_vars and group_vars that
  core does not read: not declared in group_vars or a role's defaults or
  vars, not tested with "is defined" or "| default(", not read by name in a
  plugin, and not a SYSTEM_EMAIL_<KEY> the email lookup builds; a flat
  credential name such as CLOUDFLARE_API_TOKEN names its API.<provider>.<key>
  path; ansible_* keys are exempt

A key that is read in a way the scan cannot see takes
"# nocheck: inventory-variable", documented in suppression.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 16:18:12 +02:00
kevinveenbirkenbachandClaude Opus 5 d18c1188bd docs(ci): name the one install workflow the branch kept
612266d75b consolidated call-test-install-make.yml and
call-test-install-pkgmgr.yml into call-test-install.yml. The merge of
feature/network/multi-network-serving took that branch's version of the
overview wholesale, which still described both files, so the table named and
linked two workflows the tree no longer carries.

The row is the one from before the merge, byte for byte; the deploy row above
it keeps the network input the merge brought. test_workflow_references and
test_markdown_links both hung on the same two dead links.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-23 12:11:50 +02:00
kevinveenbirkenbachandClaude Opus 5.5 50fcacb949 test(lint): require become: false on localhost delegations
A delegated task inherits the play's become and runs sudo on the
controller. The lint flags every delegate_to: localhost whose task,
enclosing block or play does not drop become, include apply: blocks
included, and accepts a localhost-become marker for a step that needs
root there. The e2e test roles only stat repository files and pack to
/tmp, so they drop become; the token-store mirror below
/var/lib/infinito and the NFS controller mount keep it with a marker.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 09:54:37 +02:00