deadpush 0.2.1__tar.gz → 0.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. deadpush-0.2.3/.cursorignore +23 -0
  2. deadpush-0.2.3/.github/workflows/ci.yml +66 -0
  3. {deadpush-0.2.1 → deadpush-0.2.3}/.gitignore +3 -4
  4. deadpush-0.2.3/.vscode/mcp.json +10 -0
  5. {deadpush-0.2.1 → deadpush-0.2.3}/AGENT.md +15 -27
  6. deadpush-0.2.3/CHANGELOG.md +293 -0
  7. deadpush-0.2.3/CONTRIBUTING.md +59 -0
  8. deadpush-0.2.3/Formula/deadpush.rb +58 -0
  9. {deadpush-0.2.1 → deadpush-0.2.3}/PKG-INFO +51 -65
  10. deadpush-0.2.3/README.md +182 -0
  11. deadpush-0.2.3/SECURITY.md +186 -0
  12. deadpush-0.2.3/action.yml +26 -0
  13. deadpush-0.2.3/deadpush/__init__.py +1 -0
  14. deadpush-0.2.3/deadpush/cli.py +1566 -0
  15. deadpush-0.2.3/deadpush/config.py +377 -0
  16. deadpush-0.2.3/deadpush/debris.py +165 -0
  17. {deadpush-0.2.1 → deadpush-0.2.3}/deadpush/deps_guard.py +1 -1
  18. deadpush-0.2.3/deadpush/guard.py +2796 -0
  19. deadpush-0.2.3/deadpush/hooks.py +901 -0
  20. deadpush-0.2.3/deadpush/intercept.py +729 -0
  21. {deadpush-0.2.1 → deadpush-0.2.3}/deadpush/layers.py +1 -2
  22. {deadpush-0.2.1 → deadpush-0.2.3}/deadpush/mcp_server.py +131 -214
  23. {deadpush-0.2.1 → deadpush-0.2.3}/deadpush/rules.py +6 -2
  24. {deadpush-0.2.1 → deadpush-0.2.3}/deadpush/session.py +1 -10
  25. deadpush-0.2.3/deadpush/types.py +47 -0
  26. deadpush-0.2.3/deadpush/ui.py +37 -0
  27. {deadpush-0.2.1 → deadpush-0.2.3}/deadpush/verifier.py +1 -2
  28. deadpush-0.2.3/deadpush_bootstrap.py +66 -0
  29. {deadpush-0.2.1 → deadpush-0.2.3}/pyproject.toml +29 -13
  30. deadpush-0.2.3/scripts/brew_release.sh +59 -0
  31. deadpush-0.2.3/scripts/deadpush +60 -0
  32. deadpush-0.2.3/scripts/dev_install.sh +54 -0
  33. deadpush-0.2.3/scripts/full_e2e_test.py +359 -0
  34. deadpush-0.2.3/scripts/hardened_qa.sh +378 -0
  35. deadpush-0.2.3/scripts/soak_test.py +180 -0
  36. deadpush-0.2.3/scripts/stress_test.py +123 -0
  37. deadpush-0.2.3/tests/test_attack_chain.py +106 -0
  38. deadpush-0.2.3/tests/test_bootstrap.py +44 -0
  39. {deadpush-0.2.1 → deadpush-0.2.3}/tests/test_deps_guard.py +0 -7
  40. {deadpush-0.2.1 → deadpush-0.2.3}/tests/test_exhaustive.py +55 -217
  41. deadpush-0.2.3/tests/test_failclosed.py +130 -0
  42. deadpush-0.2.3/tests/test_file_coverage.py +72 -0
  43. deadpush-0.2.3/tests/test_guardian_closed_loop.py +118 -0
  44. deadpush-0.2.3/tests/test_hardened.py +406 -0
  45. deadpush-0.2.3/tests/test_hooks_enforcement.py +71 -0
  46. deadpush-0.2.3/tests/test_hookspath_hijack.py +87 -0
  47. deadpush-0.2.3/tests/test_integration_lifecycle.py +172 -0
  48. {deadpush-0.2.1 → deadpush-0.2.3}/tests/test_intercept_guardrails.py +32 -38
  49. {deadpush-0.2.1 → deadpush-0.2.3}/tests/test_mcp.py +9 -23
  50. deadpush-0.2.3/tests/test_move_delete_events.py +116 -0
  51. deadpush-0.2.3/tests/test_os_hardening.py +45 -0
  52. deadpush-0.2.3/tests/test_prepush_newbranch.py +120 -0
  53. {deadpush-0.2.1 → deadpush-0.2.3}/tests/test_real_repo_e2e.py +31 -33
  54. deadpush-0.2.3/tests/test_root_immutable.py +186 -0
  55. {deadpush-0.2.1 → deadpush-0.2.3}/tests/test_rules.py +1 -2
  56. deadpush-0.2.3/tests/test_tier0_hardening.py +229 -0
  57. deadpush-0.2.3/uv.lock +559 -0
  58. deadpush-0.2.1/.github/workflows/ci.yml +0 -36
  59. deadpush-0.2.1/CHANGELOG.md +0 -19
  60. deadpush-0.2.1/CONTRIBUTING.md +0 -38
  61. deadpush-0.2.1/README.md +0 -188
  62. deadpush-0.2.1/action.yml +0 -34
  63. deadpush-0.2.1/deadpush/__init__.py +0 -1
  64. deadpush-0.2.1/deadpush/churn.py +0 -189
  65. deadpush-0.2.1/deadpush/cli.py +0 -1584
  66. deadpush-0.2.1/deadpush/comments.py +0 -265
  67. deadpush-0.2.1/deadpush/complexity.py +0 -254
  68. deadpush-0.2.1/deadpush/config.py +0 -284
  69. deadpush-0.2.1/deadpush/crawler.py +0 -133
  70. deadpush-0.2.1/deadpush/deadness.py +0 -477
  71. deadpush-0.2.1/deadpush/debris.py +0 -729
  72. deadpush-0.2.1/deadpush/deps.py +0 -323
  73. deadpush-0.2.1/deadpush/entrypoints.py +0 -193
  74. deadpush-0.2.1/deadpush/graph.py +0 -401
  75. deadpush-0.2.1/deadpush/guard.py +0 -1386
  76. deadpush-0.2.1/deadpush/hooks.py +0 -369
  77. deadpush-0.2.1/deadpush/importgraph.py +0 -122
  78. deadpush-0.2.1/deadpush/imports.py +0 -239
  79. deadpush-0.2.1/deadpush/intercept.py +0 -995
  80. deadpush-0.2.1/deadpush/languages/__init__.py +0 -143
  81. deadpush-0.2.1/deadpush/languages/base.py +0 -70
  82. deadpush-0.2.1/deadpush/languages/cpp.py +0 -150
  83. deadpush-0.2.1/deadpush/languages/go_.py +0 -177
  84. deadpush-0.2.1/deadpush/languages/java.py +0 -185
  85. deadpush-0.2.1/deadpush/languages/javascript.py +0 -202
  86. deadpush-0.2.1/deadpush/languages/python_.py +0 -278
  87. deadpush-0.2.1/deadpush/languages/rust.py +0 -147
  88. deadpush-0.2.1/deadpush/languages/typescript.py +0 -192
  89. deadpush-0.2.1/deadpush/reachability.py +0 -183
  90. deadpush-0.2.1/deadpush/registration.py +0 -280
  91. deadpush-0.2.1/deadpush/report.py +0 -113
  92. deadpush-0.2.1/deadpush/sarif.py +0 -123
  93. deadpush-0.2.1/deadpush/scorer.py +0 -151
  94. deadpush-0.2.1/deadpush/security.py +0 -187
  95. deadpush-0.2.1/deadpush/tests.py +0 -333
  96. deadpush-0.2.1/deadpush/ui.py +0 -156
  97. deadpush-0.2.1/deadpush/watch.py +0 -103
  98. deadpush-0.2.1/scripts/full_e2e_test.py +0 -510
  99. deadpush-0.2.1/tests/test_comments.py +0 -169
  100. deadpush-0.2.1/tests/test_tests_analyzer.py +0 -114
  101. {deadpush-0.2.1 → deadpush-0.2.3}/.github/workflows/python-publish.yml +0 -0
  102. {deadpush-0.2.1 → deadpush-0.2.3}/CODE_OF_CONDUCT.md +0 -0
  103. {deadpush-0.2.1 → deadpush-0.2.3}/LICENSE +0 -0
  104. {deadpush-0.2.1 → deadpush-0.2.3}/tests/__init__.py +0 -0
  105. {deadpush-0.2.1 → deadpush-0.2.3}/tests/conftest.py +0 -0
  106. {deadpush-0.2.1 → deadpush-0.2.3}/tests/test_layers.py +0 -0
