Commit Graph
112 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5.5 4ac188022c chore(agents): mirror the store-i18n sandbox and permission policy
Takes the agent policy from the feature/store-i18n worktree over unchanged:

- rm -rf* leaves the deny list.
- Edit and Write on any .claude/*.json now ask for confirmation.
- The sandbox network allowlist trades the single 127.0.0.6:8097 entry for 127.0.0.1 to 127.0.0.12 and localhost on every port, so sandboxed commands reach local services and published compose ports.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-03 19:57:29 +02:00
kevinveenbirkenbachandClaude Opus 5.5 a4d38f96d2 fix(agents): stop enabling third-party plugins for every contributor
The committed settings enabled the caveman and ponytail plugins from two
unpinned GitHub marketplaces for everyone who trusts the repository, and
plugin hooks run in the harness, outside the Bash sandbox. The block is
a leftover: since kevinveenbirkenbach/skills@3be5655b44, make
install-skills writes it to .claude/settings.local.json, so enabling the
plugins stays a per-developer choice.

Found while reviewing the settings after Hanno Horrmeyer's report; see
7ed60e1fc2.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 16:57:31 +02:00
kevinveenbirkenbachandClaude Opus 5.5 4e1c8e2de1 fix(agents): deny rm -rf and the host credential files in the sandbox
An external report flagged the Bash(*) allow in the committed
.claude/settings.json. Bash(*) stays: every command still runs inside
the OS sandbox, and deny and ask take precedence over it. Reviewing the
file against its own documentation turned up two real gaps instead.

security.md and settings.md cite an rm -rf* deny rule as the guard
against `rm -rf .` wiping the worktree and .git/, but the rule was lost
in ee30f6d446 when the deny list was rewritten. It is back.

denyRead covered ~/.aws, ~/.config/gcloud, ~/.gnupg and ~/.kube only.
~/.docker/config.json holds registry logins inline, ~/.git-credentials
and ~/.netrc hold plaintext credentials, and the default ~/.ssh/id_*
keys were readable because Assumption 10 kept ~/.ssh open for an
ask-gated git push that has since moved to deny. All nine paths are
denied now. The rest of ~/.ssh stays readable, so SSH fetches keep
known_hosts and authenticate through the agent socket. Sandboxed docker
calls pull anonymously and print a config warning on stderr.

Thanks to Hanno Horrmeyer for the report.

Reported-by: Hanno Horrmeyer
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 16:57:30 +02:00
kevinveenbirkenbachandClaude Opus 5.5 f47ca6e7c0 chore(agents): deny git commit --amend
An agent cannot see whether the operator committed on top of its last
commit in the meantime. An amend then rewrites a commit it never
authored: it happened to a signed, tagged release commit, which only a
soft reset onto the original recovered. Follow-up changes land as a new
commit instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 15:49:02 +02:00
kevinveenbirkenbachandClaude Opus 5 af605b049f refactor(domains): give local testing a .test domain, leave .example to docs
RFC 6761 reserves .test for testing and .example for documentation, and the
project's own domain was doing both jobs under the documentation name. It is
what default.env hands every local deploy and what the test fixtures assert
against, so it is a testing name.

Rename it wherever it stands for the running machine: the environment
defaults, the development inventory, the workspace and trust scripts, the test
suites, and the four documents that describe real behaviour - the DNS mapping
to loopback, the browser CA trust, WSL2 resolution. Those documents would have
started lying the moment default.env changed.

The sandbox allowlist moves with them, since egress is matched per pattern and
nothing resolves the old name any more, and the sandbox document that mirrors
that list follows it.

Two places keep .example on purpose: the link-style sample in the
documentation guide, which is documentation about documentation, and every
generic fixture name elsewhere in the suite - a.example, foo.example and their
kin. Those are a larger sweep of their own and are not touched here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 13:10:34 +02:00
kevinveenbirkenbach f6b361f42c feat(agents): add the worktree skills and the path they write to
A second branch needs its own checkout to be worked on in parallel, with its own
slot, subnet, ports and container names so two deploys do not take each other's.
The two skills route to the make targets that create and release one.

The sandbox gains the worktree directory as a writable path, without which the
checkout cannot be created and the skills are unusable. It is the only change
to the permission file: one write path, no deny or ask rule touched.
2026-08-06 21:34:05 +02:00
kevinveenbirkenbachandClaude Opus 5 1a655fb811 chore(claude): deny the git flags that route around the repo-local permissions
Bash(git commit*) sits in ask and Bash(git push*) in deny, but both match on
the argv prefix: 'git --git-dir=<path> commit' and 'git --work-tree=<path>
push' start with neither, so they slipped straight through. The confirmation
dialog never appeared for a commit issued that way.

Deny both flags outright. Repo-local work has no need for them - the worktree
tooling covers the legitimate cases - and leaving them open makes every other
git rule advisory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 02:13:30 +02:00
kevinveenbirkenbachandClaude Opus 5 3c1de767e5 chore(claude): allow WebFetch for the distro package mirrors
Verifying the swarm lab's dnsmasq behaviour meant reading the shipped config and systemd unit out of the real packages of all five distros, which needs the arch package archive and the centos stream mirror alongside the ubuntu and debian hosts already allowed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 23:53:43 +02:00
kevinveenbirkenbachandClaude Opus 5 c2b3fd3894 chore(claude): allow WebFetch for gvisor.dev
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 23:53:43 +02:00
kevinveenbirkenbachandClaude Opus 5 7c83cfb583 chore(claude): allow three more WebFetch domains
Grants accumulated during research sessions. They landed in the tracked
settings file because .claude/settings.local.json was not writable, so
the documented destination for runtime approvals was unavailable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 19:47:00 +02:00
kevinveenbirkenbachandClaude Opus 5 c2ee0dfa94 chore(claude): allow WebFetch to further package indexes
packages.ubuntu.com serves the Ubuntu leg of the external package availability
test, next to the packages.debian.org entry that is already allowed.
pkgs.alpinelinux.org and pkgs.org are general package searches with no caller
in the repository.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 23:25:50 +02:00
kevinveenbirkenbachandClaude Opus 5 2a3bf1e032 chore(claude): allow WebFetch to src.fedoraproject.org
Reading the packaged nfs-ganesha spec is how the Fedora build of the
package was checked against its libntirpc dependency. Add the domain to
the WebFetch allowlist so that lookup does not need an approval prompt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 23:25:49 +02:00
kevinveenbirkenbach e1ffc6e9c5 chore(claude): allow WebFetch of github.blog
Recorded while fetching the GitHub changelog to confirm Dependabot glob
support during the recursive-scan work.
2026-07-23 20:01:31 +02:00
kevinveenbirkenbach 8755146a0e chore(claude): allow docs.github.com for WebFetch 2026-07-18 13:03:25 +02:00
kevinveenbirkenbachandClaude Opus 4.8 6e20b94f14 chore: apply autoformat and settings churn from the quality run
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 01:11:48 +02:00
kevinveenbirkenbachandClaude Opus 4.8 17fa64e4e6 chore(claude): pause for confirmation before CHANGELOG or pyproject edits
Release metadata stays a manual operator step, mirroring the same ask-gate in backup-docker-to-local.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 01:11:48 +02:00
kevinveenbirkenbach 4e46ec5af6 chore(claude): allow consent.youtube.com and framatube.org for WebFetch 2026-07-17 19:10:01 +02:00
kevinveenbirkenbachandClaude Opus 4.8 9453ac010a chore(claude): allow the playwright browser-download CDNs for WebFetch
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 14:44:35 +02:00
kevinveenbirkenbachandClaude Fable 5 bf0f79b0e6 chore(claude): session permission updates
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:48:54 +02:00
kevinveenbirkenbachandClaude Fable 5 14844ccf8f chore(claude): allow WebFetch docs.rs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:48:53 +02:00
kevinveenbirkenbachandClaude Opus 4.8 1a908a6e5b perf(tests): parallelize suites, hoist fixtures, gate ansible-lint
Run the python suites through pytest-xdist when pytest is importable
(id-level collection parity with unittest discover verified per suite;
serial unittest stays the fallback), with -rP so warnings printed by
passing tests, like the update annotations, still reach the log. run.sh
self-provisions the dev extras through scripts/install/dev-extras.sh,
which stamps inside the venv and skips read-only venvs; make onboard
pre-warms the same path in the running container.

Hoist per-test cache resets and roles scans to setUpClass in the lookup
and resolvable suites (a per-test fingerprint-memo reset closes an
id()-reuse cache-poisoning flake the hoist exposed), inject generation
names into the nfs backup script test instead of sleeping, and narrow
ansible-lint scope to real ansible content. ansible-lint additionally
sits behind a content fingerprint gate that skips the run when nothing
it walks has changed; without a usable git (container bind mounts) the
gate disables itself and the full run remains.

Drop the import-linter layer and ignore for the deleted deps module;
the contract now locks compose -> common -> env. Measured on the host:
unit 101s to 20s, integration 90s to 24s, lint tests 43s to 19s,
repeated ansible-lint 53s to 0.4s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 21:20:46 +02:00
kevinveenbirkenbachandClaude Opus 4.8 10b25b4f00 chore(agents): enable caveman and ponytail plugins project-wide
make install-skills now registers the caveman and ponytail marketplaces
and enables both plugins in the project settings, so their SessionStart
hooks auto-activate token-compressed communication (caveman) and
minimal-code mode (ponytail) for every agent session in this repository.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 18:43:35 +02:00
kevinveenbirkenbachandClaude Opus 4.8 89138d3b82 chore(claude): allow WebFetch domains gathered during the CNG research
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 02:37:27 +02:00
kevinveenbirkenbachandClaude Opus 4.8 a0bc739537 chore(claude): allow WebFetch developer.shopware.com
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 02:25:30 +02:00
kevinveenbirkenbachandClaude Opus 4.8 a4da2faa12 chore(claude): allow WebFetch docs.nextcloud.com
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 01:18:57 +02:00
kevinveenbirkenbach 33794d3067 chore(claude): allow WebFetch registry.opencode.de 2026-07-06 05:05:11 +02:00
kevinveenbirkenbachandClaude Fable 5 a09e069ea4 chore(settings): allowlist WebFetch domains used for limit research
Adds frappe.io, discuss.frappe.io, docs.zammad.org, www.kixdesk.com, web.archive.org, amble.blog to the WebFetch allowlist (surfaced while researching upstream memory requirements for the container-worker resource limits).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 00:00:49 +02:00
kevinveenbirkenbachandClaude Opus 4.8 eec20c15ca chore(claude): allow the artifact-design skill
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 22:18:21 +02:00
kevinveenbirkenbachandClaude Opus 4.8 84296b2451 chore(claude): allow WebFetch to kix + listmonk release domains
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 00:26:06 +02:00
kevinveenbirkenbachandClaude Opus 4.8 e704e9a74d chore: autoformat + lint fixes; drop Invariant comment marker
autoformat reformats (settings sort, complexity/model.py, test_database_query.py, keycloak vars wrap); shorten akaunting swarm task name to <=80 chars; delete narration comments from keycloak vars; remove 'invariant' from the comment linter's valid marker set. make autoformat + make test verified green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 02:54:20 +02:00
kevinveenbirkenbachandClaude Opus 4.8 eb79bbfb32 chore(claude): allow research WebFetch domains
Add forum.keycloak.org, docs.redhat.com, git.coopcloud.tech and synacktiv.com to the WebFetch allowlist used during swarm/collabora/keycloak research.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 02:29:40 +02:00
kevinveenbirkenbachandClaude Opus 4.8 cdebf2e465 chore(settings): sort permissions.allow in .claude/settings.json
Autoformat re-sorted the permissions.allow list. No functional change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 21:52:24 +02:00
kevinveenbirkenbachandClaude Opus 4.8 6ce6780575 refactor(meta): split server.yml into csp/domains/networks.yml
Extract the csp, domains, and networks blocks out of roles/*/meta/server.yml
into dedicated meta files, each mapping to its own top-level applications key
via the recursive meta-walk:

  csp:      -> meta/csp.yml      -> applications.<app>.csp
  domains:  -> meta/domains.yml  -> applications.<app>.domains
  networks: -> meta/networks.yml -> applications.<app>.networks

