Skip to content

Releases: oedokumaci/dead-mans-switch

v2.1.0 — opt-in public-GitHub-activity liveness signal

08 Jun 16:04

Choose a tag to compare

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)

  1. Create a 1-year fine-grained PAT (leave all permission checkboxes at No access — endpoint is the public events feed).
  2. Add it as repo secret GH_ACTIVITY_TOKEN.
  3. Add repo variable CHECK_PUBLIC_ACTIVITY=true.
  4. (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 urllib opener installs no HTTPRedirectHandler (Authorization-header leak defense) but DOES install HTTPErrorProcessor + HTTPDefaultErrorHandler so 4xx/5xx raise HTTPError and 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, AND http.client.HTTPException (the last doesn't subclass URLError)
  • Generic ::warning:: text on fail-closed (no detail leaked as a token-expiry / rate-limit timing oracle)
  • Token shape validated at startup; CHECK_PUBLIC_ACTIVITY strict-parsed (yes/1/TRUE all raise)
  • Schema-drift guard: a non-list 200 body also fails closed

Hard invariants preserved

  • ALREADY_DECLARED_DEAD and DISARMED still 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, datetime only)

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

08 Jun 11:06

Choose a tag to compare

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 %an AND %ae to defeat impersonation by git config user.name=dms_bot alone.
  • Email and Commit are frozen dataclasses to prevent post-init mutation bypassing validation.
  • Per-commit bot identity via env vars (no global git config dependency).
  • _remote_has_commit post-push verification handles the rare TCP-RST-after-success race.

Workflow (.github/workflows/dms.yaml)

  • actions/checkout with fetch-depth: 0 and persist-credentials: false.
  • Concurrency group prevents cron / workflow_dispatch races.
  • Multi-line secrets survive intact via base64 + random-delimiter heredoc.
  • Reserved-name allowlist refuses PATH, HOME, GITHUB_*, RUNNER_*, BASH_ENV, IFS, etc.
  • workflow_dispatch inputs always routed via env: blocks (no shell-injection surface).
  • Strict ARMED parsing; type: choice dropdown.
  • timeout-minutes: 15 bounds 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.bug regression 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.template files to .txt before arming.
  • MY_EMAIL is 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_AWAY is 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_AWAY states.

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=false dispatch) to verify your setup.

If you forked:

  • git fetch upstream && git merge upstream/main and resolve any conflicts in your customized emails/ 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

27 Jul 21:59
344408f

Choose a tag to compare