@@ -0,0 +1,23 @@
1
+
2
+ # Added by deadpush
3
+ **/playground.*
4
+ **/scratch*.md
5
+ **/temp*.py
6
+ **/tmp*.go
7
+ .claude_instructions
8
+ .copilot-instructions.md
9
+ .cursorrules
10
+ .deadpush-archive/
11
+ .deadpush-autoignore
12
+ .deadpush-quarantine/
13
+ .venv/
14
+ __pycache__/
15
+ agents.md
16
+ ai_prompt.md
17
+ claude.md
18
+ dist/
19
+ llm_context.txt
20
+ node_modules/
21
+ target/
22
+ venv/
23
+ windsurf_rules.md
@@ -0,0 +1,66 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ env:
10
+ PYTHONUTF8: "1"
11
+
12
+ jobs:
13
+ lint:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - name: Install uv
19
+ uses: astral-sh/setup-uv@v5
20
+ with:
21
+ python-version: "3.12"
22
+ enable-cache: true
23
+
24
+ - name: Verify lockfile is in sync
25
+ run: uv lock --check
26
+
27
+ - name: Install dependencies
28
+ run: uv sync --extra dev --frozen
29
+
30
+ - name: Lint with ruff (blocking)
31
+ run: uv run ruff check deadpush/ tests/ scripts/
32
+
33
+ test:
34
+ # Unit + integration suite. Runs on Linux across supported Python versions
35
+ # and on macOS (the primary hardened-mode target). The integration test in
36
+ # tests/test_integration_lifecycle.py drives the installed CLI and executes
37
+ # the real generated git hooks — the closest deterministic proxy to a user's
38
+ # first run, without the flakiness of a background daemon in CI.
39
+ needs: [lint]
40
+ strategy:
41
+ fail-fast: false
42
+ matrix:
43
+ include:
44
+ - os: ubuntu-latest
45
+ python-version: "3.11"
46
+ - os: ubuntu-latest
47
+ python-version: "3.12"
48
+ - os: ubuntu-latest
49
+ python-version: "3.13"
50
+ - os: macos-latest
51
+ python-version: "3.12"
52
+ runs-on: ${{ matrix.os }}
53
+ steps:
54
+ - uses: actions/checkout@v4
55
+
56
+ - name: Install uv
57
+ uses: astral-sh/setup-uv@v5
58
+ with:
59
+ python-version: ${{ matrix.python-version }}
60
+ enable-cache: true
61
+
62
+ - name: Install dependencies
63
+ run: uv sync --extra dev --frozen
64
+
65
+ - name: Run tests
66
+ run: uv run pytest -v
@@ -95,10 +95,9 @@ ipython_config.py
95
95
  # Pipfile.lock