Residual nginx/proxy keys (status_codes, client_max_body_size, locations,
ip6_enabled, config_upstream_url) stay in server.yml; an emptied server.yml is
deleted. 98 server.yml -> 249 new files, 74 deleted.

- Consumers repointed applications.<app>.server.{csp,domains,networks} ->
  applications.<app>.{csp,domains,networks} across templates, vars, tasks,
  lookups and filters.
- Infra readers migrated: canonical_domains_map, application_domain_index,
  build_ldap_role_entries, service-registry overlay discovery,
  roles_with_service, and the file-based readers utils/domains/list.py +
  utils/meta/scan.py (read the now-deleted server.yml directly).
- Detection + mapping: _META_TOPICS gains csp/domains/networks;
  ROLE_FILE_META_{CSP,DOMAINS,NETWORKS} constants + ROLE_FILES entries.
- New self-expiring lint guard (test_server_meta_no_split_fields) fails if a
  server.yml still carries a split field; auto-fails from 2026-12-28 as a
  delete reminder.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 22:45:05 +02:00
kevinveenbirkenbachandClaude Opus 4.8 e14643bb1e chore(settings): allowlist WebFetch research domains (autoformat-sorted)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 03:07:47 +02:00
kevinveenbirkenbachandClaude Opus 4.8 7e513d1361 chore(settings): sort permissions.allow via autoformat
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 01:58:10 +02:00
kevinveenbirkenbachandClaude Opus 4.8 db7c2cf5f3 chore(claude): allow WebFetch to sdk.collaboraonline.com
Added to permissions.allow during the collabora swarm-convergence investigation (Collabora SDK docs lookup for the mount_jail_tree knob).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:55:57 +02:00
kevinveenbirkenbachandClaude Opus 4.8 c76c6da6a8 feat(svc-storage-nfs-server): userspace nfs-ganesha flavor for the swarm-test + refactor agent iteration docs
svc-storage-nfs-server gains a backend flavor selected by services.nfs-server.flavor (default "{{ 'ganesha' if RUNTIME in ['dev','act'] else 'kernel' }}"). The ganesha flavor installs nfs-ganesha + FSAL_VFS and renders /etc/ganesha/ganesha.conf (NFSv4, Pseudo == Path so clients mount the real paths unchanged, Root_Squash on the export base and No_Root_Squash on the state subdir), reloaded via systemctl restart. It runs in the dev/act DinD swarm-test, where the host kernel nfsd is not network-namespaced and a privileged node driving exportfs would wedge it into a D-state. The kernel flavor keeps nfs-kernel-server + exportfs -ra for production and every non-dev/act runtime. 01_core dispatches on the flavor, 03_prepare holds the shared export preparation, flavor/{kernel,ganesha}/main.yml hold the per-backend install/render/service steps, and handlers gains the ganesha restart.

