Secondary git worktrees live under ~/.local/share/worktrees. The sandbox did
not list that path in allowWrite, so every write from a session working in
one was denied.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
make install-lint resolved each tool's latest tag at install time, so a DNS
hiccup failed the target outright and two runs of the same commit could
install different linter versions.
roles/sys-lint/meta/services.yml now records the repository and ref of
actionlint, hadolint and shfmt. The pairs use the shape
utils/update/repository.py already scans, so update-repository-refs bumps
them by pull request. utils/install/lint/pinned.py reads the pin and applies
the <TOOL>_VERSION override; the three installers lost their duplicated
resolver along with resolve_latest_tag.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The swarm drill provisions a fresh inventory each round: _provision runs
the services disabler, which writes enabled: false, shared: false for
every service in `disable=`. _force_shared_db then ran four seconds
later and flipped shared back to true, because it only ever looked at
shared and never at enabled.
Its key set comes from every flagged top-level key of the svc-db-* roles,
so it covers svc-db-mariadb's own consumer entries tor and
container_backup, both of which sit on the disable list. The result was
enabled=False, shared=True, and the deploy aborted on the consistency
guard in services_disabler.
Guard the flip on enabled instead of narrowing the key set: the key set
would only drop today's two keys, while postgres, redis, memcached,
qdrant, typesense and ldap are all equally on the live disable list.
`is not False` keeps flipping entries that carry no enabled key at all.
derive_includes mirrors force_shared_db by contract, so it gets the same
condition; otherwise the dependency walk sees a different topology than
the deploy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GitHub cuts display_title at 512 UTF-8 bytes and closes it with a
literal '...'. The run-name template renders priority second-to-last
and whitelist last, and on the --failed path the whitelist is empty, so
the cut always lands inside the priority list: 21 of 69 roles survived
the source run's title, the 21st a half-token.
untriggered_priority parsed that title and passed undecodable names
through verbatim, so the fragment reached the dispatched priority
input, became the orchestrator's whitelist, and hit the complexity
filter's lexer, whose word class has no rule for CJK. Sequencing died
with argparse exit 2 and took the run with it. The retrigger that is
supposed to repair a red run was breaking its successor.
Read the priority from the ##[group] Inputs block a called job's log
records instead: it carries all 69 roles. Depth-1 jobs only -- a deeper
one echoes its own workflow's inputs, which have no priority.
Drop the decode passthrough now that the source is lossless. The loud
failure is only affordable once a whole value exists; before, it would
have aborted every large retrigger.
dispatched_priority cross-checks against the title: it still proves a
priority line existed even when truncation mangled it, so an empty log
read is a broken reader -- expired logs, a moved block -- and not a run
without priority. Without that check the carry-over would vanish
silently, which is what the rest of this change exists to prevent.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Removing the `# --- section ---` banners in 2ab8ec6af left the blank line
that had sat above each one, so three method boundaries in
test_rbac_group_path.py and two in test_sso_proxy_consumers.py carried two
blank lines inside a class body where PEP 8 wants one.
Whitespace only: five deletions, no additions, no code touched.
Verified: ruff format --check reports both files already formatted, and
pytest over the two => 23 passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
make install-lint resolved each tool's latest tag at install time, so a DNS
hiccup failed the target outright and two runs of the same commit could
install different linter versions.
roles/sys-lint/meta/services.yml now records the repository and ref of
actionlint, hadolint and shfmt. The pairs use the shape
utils/update/repository.py already scans, so update-repository-refs bumps
them by pull request. utils/install/lint/pinned.py reads the pin and applies
the <TOOL>_VERSION override; the three installers lost their duplicated
resolver along with resolve_latest_tag.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
74 comments restated the assertion below them, narrated a step the test
name already carries, banner-separated sections, or cross-referenced
another file. All deleted; the only touched code lines are ones that
carried a trailing comment, so the test count is unchanged.
Six of the 74 were invisible to the repo's own comment lint.
find_invalid_comments() exempts any comment block whose next code line is
a def/class (block_def, test_comments_valid.py:385), so section banners
sitting above a test method pass, and real docstrings are never inspected
at all. That let five `# --- section ---` banners through, plus a class
docstring in test_rbac_group_path.py that cited commit 10cb02461 as the
reason the guard exists -- provenance, which belongs in a commit message
and not in the source. A green comment lint is therefore not evidence
that a file complies.
Module docstrings that state what a file covers are kept: describing
behaviour is what the header slot is for.
Verified: checker reports 0 offenders across all twelve; pytest over the
same twelve => 180 passed, 4 subtests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
40 comments in test_database.py and test_sso_oidc_plugin.py restated the
assertion directly below them ("id should be present", "enabled/shared
surfaced") or narrated a step the test name already carries. The repo's
comment lint flagged all of them once the files entered its scope.
The sys.modules eviction rationale above _load_module moves into that
function's docstring, where behaviour documentation belongs; everything
else is deleted outright. Trailing comments go too: Python is linted via
tokenize, so unlike the line-based languages its inline comments are in
scope.
Verified: pytest over both files => 20 passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`tar.extractall(..., filter="data")` already makes the call safe, so ruff
no longer raises S202 and the `# noqa` is dead. Its stated reason had also
gone stale: path traversal is rejected by the filter now, not by the
member loop the comment credits.
RUF100 (unused-noqa) is selected, so the suppression would have started
failing lint on its own.
Verified: ruff check --no-cache utils/install/lint/actionlint.py => clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
59 test files imported `unittest` and then `from unittest import mock`,
giving the same names two spellings so a call site no longer says which
one it follows. CodeQL flags this as py/import-and-import-from; at 59
occurrences it is the single largest item in the repo's code-scanning
backlog.
Keep `import unittest` and reach the submodule as `import unittest.mock
as mock`. Both are ast.Import, which is what the rule asks for, and
`mock.<attr>` call sites stay untouched — the diff is exactly one line
per file and nothing else.
The opposite direction would have been wrong here: PLR0402 rewrites
`import a.b as c` into `from a import b`, i.e. straight back into the
pattern CodeQL flags. It is deliberately absent from the ruff select
list in pyproject.toml, so nothing in the toolchain will undo this.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The scenario never authenticated. It went to /auth/sign_in and then only
checked whether the URL had reached openid-connect/auth, on the assumption
that OMNIAUTH_ONLY=true makes Mastodon redirect into the OIDC chain. It does
not — Mastodon renders its own login page carrying an /auth/auth/openid_connect
link, so the condition was always false, performKeycloakLogin never ran, and
the run continued unauthenticated until it failed 1.5 min later on a missing
avatar input.
Route through clickOidcLoginLink first, exactly as personas/admin.js does; its
provider-shaped selector matches Mastodon's link. The persona logout tests on
this same role were already green through that helper — the spec had bypassed
it and rebuilt the condition by hand.
With the login repaired, a second defect surfaced behind it: newer Mastodon
moved profile editing to /profile/edit and left /settings/profile as a notice
page with no avatar input. Follow the pointer link when it is present, so the
spec still works against older builds.
Both causes come from the Playwright error-context snapshots, not from reading
the code: the first showed Mastodon's login page, the second the logged-in
settings navigation plus the relocation notice.
Verified: make compose-playwright role=web-app-mastodon pw="--grep seaweedfs"
=> 1 passed (30.3s), previously 3 x 1.5 min timeout. The check uploads and then
polls the Filer UI until a new bucket object appears, so Mastodon -> SeaweedFS
is proven end to end.
Also drops the file-header paragraph, whose OMNIAUTH_ONLY claim was the false
premise the broken conditional was built on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GitHub cuts display_title at 512 UTF-8 bytes and closes it with a
literal '...'. The run-name template renders priority second-to-last
and whitelist last, and on the --failed path the whitelist is empty, so
the cut always lands inside the priority list: 21 of 69 roles survived
the source run's title, the 21st a half-token.
untriggered_priority parsed that title and passed undecodable names
through verbatim, so the fragment reached the dispatched priority
input, became the orchestrator's whitelist, and hit the complexity
filter's lexer, whose word class has no rule for CJK. Sequencing died
with argparse exit 2 and took the run with it. The retrigger that is
supposed to repair a red run was breaking its successor.
Read the priority from the ##[group] Inputs block a called job's log
records instead: it carries all 69 roles. Depth-1 jobs only -- a deeper
one echoes its own workflow's inputs, which have no priority.
Drop the decode passthrough now that the source is lossless. The loud
failure is only affordable once a whole value exists; before, it would
have aborted every large retrigger.
dispatched_priority cross-checks against the title: it still proves a
priority line existed even when truncation mangled it, so an empty log
read is a broken reader -- expired logs, a moved block -- and not a run
without priority. Without that check the carry-over would vanish
silently, which is what the rest of this change exists to prevent.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The member loop already rejects path traversal, but extractall carried no filter, which is what CodeQL flags as py/tarslip. Naming data makes the guarantee independent of the interpreter and of any global extraction_filter a different code path might set.
On Python 3.14 this is a no-op: _get_filter_function falls back to data_filter when none is given, and a probe archive carrying ../escaped is refused identically with and without the argument. The protection is real on 3.12 and 3.13, where the default is None with a deprecation warning, and the project builds images for five distributions.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
svc-registry-cache is the twin of the registry volume fixed in 1e5d2abb8: same /var/lib/registry target, same missing declaration, and the same claim in its own first line that the volume is backup-disabled. Matomo's tmp entry likewise already documents a regenerable Twig and session cache. Neither statement was true, because backup_volume.py keys on the declaration alone.
Also jellyfin's /cache, which sits beside the /config volume that stays backed up, and nextcloud's whiteboard tmp, whiteboard fontcache and talk recording tmp.
Left alone: gitlab's builds volume holds CI artifacts, which are reproducible but whose retention is a product decision, and the gitlab config templates, the cdn build script and the openldap ldif bind, which a name-based scan turns up but which are not caches.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The file's own header already claims the volume is backup-disabled, but nothing declared it, so the cluster's local registry - a compressed copy of every image built for the closure - went into the backup at 6185 MB. The NFS dedup path cannot exclude it either, because it short-circuits on placement: manager, and the image-level escape hatch compares against a ref the registry container cannot run under.
That is what tipped the swarm DR drill's disk precondition: 16397 MB required against 14825 MB free. Removing the bytes helps twice over, since capture and free space share one filesystem, and the guard passes from 786 MB saved onwards.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ACCOUNT_MENU_NAME_RE matched profile unanchored, so Mastodon's public 'Profiles directory' link counted as an account menu. The sweep clicked it, navigated the authenticated SPA onto a Rails page with no settings link, and the settings fallback added in 141ff0645 then ran with nothing to find.
Across all 53 traces of run 31195451847 the only names that lose their match are 'profiles directory' and 'browse profiles' - the target - plus the class name profiler-toolbar, which is never an accessible name.
actionTimeout was unset, so Playwright's default of 0 let a click hang until the whole 300s test budget expired; Fediwall 2 reports an error with an empty body instead of a clean failure because of it. The slowest successful action in those traces takes 1.6s.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mkstemp creates its file 0600 and Path.replace carries that mode into the target, so every freshly rendered Corefile was owner-only while coredns runs as nonroot. The container exits with 'open /Corefile: permission denied', name resolution dies for the whole stack, and deploys then fail at whatever they try to fetch first.
Long-running stacks hid it: coredns reads its config once at startup, so a container created before a re-render keeps working. Only new worktree stacks broke.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every write scope sat at workflow level, so it reached each job in the file: entry-push-latest handed contents, packages, actions, security-events and issues write to all eight of its jobs, and call-orchestrator to all twenty-four, including every deploy and test job of the 134-job runs. The scopes now sit on the jobs that push, release or upload SARIF.
call-test-workspace, -install-make and -install-pkgmgr declared actions: write with no consumer at all, while passing the raw GITHUB_TOKEN into containers that start from mutable public tags and mount the docker socket. cron-cleanup-stale gave packages: write over the whole namespace to two jobs running third-party JavaScript that never touch a registry.
Eight single-job workflows are deliberately left alone: their one job needs the scope, so moving it produces an identical effective token while risking a job block that silently replaces contents: read. A check across every uses: edge confirms no callee now declares more than its caller grants.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
assert-main-merged checked out the PR head and then executed scripts/git/assert/main_merged.sh from that same tree. Under pull_request_target the job holds the base repository's token, so a fork PR could run anything it liked with write access. This is the run's only critical scanner finding.
The workflow file itself comes from the base branch and is trusted; only the checked-out files are not. Two checkouts now separate them: the script comes from .base-tooling, the tree it inspects is pr-head, and neither checkout persists credentials. Both repositories are public, so the script's git fetch still resolves anonymously, and git rev-parse --show-toplevel follows the working directory rather than the script's location - both verified by execution.
The same pass drops the top-level write scopes to contents: read, moves packages: write onto the two fork image jobs, and downgrades the orchestrator call site to actions: read now that call-orchestrator no longer declares actions: write.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>