96
96
 
97
97
  # UV
98
- # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
- # This is especially recommended for binary packages to ensure reproducibility, and is more
100
- # commonly ignored for libraries.
101
- uv.lock
98
+ # deadpush is a distributed application/security tool, so uv.lock IS committed
99
+ # for reproducible builds. CI enforces it (uv lock --check, uv sync --frozen).
100
+ # uv.lock
102
101
 
103
102
  # poetry
104
103
  # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
@@ -0,0 +1,10 @@
1
+ {
2
+ "servers": {
3
+ "deadpush": {
4
+ "command": "/Users/harris/Documents/personal/deadpush/.venv/bin/deadpush",
5
+ "args": [
6
+ "mcp"
7
+ ]
8
+ }
9
+ }
10
+ }
@@ -6,9 +6,9 @@ This project uses **deadpush**: an agent-native guardrail system that intercepts
6
6
 
7
7
  - Write files normally via MCP (`write_file` tool). Guardrails run automatically.
8
8
  - If blocked → read the feedback file → fix the issue → retry.
9
- - If you hit a false positive → `add_allowed_pattern` to whitelist the code.
9
+ - If you hit a false positive → `add_allowed_pattern` to whitelist the code (requires `deadpush mcp --danger`).
10
10
 
11
- ## MCP Tools (24 available)
11
+ ## MCP Tools
12
12
 
13
13
  Connect via `deadpush mcp` (stdio JSON-RPC on `2024-11-05` protocol).
14
14
 
@@ -17,34 +17,27 @@ Connect via `deadpush mcp` (stdio JSON-RPC on `2024-11-05` protocol).
17
17
  |---|---|
18
18
  | `write_file(path, content)` | Write through guardrails. Blocked files go to quarantine + feedback. |
19
19
  | `check_file(path, content)` | Preview: would the file pass? Returns violations without writing. |
20
+ | `verify_write(path, content)` | Write + run related tests. File only persists if tests pass. |
21
+ | `get_write_diff(path, content)` | Preview diff + guardrail violations before writing. |
22
+ | `retry_write(path, content)` | Submit corrected content for a previously blocked file. |
20
23
 
21
- ### Scan
24
+ ### Quarantine
22
25
  | Tool | What it does |
23
26
  |---|---|
24
- | `scan` | Full analysis summary (dead symbols, debris, test issues, etc.) |
25
- | `get_dead_symbols` | Unreachable code |
26
- | `get_debris` | AI artifacts, stale files |
27
- | `get_test_issues` | No-assertion / tautology / empty tests |
28
- | `get_stale_docs` | Docstring-param mismatches |
29
- | `get_layer_violations` | Architectural import violations |
30
- | `get_security_boundaries` | Untested security-sensitive ops |
31
- | `get_complexity_alerts` | Complexity spikes |
32
-
33
- ### Clean / Quarantine
34
- | Tool | What it does |
35
- |---|---|
36
- | `clean(mode)` | Remove dead code / debris (safe, dry_run, force) |
37
27
  | `quarantine_list(limit)` | List quarantined files |
38
- | `quarantine_restore(name)` | Restore from quarantine |
28
+ | `quarantine_restore(name)` | Restore from quarantine (danger mode) |
39
29
 
40
30
  ### Feedback
41
31
  | Tool | What it does |
42
32
  |---|---|
43
33
  | `get_feedback(limit)` | Read recent guardrail feedback |
34
+ | `get_recent_feedback(limit)` | Unacknowledged feedback only |
35
+ | `acknowledge_feedback(name)` | Mark feedback as read/addressed |
44
36
  | `get_status` | Current paths + available tools |
45
37
  | `get_safety_score` | Latest guardian score |
38
+ | `get_test_results(limit)` | Recent verify_write test output |
46
39
 
47
- ### Config (agent self-service)
40
+ ### Config (agent self-service — danger mode for softening actions)
48
41
  | Tool | What it does |
49
42
  |---|---|
50
43
  | `get_runtime_config` | View all current rules |
@@ -53,15 +46,12 @@ Connect via `deadpush mcp` (stdio JSON-RPC on `2024-11-05` protocol).
53
46
  | `ignore_path(path)` | Skip a file entirely |
