The served document runs an inline script at line 38. It is not ours: in the
failing variant the Lua body filter renders an empty snippet list, so nothing
this repository injects can account for it.
Until now the only token that admitted that script came from csp_filters.py,
which grants 'unsafe-inline' to script-src-attr and script-src-elem whenever
services.logout is enabled. socialhome derives that flag from 'web-svc-logout'
in group_names, so the app's own page worked or broke depending on whether an
unrelated service shared the round.
Measured in run 35152776835. Job 105253846799 reports `Blocked URI: inline`,
`Directive: script-src-elem`, `Source: /:38:0`. The two rendered headers differ
in exactly that token: variant 0 emits `script-src-elem 'self' 'unsafe-inline'
...`, variant 1 emits `script-src-elem 'self' ...`. csp-checker exits with the
violation count, so this single violation failed the hlth-csp unit and
sys-service-terminator took the job down after 25 attempts.
Rendering build_csp_header against HEAD and against this change confirms the
delta is bounded: with logout off the directive gains the token, with logout
on nothing moves. script-src-attr stays undeclared because no artifact reports
a violation there.
An inline hash was considered and dropped. No role in this repository declares
meta/csp.yml hashes, the hash would have to byte-match the served document,
and csp_filters.py discards every registered hash whenever 'unsafe-inline' is
present, which is the state this app is in as soon as logout is deployed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
socialhome's own page head links a Google Fonts stylesheet. The deployment CSP did not know the origin, so the browser blocked it, csp-checker reported the violation, the hlth-csp unit failed and sys-service-terminator took the job down with it.
Job 104991069038 carries the only violation in the whole bundle, 52 times: [CSP DOM] Blocked URI: https://fonts.googleapis.com/css2?family=Fraunces:... with Directive: style-src-elem. Fraunces appears nowhere in this repository, so the reference comes from upstream socialhome rather than from our theming.
font-src for fonts.gstatic.com rides along: the stylesheet pulls the font files from there, so declaring only the stylesheet origin would have surfaced the next violation instead of clearing the check. That half is derived from how @font-face resolves and from the four sibling roles that declare the same pair, not observed, because the stylesheet never loaded.
This keeps a clearnet origin reachable from an onion deployment, which the mirror exists to avoid. The sibling roles carry a standing TODO to move such assets to the mirror; socialhome joins that debt rather than adding a new kind of it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The role was a scaffold: tasks/main.yml was a two-line comment, there was no
compose.yml.j2, no domains.yml, no volumes.yml, no env.j2. It deployed nothing
while carrying a Playwright spec and two matrix variants.
It also described the wrong project. meta/main.yml and meta/info.yml pointed at
jaywink/socialhome -- a Django ActivityPub hub. The target here is
social-home-io/socialhome: created 2026-04-21, MPL-2.0, aiohttp + SQLite +
Preact, federating peer-to-peer over Ed25519/WebRTC rather than ActivityPub.
Different codebase, no fork relationship.
It turns out to be the simplest web-app in the repo. One published multi-arch
image (ghcr.io/social-home-io/socialhome:2026.6.16, pinned), no Postgres, no
Redis, no queue, no worker, no collectstatic -- SQLite in one volume, one port,
migrations applied in-process at startup.
Two things the source forced that reading the README would not have:
- [standalone].external_url is reachable only through the TOML file, never
through SH_* env (config.py:238-261). Without it routes/pairing.py answers
422 NOT_CONFIGURED and federation pairing is dead, so the role renders a
config and mounts it as type: config.
- provision_admin() runs from SH_ADMIN_PASSWORD but never calls
mark_complete(), so / keeps serving the setup wizard instead of the login
form -- upstream's own docstring claims otherwise. 01_setup.yml closes it
with an idempotent POST to /api/setup/standalone.
No SSO sidecar, deliberately, against the pgadmin model this otherwise copies:
oauth2-proxy would redirect POST /federation/inbox and /api/pairing/accept to
Keycloak and break the one thing the software exists for.
Verified against the real image rather than reasoned about. The http_status
probe's exact argv ran inside the container and returned 0. /healthz answers
with the byte-for-byte shape the spec asserts. The setup POST returns 201 then
409 and flips setup_required true -> false, which is the whole justification
for 01_setup.yml existing. [standalone].external_url lands in the inbox_url of
POST /api/pairing/initiate. The container runs as uid 10001, matching
meta/users.yml.
Both persona blocks are proven, not assumed: biber gets 401 from the token
endpoint, and the 97 static files contain zero occurrences of logout / sign out
/ abmelden, so utils/logout.js has nothing to click. Injecting a control the
way web-app-prometheus does was rejected -- that model points its button at
OIDC.CLIENT.LOGOUT_URL, which does not exist for a role with no OIDC.
swarm stays disabled for the first round. The orchestrator chains
swarm -> compose -> host under mode_fail_fast, and sequencing measures serial
at 198 jobs, so a red swarm job here would skip 92 unrelated jobs and leave 85
roles untested. Compose-only drops that to 10. Flip it once compose is green.
Adversarial review caught five defects before this landed, three of them
certain rather than likely:
- SOCIALHOME_INTERNAL_URL hardcoded 127.0.0.1, which sys-svc-container rewrites
to the bridge gateway whenever DOCKER_IN_CONTAINER is set -- from inside the
Ansible container that address is the Ansible container, so both setup tasks
would have burned five minutes of retries and failed the play.
- SH_WEBRTC_STUN_URL sat outside the coturn guard while the TURN pair sat
inside it. Upstream reads by env PRESENCE, so the empty value would have
beaten the in-app default and left WebRTC with no STUN at all.
- meta/csp.yml whitelisted blob: on worker-src. The bundle registers no service
worker and constructs no Worker; the only blob of JavaScript is an audio
worklet, and worklet modules load under script-src. Voice recording would
have died with the app's own "Audio processing unavailable".
- tasks/main.yml hand-rolled meta: flush_handlers instead of including
sys-svc-compose's finalize.yml, which silently skipped utils/post_deploy.yml.
- 01_setup.yml was gated on IS_STACK_HOST but not run_once, so a three-manager
swarm would fire three concurrent setup POSTs at one ingress port.
TODO.md carried a claim that steered the CSP design and was simply false --
that img-src is not a directive csp_filters.py emits. It appends
"img-src * data: blob:" unconditionally at line 383.
lifecycle: alpha puts the role in the CI deploy matrix before it has ever run
anywhere, deliberately: the first CI round is its first execution. README and
TODO say so rather than claiming a maturity it does not have.
Still unverified: no browser has enforced the CSP, the swarm path was only
rendered, TURN against a real coturn is untested, and arm64 has not been
exercised. TODO.md lists them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>