v0.4.10
Signedpublished August 19, 2026What changed
Added
-
Session, lockout, OAuth and HSTS lifetimes are settings now, not constants compiled into the binary. Every one of them was a Go value, so the only way to run a test or staging host on different numbers was to build your own image. Two cases made that hurt. A host running automated auth suites trips the login lockout constantly, and the counters are in-process — restarting the server was the only way to clear one. And a staging hostname that took the two-year
Strict-Transport-Securityheader could never be served over plain HTTP again, because onlymax-age=0undoes that and there was no way to send it.DEVGRAIL_SESSION_TTL,DEVGRAIL_SCOPED_TOKEN_TTL,DEVGRAIL_LOGIN_MAX_FAILURES,DEVGRAIL_LOGIN_LOCKOUT, the threeDEVGRAIL_OAUTH_*_TTLvalues andDEVGRAIL_HSTS_MAX_AGEare all optional and all documented indeploy/.env.example; leaving them unset is exactly what shipped before, because the defaults still live in the packages that enforce them rather than being copied into the configuration.A value that does not parse is a startup error, not a skipped assignment.
DEVGRAIL_SESSION_TTL=24leaving 24h quietly in force under a name that says it changed is the failure these variables exist to prevent. -
GET /settings/policytells the dashboard what this binary actually validates against. Settings carried its own copy of the minimum password length and the SSH port bounds, kept in step with the server by a comment. Raise the minimum and the form went on accepting a password the API would reject; the message the user got was the API's, arriving after they had typed it twice. The page now reads the rules — password lengths, SSH port range and defaults — from the server it is talking to. If that fetch fails it offers no hint and no input bounds at all and lets the API decide, rather than falling back to a number that may be wrong.
Changed
-
The Traefik image is a setting.
TRAEFIK_IMAGEindeploy/.envnames the full reference, pin included, so an install can pull it from a private mirror or an air-gapped registry. Unset, it is the same pinned version the compose file has always used. -
The Claude.ai OAuth callback is no longer unconditional. The connector's redirect URI was accepted whatever a client registered, and there was no way to say "none" — an install that will never talk to claude.ai carried it anyway.
oauth_extra_redirect_urisinconfig.yaml(orDEVGRAIL_OAUTH_EXTRA_REDIRECT_URIS) sets the list; unset still means the documented Claude.ai callback, so nothing changes unless you change it, and an empty list means none. -
A server with no registry configured says so instead of pointing at the vendor.
registry_urlhad a compiled-in default, so an install that was never pointed at a registry silently asked devgrail.com about updates, and Settings → About offered an upgrade link on an origin that host had nothing to do with. It now does nothing and logs why, and the link is omitted rather than pointing at a page this server does not serve.install.shhas always written the key, so no installed host is affected. -
The backup directory is reported from configuration rather than assumed to be
/opt/devgrail/backups.install.shrecords where it actually put it, and the two messages that send an operator to find a snapshot — the schema-too-new refusal and the missing-jwt_secretrefusal — name that path instead of a default that may not be where the backups are.
Container images
Installer files
Signature
One signature covers every artifact above. It is made off this host, and neither this site nor the registry behind it can check it — that is the point, and it is why verifying it catches things a checksum from the same server cannot. How to verify it.
curl -fsSL https://web-dev.trixibot.com/api/registry/releases/v0.4.10 -o release.json
# -j, not -r: -r appends a newline the signer never saw, and the check fails.
jq -j '.signature.payload' release.json > payload
jq -r '.signature.signature' release.json | base64 -d > payload.sig
openssl dgst -sha256 -verify devgrail-release.pem -signature payload.sig payload
# -> Verified OK
# The payload must name this release, so an older signed one cannot be replayed.
head -2 payload
# ...and it must name this exact file. -qxF: whole line, literal.
grep -qxF 'image devgrail-container 18 amd64 a74634b0405a189b1b960c4241781a0454ed6175b6cba7d208fba5ff60b31988' payload && echo "digest is covered by the signature"The signed payload — shown for reading, not for verifying
Copy this from the manifest with jq -j, never from this page: the signature covers exact bytes, and rendering here drops the trailing newline the signer included.
devgrail-release-signature-v1
release v0.4.10
asset docker-compose.yml 17 5b2a3db83132da5e0611dbfdb9993b03690ae4a13a27bed45f24eba1eb637e67
asset install.sh 17 6122e56cdb0b9f8dc11f6ec5f81644f0c5ed5c4e0da567d72e2ad9d8ab8527aa
asset traefik-dynamic-http.yml 16 e344c8efd13c7d84996e2ebfbc75dcee2d995d8538c8a245c89c0e44fad0089b
asset traefik-dynamic.yml 17 d18fa561b8eb7d53b015284e0246b2d05953bf281b65269c1fae3d6883530db6
asset uninstall.sh 17 d8dc586f38e0cf33eadc126d75b303c660e970e55028f60ad4e2872c40cbaa71
image devgrail-container 18 amd64 a74634b0405a189b1b960c4241781a0454ed6175b6cba7d208fba5ff60b31988
image devgrail-container 18 arm64 39c4b4c5b5fdcb5640fb239b9249e1a1c40083c3cc9a29234ce217e5596a8563
image devgrail-server 18 amd64 46c5599abb6416e4d6701e2ab8ee4ff98be5606f5006dbcf87db38dbbd975176
image devgrail-server 18 arm64 5f7f24178e16e63f11090bd39aff84dbff89870229a1d4c5075bafdca17b20dd