54
47
  | `set_guardrail_level(category, level)` | Set severity: `off`, `warn`, or `block` |
55
48
  | `reset_runtime_config` | Clear all overrides to defaults |
49
+ | `allow_sensitive_write(path)` | Opt in to writing a sensitive config file |
50
+ | `learn_false_positive(category, pattern, reason)` | Teach deadpush to suppress a false positive |
51
+ | `adjudicate_finding(...)` | Structured adjudication rubric for a finding |
56
52
 
57
53
  All tools return `{"success": bool, "data": ..., "summary": "..."}`.
58
54
 
59
- ### Diff / Preview
60
- | Tool | What it does |
61
- |---|---|
62
- | `get_write_diff(path, content)` | Preview diff + guardrail violations before writing |
63
- | `allow_sensitive_write(path)` | Opt in to writing a sensitive config file (CI/CD, Docker, etc.) |
64
-
65
55
  ## Guardrail Categories
66
56
 
67
57
  | Category | Default level | What it catches |
@@ -72,7 +62,7 @@ All tools return `{"success": bool, "data": ..., "summary": "..."}`.
72
62
  | `layer` | `block` | Imports violating architecture layer rules |
73
63
  | `sensitive` | `block` | Writes to CI/CD, deployment, Docker, and other sensitive config files |
74
64
  | `destructive` | `warn` | Near-empty rewrites of substantial files, >50% line reduction |
75
- | `debris` | `warn` | TODO stubs, FIXME markers, bare `pass` statements |
65
+ | `debris` | `warn` | LLM context files, scratchpads, hardcoded secrets in debris scan |
76
66
  | `dependency` | `warn` | Typosquat packages, suspicious package names in dep files |
77
67
 
78
68
  ## Self-Correction Flow
@@ -95,8 +85,6 @@ Then retry — it will pass.
95
85
  Persisted in `.deadpush/rules.json`. Survives server restarts.
96
86
  Modify via MCP tools above. Do NOT edit the file directly.
97
87
 