docs/agents/action/iteration: role.md renamed to compose.md (Compose Loop); new swarm.md for the swarm deploy loop split out of workflow.md; workflow.md trimmed to GitHub Actions workflow iteration; all cross-references updated; the duplicated README gotcha removed; the NFS gotcha rewritten to the ganesha design.

Also drops a retired-flag history comment in group_vars/all/01_modes.yml and a redundant comment in the init selection script, and allows WebFetch(repology.org) used during ganesha packaging research.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 07:30:34 +02:00
kevinveenbirkenbachandClaude Opus 4.8 e8c00c5dca chore(claude): record WebFetch(deepwiki.com) permission
Auto-recorded while a research agent fetched deepwiki.com; added in sorted
position in permissions.allow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 18:31:36 +02:00
kevinveenbirkenbachandClaude Opus 4.8 38784fbd17 chore(claude): allow WebFetch for cdn01.quay.io
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 18:31:35 +02:00
kevinveenbirkenbachandClaude Opus 4.8 b03719106f chore(claude): allow WebFetch(domain:typesense.org), sorted into the allow list
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 03:46:13 +02:00
kevinveenbirkenbachandClaude Opus 4.8 e09dc10720 chore(claude): allow docs.pretix.eu and owncloud.dev WebFetch
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 19:29:39 +02:00
kevinveenbirkenbachandClaude Opus 4.8 5cd9053b05 docs(requirements): add 028 atmo.rsvp role + Mobilizon bridge
File requirement 028 for a self-hosted web-app-atmo role (upstream
flo-bit/atmo-events) that reuses web-app-bluesky for AT Protocol
identity, plus a one-way AT Protocol -> ActivityPub event bridge to
web-app-mobilizon. Index it in the requirements README and allow
WebFetch(atmo.rsvp), used while researching the upstream project.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 23:35:25 +02:00
kevinveenbirkenbachandClaude Opus 4.8 0863b83c92 chore(claude): sort permissions.allow, record git add/log allows
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 11:26:26 +02:00
kevinveenbirkenbachandClaude Opus 4.7 2539ba5cb2 chore(claude/settings): allow writes to ~/.cache/act for sandbox act runs
Adds ~/.cache/act to the sandbox filesystem allowWrite list so the
act-swarm-zombie iteration loop can refresh per-action git caches
(actions/upload-artifact@v7 etc.) without hitting "Read-only file
system" when act re-clones upstream tags. Mirrors the existing
~/.cache/{gh,pip,pre-commit} entries.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 00:17:10 +02:00
kevinveenbirkenbachandClaude Opus 4.7 44f65e2fbe chore(claude): allow WebFetch to docs.docker.com and forums.docker.com
Added while researching docker swarm vs compose mode-incompatibility
during the compose_restart / mode-incompatible-keys audit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 02:24:50 +02:00
kevinveenbirkenbachandClaude Opus 4.7 2cdf1f1a31 chore: swarm-mode template lint, act tweaks, formatting
Add tests/lint/ansible/jinja/test_no_ansible_facts_in_templates.py: under
swarm-mode the rendered template lands once on the primary manager and is
slurp-distributed to every other node, so any per-host fact reference
(ansible_hostname, ansible_default_ipv4, ansible_fqdn, ansible_nodename,
ansible_machine_id, ansible_eth0, ansible_all_ipv*_addresses) would
silently produce wrong output on the other nodes. The lint blocks them
at PR time.

