Releases: oedokumaci/dead-mans-switch
v2.1.0 — opt-in public-GitHub-activity liveness signal
Highlights
Adds an opt-in second-chance liveness signal. When CHECK_PUBLIC_ACTIVITY=true and a heartbeat interval has elapsed, the switch consults your public GitHub activity feed before advancing state — recent non-bot events on any repo other than this one keep the switch ALIVE for one more cycle. Off by default; zero behaviour change unless you explicitly enable it.
Implements docs/PLAN_liveness.md. Went through 4 rounds of multi-agent review (14 reviewers total) before merge.
Why
If you commit frequently to other repos but rarely to the DMS repo, the v2.0 commit-only liveness check fires false-positive warnings. The new override consults your public activity feed and rescues the switch when you're alive-but-quiet-here.
Setup (when you want it on)
- Create a 1-year fine-grained PAT (leave all permission checkboxes at No access — endpoint is the public events feed).
- Add it as repo secret
GH_ACTIVITY_TOKEN. - Add repo variable
CHECK_PUBLIC_ACTIVITY=true. - (Optional)
GH_USERNAME,BOT_AUTHOR_PATTERNS,BOT_MESSAGE_PATTERNS.
See the README's Optional: GitHub activity as fallback liveness signal section for the click-by-click walkthrough, hard limitations, and threat model.
Security posture
- Custom
urllibopener installs noHTTPRedirectHandler(Authorization-header leak defense) but DOES installHTTPErrorProcessor+HTTPDefaultErrorHandlerso 4xx/5xx raiseHTTPErrorand route to the fail-closed branch - Regex inputs capped at 4 KB (ReDoS defense on long PR/issue bodies)
- Per-event errors caught and skipped; the bot-filter call sits OUTSIDE that try, so filter-logic bugs propagate (loud failure, not silent over-fire)
- Fail-closed network branch catches
URLError(incl.HTTPError),JSONDecodeError,TimeoutError, ANDhttp.client.HTTPException(the last doesn't subclassURLError) - Generic
::warning::text on fail-closed (no detail leaked as a token-expiry / rate-limit timing oracle) - Token shape validated at startup;
CHECK_PUBLIC_ACTIVITYstrict-parsed (yes/1/TRUE all raise) - Schema-drift guard: a non-list 200 body also fails closed
Hard invariants preserved
ALREADY_DECLARED_DEADandDISARMEDstill take precedence — override gate runs only if interval has elapsed AND feature enabled AND not a manual dispatch- Manual dispatch short-circuits the API call entirely (no rate-limit burn on every "Run workflow" click; config validation still runs)
- Heartbeat intervals longer than 30 days silently clamp to GitHub's events-feed ceiling with a
::notice::
Stats
- Code: +423 LOC in
dead_mans_switch.py - Tests: +1351 LOC in
tests/test_liveness.py(332 passing, 1 skipped integration test) - Docs: +221 LOC in README
- Workflow: 1 added line (
CHECK_PUBLIC_ACTIVITY: "false"default) - 100% line + branch coverage maintained (enforced via
--cov-fail-under=100) - No new third-party dependencies (stdlib
urllib,json,http.client,re,datetimeonly)
Upgrade
If you don't want the feature, do nothing — it's off by default. To enable, follow the README walkthrough above.
Full Changelog: v2.0.0...v2.1.0
v2.0.0 — Production hardening
Substantial hardening pass after multiple independent review rounds. 201 tests at 100% line + branch coverage, enforced via --cov-fail-under=100. The single-file Python script shrinks from 1146 lines to ~890 while gaining real defensive code (the v1.0.0 version was mostly doctest scaffolding).
What's new
Code (dead_mans_switch.py)
- State machine handles clock skew, push race conditions, partial delivery, multi-line subjects, NaN/inf intervals, and future-dated commits.
- PASSED_AWAY is now permanent — no revival on owner heartbeat after fire (prevents duplicate mortality storms after vacation false-positives).
- Strict env-var substitution allows bare
$(so"$5","$HOME"work in bodies) but errors on missing${VAR}. - Email parsing rejects header injection (CRLF, comma, whitespace); Subject also rejects control characters (
\r \n \v \f \x00). - Bot identity uses both
%anAND%aeto defeat impersonation bygit config user.name=dms_botalone. EmailandCommitare frozen dataclasses to prevent post-init mutation bypassing validation.- Per-commit bot identity via env vars (no global git config dependency).
_remote_has_commitpost-push verification handles the rare TCP-RST-after-success race.
Workflow (.github/workflows/dms.yaml)
actions/checkoutwithfetch-depth: 0andpersist-credentials: false.- Concurrency group prevents cron /
workflow_dispatchraces. - Multi-line secrets survive intact via base64 + random-delimiter heredoc.
- Reserved-name allowlist refuses
PATH,HOME,GITHUB_*,RUNNER_*,BASH_ENV,IFS, etc. workflow_dispatchinputs always routed viaenv:blocks (no shell-injection surface).- Strict
ARMEDparsing;type: choicedropdown. timeout-minutes: 15bounds the damage from a hung SMTP handshake.
Tests
- 201 pytest cases at enforced 100% line + branch coverage.
- Fixtures: real-git temp repos, controllable fake SMTP, env isolation.
@pytest.mark.bugregression tests for every fixed behavior.- Workflow YAML lint tests.
Documentation
- Corrected provider count (11 providers across 18 domain aliases).
- ProtonMail caveat: Bridge / SMTP tokens required for direct SMTP.
- New "Required Template Variables" table.
- Manual-dispatch semantics documented explicitly.
- New "Resetting the switch after a false-positive fire" section with the force-push escape hatch and the strong recommendation to disable the workflow first.
- Threat-model statement: private repo, single owner, no collaborators, no branch protection on the working branch.
Breaking changes from v1.0.0
- Empty
emails/directory now raises in all states (was: silent succeed in DISARMED). Rename your.txt.templatefiles to.txtbefore arming. MY_EMAILis regex-validated at startup. A secret with stray whitespace or trailing CRLF will fail loudly. Re-paste if your run fails immediately on startup.- Strict
${VAR}substitution. Templates with missing variables now fail loudly instead of leaving the literal placeholder. Verify all your template vars are set as repo secrets/variables. PASSED_AWAYis permanent. If your switch ever fired accidentally, see the new "Resetting" section in the README to clear the terminal commit.- Manual dispatch never advances state and parses templates even in
ISSUE_WARNING/PASSED_AWAYstates.
How to upgrade
If you used this repo as a template (most common):
- Copy the v2.0.0 files into your own dead-man's-switch repo.
- Re-run step 5b (manual
armed=falsedispatch) to verify your setup.
If you forked:
git fetch upstream && git merge upstream/mainand resolve any conflicts in your customizedemails/directory.
Threat model
This switch is intended for a private personal repository with a single owner and no collaborators. The bot-identity check (now both name AND email) raises the bar against accidental impersonation but cannot defeat a determined contributor with push access. Do not enable branch protection on the bot's working branch — required reviews or signed commits would block the bot's own commits.
Full Changelog: v1.0.0...v2.0.0
v1.0.0
Full Changelog: https://github.com/oedokumaci/dead-mans-switch/commits/v1.0.0