98
- Use `set_guardrail_level("prompt_injection", "warn")` to downgrade a category to warning-only (doesn't block, just reports).
99
-
100
88
  ## What NOT To Do
101
89
 
102
90
  - Do NOT bypass guardrails by writing directly to the filesystem.
@@ -0,0 +1,293 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.2.3] - 2026-07-04
11
+
12
+ ### Fixed
13
+ - **`protect`/`guard` now default to soft mode (critical first-run fix)**: the
14
+ default was hardened (`use_hardened = not soft`), so the documented one-liner
15
+ `pip install deadpush && deadpush protect --daemon` silently triggered a `sudo`
16
+ password prompt — and on a normal (non-editable) pip install it then failed
17
+ outright. Soft (same-UID) mode is now the default; hardened mode is opt-in via
18
+ `--hardened`. This matches the README and `SECURITY.md`.
19
+ - **Hardened mode works from a pip/wheel install**: `_ensure_hardened_venv`
20
+ assumed a source checkout and looked for a `pyproject.toml` above the package,
21
+ raising `deadpush source not found at .../site-packages` for every
22
+ pip-installed user who ran `--hardened`. It now installs the matching version
23
+ from PyPI (`deadpush==<version>`) when there is no local source tree, and still
24
+ installs the working tree for dev checkouts.
25
+
26
+ ## [0.2.2] - 2026-07-04
27
+
28
+ First public release on PyPI (`0.2.1` was burned by PyPI's deleted-filename
29
+ reuse rule and skipped). The project stays on a 0.2.x beta line until the
30
+ production-readiness gates (blocking lint, lockfiled reproducible builds, and
31
+ green macOS + Linux CI including the lifecycle integration test) are met.
32
+
33
+ ### Security (hardened mode — Tier 0)
34
+
35
+ A threat-model audit (adversarial agent running as the same UID) found five ways
36
+ a same-UID agent could defeat *hardened* mode even though the daemon and hook
37
+ files were protected. These close them so hardened mode is a real boundary, not
38
+ just defense-in-depth. Covered by `tests/test_tier0_hardening.py` and validated
39
+ end-to-end by `scripts/hardened_qa.sh`.
40
+
41
+ - **Hardened hooks now execute the root-owned interpreter (H2)**: git hooks used
42
+ the *user's* interpreter and user-writable `deadpush` package, so a same-UID
43
+ agent could no-op the guardrails by editing the package — defeating even a
44
+ root-immutable hook file. Hardened hooks now run `/var/db/deadpush/venv/bin/
45
+ python` (owned by `_deadpush`, not user-writable). The guardian's auto-repair
46
+ re-pins this interpreter and re-locks `schg` instead of downgrading.
47
+ - **Guardrail policy is now read from a root-owned dir (H1)**: enforcement read
48
+ `rules.json` / `learned_patterns.json` from the in-repo `.deadpush/` directory,
49
+ which the agent can write (it could set every category to `off` or allowlist
50
+ `.*`). Hardened installs now read/write policy from a root/`_deadpush`-owned
51
+ tree (`/var/db/deadpush/policy/<repo-id>/`) that the user can traverse and read
52
+ but not modify. Soft mode is unchanged (in-repo, deterrence only).
53
+ - **Control server enforces a bearer token on mutations (H3)**: the localhost
54
+ control server shipped with auth wired up but disabled (no token), so any
55
+ local process — including the agent — could reset the allowlist or restore
56
+ quarantined files. A random token is now generated per guardian and stored
57
+ `0600` (root-owned in hardened mode, so the agent can't read it); state-changing
58
+ `POST`s require it (with a constant-time compare), while read-only `GET`s stay
59
+ open so the human dashboard still works.
60
+ - **Quarantine restore is confined to the repo tree (H4)**: `restore()` moved a
61
+ file to the absolute "Original path" read from an agent-writable `.reason`
62
+ file, letting a crafted `.reason` make the guardian (running as `_deadpush`)
63
+ write anywhere — a confused-deputy write. The destination is now resolved and
64
+ required to stay within the repo, else the restore is refused.
65
+ - **Fail-closed can no longer be cleared by the agent (H5)**: the hooks' fail-
66
+ closed decision keyed only off the agent-deletable in-repo `.deadpush/installed`
67
+ marker. Hardened setup now also writes a root-owned marker under
68
+ `/var/db/deadpush/policy/<repo-id>/`, and the hook prelude honors it, so
69
+ fail-closed holds even if the in-repo marker is deleted. (Combined with H2 the
70
+ root-owned interpreter is itself un-deletable by the agent.)
71
+ - **Hardened state dir is now `0711` (was `0700`)**: required so the user's git
72
+ hooks can *traverse* to the root-owned interpreter and policy. The directory is
73
+ not listable and secret files (the control token) remain `0600`.
74
+
75
+ ### Added
76
+ - **`SECURITY.md` threat model**: documents the adversary (a same-UID AI agent),
77
+ the soft-vs-hardened guarantees, the inherent limits of heuristic detection, and
78
+ private vulnerability reporting.
79
+ - **Real-time move/delete handling**: the watchdog guardian only handled
80
+ `on_created`/`on_modified`, so an agent could evade the real-time layer by
81
+ moving un-scanned content into place (stage a payload in a skipped dir like
82
+ `node_modules/` then `mv` it in, or rename a benign file onto a dangerous
83
+ path/name). `on_moved` now evaluates the destination exactly like a fresh
84
+ write (dangerous content is quarantined). `on_deleted` records forensic
85
+ telemetry (log + session incident) but is deliberately non-punitive — it does
86
+ not auto-restore or dock the safety score, since the score never recovers and
87
+ a legitimate multi-file `git rm`/refactor would otherwise false-trigger
88
+ lockdown; tracked files remain recoverable from git history. Covered by
89
+ `tests/test_move_delete_events.py`.
90
+ - **Fail-closed git hooks**: once a repo is protected (a `.deadpush/installed`
91
+ marker is written), the installed hooks refuse to run when the deadpush
92
+ interpreter goes missing, instead of silently allowing the push/commit.
93
+ `DEADPUSH_STRICT=1` forces fail-closed everywhere.
94
+ - **`protect` verifies and reports failure**: `deadpush protect` now polls that
95
+ the guardian actually started, and exits non-zero with actionable guidance if
96
+ hooks, the marker, or the daemon did not come up (no more false "success").
97
+ - **`doctor` protection checks**: reports the protection marker (and whether the
98
+ pinned interpreter still exists) plus git-hook integrity.
99
+ - **Lifecycle integration test** (`tests/test_integration_lifecycle.py`): drives
100
+ the installed CLI and executes the real generated hooks; runs on macOS + Linux
101
+ in CI.
102
+ - **Hardened-mode QA harness** (`scripts/hardened_qa.sh`): a manual, root-gated
103
+ end-to-end validator for the hardened guarantees CI cannot cover — privilege
104
+ separation, an agent-unkillable daemon (and launchd/systemd respawn),
105
+ root-immutable `schg`/`+i` hooks, repo + `$HOME`-traverse ACLs, real-time
106
+ quarantine, `core.hooksPath` self-heal, and a verified clean teardown. It runs
107
+ in a throwaway repo, escalates only via `sudo`, and refuses to run when a
108
+ `_deadpush` account already exists so it can't disturb a real install.
109
+ - **`core.hooksPath` hijack detection + self-heal**: `git config core.hooksPath
110
+ /dev/null` (or any dir other than `.git/hooks`) silently disabled every hook
111
+ without tripping the checksum/immutability checks. `verify_hooks_installed` now
112
+ flags it, `repair_deadpush_hooks` restores it (repo-local only, overriding a
113
+ global hijack if needed), the running guardian auto-repairs it every cycle, and
114
+ `doctor` reports it. Covered by `tests/test_hookspath_hijack.py`.
115
+ - **Root-immutable hooks in hardened mode**: hardened installs now lock the git
116
+ hooks ROOT-immutable (`sudo chflags schg` on macOS, `sudo chattr +i` on Linux)
117
+ instead of only user-immutable (`uchg`). A same-UID agent can no longer clear
118
+ the flag to delete or rewrite a hook — only root can. Soft mode keeps the
119
+ owner-clearable `uchg`. `_is_immutable` now recognises `schg` (so the guardian
120
+ never loops trying to "repair" a locked hook it cannot rewrite), `uninstall`
121
+ clears it via sudo (with an automatic escalation fallback), and if `schg` can't
122
+ be set it fails safe to `uchg` with a loud warning. Covered by
123
+ `tests/test_root_immutable.py`.
124
+
125
+ ### Fixed
126
+ - **Pre-push new-branch range can no longer be poisoned (D4)**: for a brand-new
127
+ branch (remote side all-zeros) the hook derived the scan boundary from
128
+ `git rev-list … --not --remotes`, trusting local `refs/remotes/*`. Those refs are
129
+ writable by a same-UID agent, so forging `refs/remotes/x/y` at the payload commit
130
+ made git treat the dangerous content as "already on the remote" and shrank the
131
+ scanned diff to exclude it — while `git push` still shipped those commits. New
132
+ refs now always scan the entire pushed tree (`empty-tree..local_sha`), which
133
+ cannot be poisoned by local refs; existing-branch updates keep using the
134
+ trustworthy `remote_sha` git supplies from the real push negotiation. Covered by
135
+ `tests/test_prepush_newbranch.py`
136
+ (`test_new_branch_push_cannot_be_poisoned_by_fake_remote_ref`).
137
+ - **Uninstall leaves the repo pristine**: cleanup only removed an empty
138
+ `.guardian`, leaving behind empty `.deadpush` (e.g. `.deadpush/feedback/`) and
139
+ `.deadpush-quarantine` directories. It now prunes deadpush's own now-empty
140
+ bookkeeping dirs — but strictly non-destructively: a directory is removed only
141
+ when it has no remaining contents, so real feedback data, config, and
142
+ quarantined files are always preserved. Covered by
143
+ `tests/test_integration_lifecycle.py`
144
+ (`test_uninstall_leaves_repo_pristine`, `test_uninstall_preserves_user_data`).
145
+ - **Cross-platform uninstall**: `deadpush uninstall` invoked `launchctl`
146
+ unconditionally, which raised `FileNotFoundError` on Linux and made the command
147
+ exit non-zero. It now unloads/removes the correct service per platform (launchd
148
+ plist on macOS, systemd unit on Linux — the systemd unit was previously never
149
+ removed) and treats a missing/inactive service manager as a no-op.
150
+ - **Hardened-mode teardown parity on Linux**: hardened setup already supported
151
+ Linux (`useradd`/`groupadd`/`setfacl`), but uninstall only ran macOS
152
+ `dscl`/`chmod -N`, so it crashed on Linux and orphaned the `_deadpush`
153
+ account/ACLs. Added `teardown_hardened_environment`, a platform-aware,
154
+ best-effort reversal that revokes every `_deadpush` ACL (repo tree,
155
+ `.guardian`, and parent traverse dirs — a shared walk with setup so the two
156
+ can't drift) and deletes the account (macOS `dscl`, Linux `userdel`/`groupdel`),
157
+ revoking ACLs before removing the account. Covered by
158
+ `tests/test_hardened.py::TestTeardownHardenedEnvironment`.
159
+
160
+ ### Changed
161
+ - **Wider git-hook file coverage**: the pre-commit/pre-push/post-commit hooks
162
+ gated content scanning on a small extension allowlist (~18 types), so an agent
163
+ could commit dangerous content in files the hooks never read — extensionless
164
+ and config files such as `Dockerfile`, `Makefile`, `.env`, `.npmrc`, shell
165
+ rc-files, and LLM-context files like `.cursorrules` / `.claude_instructions`
166
+ (the watchdog daemon already scanned these; the hooks did not). A shared
167
+ `is_enforceable_path()` now mirrors the daemon's coverage: a much wider set of
168
+ code/config/infra/text extensions plus sensitive-by-name and `.env*` /
169
+ `Dockerfile*` files, while binary formats (images, archives, compiled objects,
170
+ keystores) are always skipped. Covered by `tests/test_file_coverage.py`.
171
+ - **Robust hook/daemon entrypoint**: generated hooks, the launchd/systemd units,
172
+ and the shadow respawn command now launch via `deadpush_bootstrap` instead of
173
+ `-m deadpush.cli`, so they work in editable installs where macOS hides the
174
+ `.pth` file (previously the guardian and hooks failed to import deadpush from a
175
+ non-source working directory).
176
+ - **Single source of truth for version**: `pyproject.toml` derives the version
177
+ from `deadpush/__init__.py` (hatchling dynamic version); metadata and
178
+ `deadpush.__version__` can no longer drift.
179
+
180
+ ### CI / tooling
181
+ - Ruff is now a **blocking** gate; the codebase is lint-clean.
182
+ - `uv.lock` is committed and CI runs `uv lock --check` + `uv sync --frozen` for
183
+ reproducible builds.
184
+ - CI test matrix runs on **macOS and Linux** across supported Python versions.
185
+
186
+ ### Fixed
187
+ - **launchctl bootstrap bug**: `protect` no longer marks the guardian as
188
+ bootstrapped when `launchctl bootstrap` fails; it now falls back to a direct
189
+ daemon launch and verifies the result.
190
+ - `uninstall` now also removes the deadpush git hooks and the protection marker,
191
+ so a full uninstall truly leaves nothing behind.
192
+ - **Pre-push scanning of new branches**: pushing a brand-new branch (remote side
193
+ all-zeros) previously diffed the working tree against the tip and scanned
194
+ nothing, letting a first push bypass content enforcement. New commits are now
195
+ resolved against the boundary already on the remote (or the whole tree when
196
+ there is no shared history) and scanned. Covered by `tests/test_prepush_newbranch.py`.
197
+ - **Test isolation / macOS notification spam**: the autostart unit tests wrote
198
+ real plists into `~/Library/LaunchAgents` (path derived from `$HOME`, not the
199
+ temp repo), triggering a macOS "app can run in the background" notification per
200
+ run and littering hundreds of stale plists. They now use the isolated
201
+ `hardened_env` fixture.
202
+
203
+ ### Packaging
204
+ - **Homebrew formula rewrite** (`Formula/deadpush.rb`): switched from a fragile
205
+ `pip3 install --prefix` to `virtualenv_install_with_resources` with runtime
206
+ dependencies pinned to the `uv.lock` versions (`click`, `pathspec`, `watchdog`),
207
+ a Homebrew-audit-clean `desc`, and a `--version` test. Passes `brew style`.
208
+ A release helper (`scripts/brew_release.sh`) fills the source `sha256` and runs
209
+ audit/install/test. Note: `brew install` requires the repo to be **public** with
210
+ the release tag pushed; the `sha256` is a placeholder until then.
211
+
212
+ ---
213
+
214
+ ## [1.0.0] - unreleased
215
+
216
+ _Historical notes previously drafted under a 1.0.0 heading; not yet released._
217
+
218
+ ### Added
219
+ - **Log rotation**: RotatingFileHandler (10MB × 5 files) prevents unbounded log growth
220
+ - **Safety score JSON persistence**: Structured `safety_score.json` in state dir, auto-saved on every incident, read by MCP server
221
+ - **MCP Bearer token authentication**: Optional token auth for GuardianControlServer endpoints
222
+ - **Health endpoint**: `GET /health` on GuardianControlServer for launchd/systemd health checks
223
+ - **CLI: doctor**: Comprehensive health check (`deadpush doctor`)
224
+ - **CLI: uninstall**: Complete removal including hardened mode cleanup (`deadpush uninstall`)
225
+ - **CLI: init**: Guided first-time setup (`deadpush init --mode default|hardened --daemon`)
226
+ - **Hardened mode**: Full privilege separation via dedicated `_deadpush` user/group
227
+ - Dedicated `_deadpush` group (not wheel) — eliminates root escalation risk
228
+ - ACLs on repo (read) + `.guardian/` (write), hooks write access removed
229
+ - LaunchDaemon with `UserName=_deadpush`, state in `/var/db/deadpush/`
230
+ - `setup_hardened_environment()` one-time sudo setup
231
+ - **Lock/PID improvements**: Holder PID stored in lock file, start-time verification, `--force` cleanup
232
+ - **Daemon fork safety**: Handler and control server created post-fork, FD closure
233
+ - **Shadow process hardening**: Health checks via `ps`, exponential backoff, PID coordination
234
+ - **Lock manager**: Holder PID in lock file, `force_cleanup()`, `--force` CLI flag
235
+ - **Test scripts**: `scripts/soak_test.py` (multi-repo), `scripts/stress_test.py` (high-concurrency)
236
+ - **CI/CD**: GitHub Actions workflow for building wheels, publishing to PyPI, GitHub Releases
237
+ - **Homebrew formula**: Template formula for `brew install deadpush`
238
+
239
+ ### Changed
240
+ - **Per-repo hardened flag**: Replaced global `_hardened_enabled` with explicit `hardened` parameter on all functions
241
+ - **Safety score storage**: Migrated from log parsing to structured JSON (`safety_score.json`)
242
+ - **MCP safety score tool**: Now reads structured JSON instead of log parsing
243
+ - **Lock manager**: Added holder PID tracking, force cleanup, `get_holder_pid()`
244
+ - **Daemon fork safety**: `GuardianHandler` and `GuardianControlServer` created post-fork, all inherited FDs closed
245
+ - **Shadow process**: Health checks via `ps`, exponential backoff (3s→60s), max 10 failures, PID file coordination
246
+ - **PID reuse protection**: Start time verification via `ps -o etimes=` + command line check
247
+ - **TOCTOU lock fix**: Holder PID stored, `--force` cleanup flag
248
+
249
+ ### Security
250
+ - Removed `_deadpush` from wheel group — eliminated root escalation via sudoers
251
+ - Removed hook write ACL for `_deadpush` — guardian no longer modifies `.git/hooks/`
252
+ - Dedicated `_deadpush` group with GID 499
253
+
254
+ ### Fixed
255
+ - PID reuse false positive in `DaemonManager.is_running()`
256
+ - TOCTOU race in lock acquisition
257
+ - Fork crash from thread-before-fork in daemon mode
258
+ - Duplicate `_require_auth` method in control handler
259
+
260
+ ---
261
+
262
+ ## [0.2.1] - 2026-06-20
263
+
264
+ ### Added
265
+ - Multi-agent Safety Score with burst detection
266
+ - Session tracking (events, recent files, duration)
267
+ - Per-repo scoped state files (PID, lock, port, suspend, plist)
268
+ - Launchd/systemd autostart config generation
269
+ - Shadow process for guardian respawn
270
+ - MCP server with stdio transport
271
+ - Agent-as-Adjudicator feedback loop
272
+ - Debris detection (LLM context files, vibe scratchpads, secrets, duplicates)
273
+ - Layer violation detection (architecture import rules)
274
+ - Smart ignore file auto-merge
275
+
276
+ ### Changed
277
+ - Safety score now tracks multi-agent bursts
278
+ - Path-aware guardrail lowering for test/mock files
279
+ - Learned false positive patterns persisted to `.deadpush/learned_patterns.json`
280
+
281
+ ---
282
+
283
+ ## [0.1.0] - 2026-06-10
284
+
285
+ ### Added
286
+ - Initial release
287
+ - Real-time filesystem guardian with watchdog
288
+ - 7-category guardrail pipeline (security, secrets, prompt injection, debris, layers, destructive, sensitive config)
289
+ - Call graph dead code analysis with 8-factor scoring
290
+ - Git pre-push hook installation
291
+ - Quarantine system (safe restore via git)
292
+ - Static call graph resolution
293
+ - Basic MCP server with stdio transport
@@ -0,0 +1,59 @@
1
+ # Contributing
2
+
3
+ Thanks for your interest in deadpush.
4
+
5
+ ## Getting Started
6
+
7
+ ```bash
8
+ git clone https://github.com/harris-ahmad/deadpush
9
+ cd deadpush
10
+ ./scripts/dev_install.sh
11
+ source .venv/bin/activate
12
+ ```
13
+
14
+ Or manually:
15
+
16
+ ```bash
17
+ python -m venv .venv
18
+ source .venv/bin/activate
19
+ pip install -e ".[dev,rich]"
20
+ ```
21
+
22
+ ### macOS: editable install silently broken?
23
+
24
+ On macOS, Python 3.12+ **skips `.pth` files marked hidden** (`UF_HIDDEN`). Files under `~/Documents` (and other iCloud-synced folders) often get this flag, so `pip install -e .` succeeds but `deadpush` only works when your cwd is the repo — from any other directory you get `ModuleNotFoundError`.
25
+
26
+ Fix:
27
+
28
+ ```bash
29
+ ./scripts/dev_install.sh
30
+ # or, if already installed:
31
+ chflags -R nohidden .venv
32
+ ```
33
+
34
+ Do **not** run plain `pip install deadpush` in the dev venv; use `pip install -e .` so the editable hook points at your source tree.
35
+
36
+ ## Running Tests
37
+
38
+ ```bash
39
+ pytest
40
+ ```
41
+
42
+ ## Code Style
43
+
44
+ We use ruff. Run before committing:
45
+
46
+ ```bash
47
+ ruff check .
48
+ ```
49
+
50
+ ## Pull Requests
51
+
52
+ - Keep changes focused. One PR = one concern.
53
+ - Add tests for new functionality.
54
+ - Run the full test suite before submitting.
55
+ - Update CHANGELOG.md if the change is user-facing.
56
+
57
+ ## Questions?
58
+
59
+ Open an issue at https://github.com/harris-ahmad/deadpush/issues
@@ -0,0 +1,58 @@
1
+ class Deadpush < Formula
2
+ include Language::Python::Virtualenv
3
+
4
+ desc "Real-time AI agent guardian with git hooks and MCP write interception"
5
+ homepage "https://github.com/harris-ahmad/deadpush"
6
+ url "https://github.com/harris-ahmad/deadpush/archive/refs/tags/v0.2.1.tar.gz"
7
+ # RELEASE STEP: placeholder sha256 (all-zeros). It can only be filled once the
8
+ # repo is PUBLIC and the tag is pushed. `brew install` will fail loudly with a
9
+ # checksum mismatch until then. Run scripts/brew_release.sh to fetch the tarball,
10
+ # compute the real sha256, write it here, and verify with brew audit/install/test.
11
+ sha256 "0000000000000000000000000000000000000000000000000000000000000000"
12
+ license "MIT"
13
+
14
+ depends_on "python@3.14"
15
+
16
+ # Runtime dependencies, pinned to the exact versions in uv.lock (the versions
17
+ # the test suite runs against). Homebrew installs these with --no-deps, so every
18
+ # runtime dependency must be listed explicitly. The PEP 517 build backend
19
+ # (hatchling) is resolved automatically via pip build isolation and is not vendored.
20
+ resource "click" do
21
+ url "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz"
22
+ sha256 "918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96"
23
+ end
24
+
25
+ resource "pathspec" do
26
+ url "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz"
27
+ sha256 "17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a"
28
+ end
29
+
30
+ resource "watchdog" do
31
+ url "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz"
32
+ sha256 "9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"
33
+ end
34
+
35
+ def install
36
+ virtualenv_install_with_resources
37
+ end
38
+
39
+ def caveats
40
+ <<~EOS
41
+ Get started by protecting a repository:
42
+ cd your-repo && deadpush init
43
+
44
+ For hardened mode (privilege separation via a dedicated _deadpush user):
45
+ sudo deadpush init --mode hardened
46
+
47
+ Run the guardian as a background daemon:
48
+ deadpush protect --daemon
49
+
50
+ Check status:
51
+ deadpush status
52
+ EOS
53
+ end
54
+
55
+ test do
56
+ assert_match version.to_s, shell_output("#{bin}/deadpush --version")
57
+ end
58
+ end