scripts/tests/deploy/act/workflow.sh forwards --action-offline-mode +
--action-cache-path so repeated local act runs do not refetch the actions
catalog.

roles/sys-ctl-cln-anon-volumes/tasks/01_core.yml quoting cleanup applied
by autoformat (single to double quotes, no semantic change).

.claude/settings.json adds Workflow to permissions.allow so the
multi-agent Workflow tool no longer prompts per invocation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 17:11:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 bdea4987e7 chore(claude/settings): align allowlist with cmd + quay reachability
`make exec` was a leftover from before the target was renamed to
`compose-exec`; quay.io is reached by the new image-reachability test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 05:31:16 +02:00
kevinveenbirkenbachandClaude Opus 4.7 5ed0a895ab chore(sandbox): allow *.docker.io for Docker Hub pulls
CI deploys (and the local DinD bridge) pull upstream images
(alpine, getfider/fider, matomo, ...) from Docker Hub. The sandbox
network filter previously allowed *.docker.com but not the .docker.io
TLD where Docker Hub's registry/auth endpoints live, so TLS handshakes
from the registry-cache MITM proxy to registry-1.docker.io / auth.docker.io
timed out.

Wildcard matches the existing *.docker.com pattern and covers any
future Docker Hub subdomain (image blob redirects, etc.).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 04:01:39 +02:00
kevinveenbirkenbachandClaude Opus 4.7 cb0323c74e chore(sandbox): allow docker buildx state writes under ~/.docker/buildx
docker build invokes buildx which writes activity timestamps and
instance state under ~/.docker/buildx/{activity,current,defaults,instances,refs}/.
The sandbox previously blocked this with read-only filesystem errors
during local builds (`make build`, `make up`, `make deploy-fresh-purged-apps`).

Restricting the grant to the buildx subdirectory (not all of ~/.docker)
keeps credentials in ~/.docker/config.json and the auth token in
~/.docker/.token_seed protected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 02:55:22 +02:00
kevinveenbirkenbachandClaude Opus 4.7 2305f08553 chore(.claude/settings): keep permissions.allow ASCII-sorted (googletagmanager.com)
The runtime auto-allow appended ``WebFetch(domain:www.googletagmanager.com)``
at the end of the array after the previous WebFetch against the GTM
domain. ``test_arrays_are_ascending`` failed; resort the entry into
the correct alphabetical slot between ``www.google.com`` and
``www.jenkins.io``. No semantic change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:13:08 +02:00