deadpush 0.2.1__tar.gz → 0.2.2__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.
- deadpush-0.2.2/.cursorignore +23 -0
- deadpush-0.2.2/.github/workflows/ci.yml +66 -0
- {deadpush-0.2.1 → deadpush-0.2.2}/.gitignore +3 -4
- deadpush-0.2.2/.vscode/mcp.json +10 -0
- {deadpush-0.2.1 → deadpush-0.2.2}/AGENT.md +15 -27
- deadpush-0.2.2/CHANGELOG.md +277 -0
- deadpush-0.2.2/CONTRIBUTING.md +59 -0
- deadpush-0.2.2/Formula/deadpush.rb +58 -0
- {deadpush-0.2.1 → deadpush-0.2.2}/PKG-INFO +51 -65
- deadpush-0.2.2/README.md +182 -0
- deadpush-0.2.2/SECURITY.md +186 -0
- deadpush-0.2.2/action.yml +26 -0
- deadpush-0.2.2/deadpush/__init__.py +1 -0
- deadpush-0.2.2/deadpush/cli.py +1565 -0
- deadpush-0.2.2/deadpush/config.py +377 -0
- deadpush-0.2.2/deadpush/debris.py +165 -0
- {deadpush-0.2.1 → deadpush-0.2.2}/deadpush/deps_guard.py +1 -1
- deadpush-0.2.2/deadpush/guard.py +2785 -0
- deadpush-0.2.2/deadpush/hooks.py +901 -0
- deadpush-0.2.2/deadpush/intercept.py +729 -0
- {deadpush-0.2.1 → deadpush-0.2.2}/deadpush/layers.py +1 -2
- {deadpush-0.2.1 → deadpush-0.2.2}/deadpush/mcp_server.py +131 -214
- {deadpush-0.2.1 → deadpush-0.2.2}/deadpush/rules.py +6 -2
- {deadpush-0.2.1 → deadpush-0.2.2}/deadpush/session.py +1 -10
- deadpush-0.2.2/deadpush/types.py +47 -0
- deadpush-0.2.2/deadpush/ui.py +37 -0
- {deadpush-0.2.1 → deadpush-0.2.2}/deadpush/verifier.py +1 -2
- deadpush-0.2.2/deadpush_bootstrap.py +66 -0
- {deadpush-0.2.1 → deadpush-0.2.2}/pyproject.toml +29 -13
- deadpush-0.2.2/scripts/brew_release.sh +59 -0
- deadpush-0.2.2/scripts/deadpush +60 -0
- deadpush-0.2.2/scripts/dev_install.sh +54 -0
- deadpush-0.2.2/scripts/full_e2e_test.py +359 -0
- deadpush-0.2.2/scripts/hardened_qa.sh +378 -0
- deadpush-0.2.2/scripts/soak_test.py +180 -0
- deadpush-0.2.2/scripts/stress_test.py +123 -0
- deadpush-0.2.2/tests/test_attack_chain.py +106 -0
- deadpush-0.2.2/tests/test_bootstrap.py +44 -0
- {deadpush-0.2.1 → deadpush-0.2.2}/tests/test_deps_guard.py +0 -7
- {deadpush-0.2.1 → deadpush-0.2.2}/tests/test_exhaustive.py +55 -217
- deadpush-0.2.2/tests/test_failclosed.py +130 -0
- deadpush-0.2.2/tests/test_file_coverage.py +72 -0
- deadpush-0.2.2/tests/test_guardian_closed_loop.py +118 -0
- deadpush-0.2.2/tests/test_hardened.py +406 -0
- deadpush-0.2.2/tests/test_hooks_enforcement.py +71 -0
- deadpush-0.2.2/tests/test_hookspath_hijack.py +87 -0
- deadpush-0.2.2/tests/test_integration_lifecycle.py +172 -0
- {deadpush-0.2.1 → deadpush-0.2.2}/tests/test_intercept_guardrails.py +32 -38
- {deadpush-0.2.1 → deadpush-0.2.2}/tests/test_mcp.py +9 -23
- deadpush-0.2.2/tests/test_move_delete_events.py +116 -0
- deadpush-0.2.2/tests/test_os_hardening.py +45 -0
- deadpush-0.2.2/tests/test_prepush_newbranch.py +120 -0
- {deadpush-0.2.1 → deadpush-0.2.2}/tests/test_real_repo_e2e.py +31 -33
- deadpush-0.2.2/tests/test_root_immutable.py +186 -0
- {deadpush-0.2.1 → deadpush-0.2.2}/tests/test_rules.py +1 -2
- deadpush-0.2.2/tests/test_tier0_hardening.py +229 -0
- deadpush-0.2.2/uv.lock +559 -0
- deadpush-0.2.1/.github/workflows/ci.yml +0 -36
- deadpush-0.2.1/CHANGELOG.md +0 -19
- deadpush-0.2.1/CONTRIBUTING.md +0 -38
- deadpush-0.2.1/README.md +0 -188
- deadpush-0.2.1/action.yml +0 -34
- deadpush-0.2.1/deadpush/__init__.py +0 -1
- deadpush-0.2.1/deadpush/churn.py +0 -189
- deadpush-0.2.1/deadpush/cli.py +0 -1584
- deadpush-0.2.1/deadpush/comments.py +0 -265
- deadpush-0.2.1/deadpush/complexity.py +0 -254
- deadpush-0.2.1/deadpush/config.py +0 -284
- deadpush-0.2.1/deadpush/crawler.py +0 -133
- deadpush-0.2.1/deadpush/deadness.py +0 -477
- deadpush-0.2.1/deadpush/debris.py +0 -729
- deadpush-0.2.1/deadpush/deps.py +0 -323
- deadpush-0.2.1/deadpush/entrypoints.py +0 -193
- deadpush-0.2.1/deadpush/graph.py +0 -401
- deadpush-0.2.1/deadpush/guard.py +0 -1386
- deadpush-0.2.1/deadpush/hooks.py +0 -369
- deadpush-0.2.1/deadpush/importgraph.py +0 -122
- deadpush-0.2.1/deadpush/imports.py +0 -239
- deadpush-0.2.1/deadpush/intercept.py +0 -995
- deadpush-0.2.1/deadpush/languages/__init__.py +0 -143
- deadpush-0.2.1/deadpush/languages/base.py +0 -70
- deadpush-0.2.1/deadpush/languages/cpp.py +0 -150
- deadpush-0.2.1/deadpush/languages/go_.py +0 -177
- deadpush-0.2.1/deadpush/languages/java.py +0 -185
- deadpush-0.2.1/deadpush/languages/javascript.py +0 -202
- deadpush-0.2.1/deadpush/languages/python_.py +0 -278
- deadpush-0.2.1/deadpush/languages/rust.py +0 -147
- deadpush-0.2.1/deadpush/languages/typescript.py +0 -192
- deadpush-0.2.1/deadpush/reachability.py +0 -183
- deadpush-0.2.1/deadpush/registration.py +0 -280
- deadpush-0.2.1/deadpush/report.py +0 -113
- deadpush-0.2.1/deadpush/sarif.py +0 -123
- deadpush-0.2.1/deadpush/scorer.py +0 -151
- deadpush-0.2.1/deadpush/security.py +0 -187
- deadpush-0.2.1/deadpush/tests.py +0 -333
- deadpush-0.2.1/deadpush/ui.py +0 -156
- deadpush-0.2.1/deadpush/watch.py +0 -103
- deadpush-0.2.1/scripts/full_e2e_test.py +0 -510
- deadpush-0.2.1/tests/test_comments.py +0 -169
- deadpush-0.2.1/tests/test_tests_analyzer.py +0 -114
- {deadpush-0.2.1 → deadpush-0.2.2}/.github/workflows/python-publish.yml +0 -0
- {deadpush-0.2.1 → deadpush-0.2.2}/CODE_OF_CONDUCT.md +0 -0
- {deadpush-0.2.1 → deadpush-0.2.2}/LICENSE +0 -0
- {deadpush-0.2.1 → deadpush-0.2.2}/tests/__init__.py +0 -0
- {deadpush-0.2.1 → deadpush-0.2.2}/tests/conftest.py +0 -0
- {deadpush-0.2.1 → deadpush-0.2.2}/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
|
-
#
|
|
99
|
-
#
|
|
100
|
-
#
|
|
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.
|
|
@@ -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
|
|
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
|
-
###
|
|
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` |
|
|
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,277 @@
|
|
|
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.2] - 2026-07-04
|
|
11
|
+
|
|
12
|
+
First public release on PyPI (`0.2.1` was burned by PyPI's deleted-filename
|
|
13
|
+
reuse rule and skipped). The project stays on a 0.2.x beta line until the
|
|
14
|
+
production-readiness gates (blocking lint, lockfiled reproducible builds, and
|
|
15
|
+
green macOS + Linux CI including the lifecycle integration test) are met.
|
|
16
|
+
|
|
17
|
+
### Security (hardened mode — Tier 0)
|
|
18
|
+
|
|
19
|
+
A threat-model audit (adversarial agent running as the same UID) found five ways
|
|
20
|
+
a same-UID agent could defeat *hardened* mode even though the daemon and hook
|
|
21
|
+
files were protected. These close them so hardened mode is a real boundary, not
|
|
22
|
+
just defense-in-depth. Covered by `tests/test_tier0_hardening.py` and validated
|
|
23
|
+
end-to-end by `scripts/hardened_qa.sh`.
|
|
24
|
+
|
|
25
|
+
- **Hardened hooks now execute the root-owned interpreter (H2)**: git hooks used
|
|
26
|
+
the *user's* interpreter and user-writable `deadpush` package, so a same-UID
|
|
27
|
+
agent could no-op the guardrails by editing the package — defeating even a
|
|
28
|
+
root-immutable hook file. Hardened hooks now run `/var/db/deadpush/venv/bin/
|
|
29
|
+
python` (owned by `_deadpush`, not user-writable). The guardian's auto-repair
|
|
30
|
+
re-pins this interpreter and re-locks `schg` instead of downgrading.
|
|
31
|
+
- **Guardrail policy is now read from a root-owned dir (H1)**: enforcement read
|
|
32
|
+
`rules.json` / `learned_patterns.json` from the in-repo `.deadpush/` directory,
|
|
33
|
+
which the agent can write (it could set every category to `off` or allowlist
|
|
34
|
+
`.*`). Hardened installs now read/write policy from a root/`_deadpush`-owned
|
|
35
|
+
tree (`/var/db/deadpush/policy/<repo-id>/`) that the user can traverse and read
|
|
36
|
+
but not modify. Soft mode is unchanged (in-repo, deterrence only).
|
|
37
|
+
- **Control server enforces a bearer token on mutations (H3)**: the localhost
|
|
38
|
+
control server shipped with auth wired up but disabled (no token), so any
|
|
39
|
+
local process — including the agent — could reset the allowlist or restore
|
|
40
|
+
quarantined files. A random token is now generated per guardian and stored
|
|
41
|
+
`0600` (root-owned in hardened mode, so the agent can't read it); state-changing
|
|
42
|
+
`POST`s require it (with a constant-time compare), while read-only `GET`s stay
|
|
43
|
+
open so the human dashboard still works.
|
|
44
|
+
- **Quarantine restore is confined to the repo tree (H4)**: `restore()` moved a
|
|
45
|
+
file to the absolute "Original path" read from an agent-writable `.reason`
|
|
46
|
+
file, letting a crafted `.reason` make the guardian (running as `_deadpush`)
|
|
47
|
+
write anywhere — a confused-deputy write. The destination is now resolved and
|
|
48
|
+
required to stay within the repo, else the restore is refused.
|
|
49
|
+
- **Fail-closed can no longer be cleared by the agent (H5)**: the hooks' fail-
|
|
50
|
+
closed decision keyed only off the agent-deletable in-repo `.deadpush/installed`
|
|
51
|
+
marker. Hardened setup now also writes a root-owned marker under
|
|
52
|
+
`/var/db/deadpush/policy/<repo-id>/`, and the hook prelude honors it, so
|
|
53
|
+
fail-closed holds even if the in-repo marker is deleted. (Combined with H2 the
|
|
54
|
+
root-owned interpreter is itself un-deletable by the agent.)
|
|
55
|
+
- **Hardened state dir is now `0711` (was `0700`)**: required so the user's git
|
|
56
|
+
hooks can *traverse* to the root-owned interpreter and policy. The directory is
|
|
57
|
+
not listable and secret files (the control token) remain `0600`.
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
- **`SECURITY.md` threat model**: documents the adversary (a same-UID AI agent),
|
|
61
|
+
the soft-vs-hardened guarantees, the inherent limits of heuristic detection, and
|
|
62
|
+
private vulnerability reporting.
|
|
63
|
+
- **Real-time move/delete handling**: the watchdog guardian only handled
|
|
64
|
+
`on_created`/`on_modified`, so an agent could evade the real-time layer by
|
|
65
|
+
moving un-scanned content into place (stage a payload in a skipped dir like
|
|
66
|
+
`node_modules/` then `mv` it in, or rename a benign file onto a dangerous
|
|
67
|
+
path/name). `on_moved` now evaluates the destination exactly like a fresh
|
|
68
|
+
write (dangerous content is quarantined). `on_deleted` records forensic
|
|
69
|
+
telemetry (log + session incident) but is deliberately non-punitive — it does
|
|
70
|
+
not auto-restore or dock the safety score, since the score never recovers and
|
|
71
|
+
a legitimate multi-file `git rm`/refactor would otherwise false-trigger
|
|
72
|
+
lockdown; tracked files remain recoverable from git history. Covered by
|
|
73
|
+
`tests/test_move_delete_events.py`.
|
|
74
|
+
- **Fail-closed git hooks**: once a repo is protected (a `.deadpush/installed`
|
|
75
|
+
marker is written), the installed hooks refuse to run when the deadpush
|
|
76
|
+
interpreter goes missing, instead of silently allowing the push/commit.
|
|
77
|
+
`DEADPUSH_STRICT=1` forces fail-closed everywhere.
|
|
78
|
+
- **`protect` verifies and reports failure**: `deadpush protect` now polls that
|
|
79
|
+
the guardian actually started, and exits non-zero with actionable guidance if
|
|
80
|
+
hooks, the marker, or the daemon did not come up (no more false "success").
|
|
81
|
+
- **`doctor` protection checks**: reports the protection marker (and whether the
|
|
82
|
+
pinned interpreter still exists) plus git-hook integrity.
|
|
83
|
+
- **Lifecycle integration test** (`tests/test_integration_lifecycle.py`): drives
|
|
84
|
+
the installed CLI and executes the real generated hooks; runs on macOS + Linux
|
|
85
|
+
in CI.
|
|
86
|
+
- **Hardened-mode QA harness** (`scripts/hardened_qa.sh`): a manual, root-gated
|
|
87
|
+
end-to-end validator for the hardened guarantees CI cannot cover — privilege
|
|
88
|
+
separation, an agent-unkillable daemon (and launchd/systemd respawn),
|
|
89
|
+
root-immutable `schg`/`+i` hooks, repo + `$HOME`-traverse ACLs, real-time
|
|
90
|
+
quarantine, `core.hooksPath` self-heal, and a verified clean teardown. It runs
|
|
91
|
+
in a throwaway repo, escalates only via `sudo`, and refuses to run when a
|
|
92
|
+
`_deadpush` account already exists so it can't disturb a real install.
|
|
93
|
+
- **`core.hooksPath` hijack detection + self-heal**: `git config core.hooksPath
|
|
94
|
+
/dev/null` (or any dir other than `.git/hooks`) silently disabled every hook
|
|
95
|
+
without tripping the checksum/immutability checks. `verify_hooks_installed` now
|
|
96
|
+
flags it, `repair_deadpush_hooks` restores it (repo-local only, overriding a
|
|
97
|
+
global hijack if needed), the running guardian auto-repairs it every cycle, and
|
|
98
|
+
`doctor` reports it. Covered by `tests/test_hookspath_hijack.py`.
|
|
99
|
+
- **Root-immutable hooks in hardened mode**: hardened installs now lock the git
|
|
100
|
+
hooks ROOT-immutable (`sudo chflags schg` on macOS, `sudo chattr +i` on Linux)
|
|
101
|
+
instead of only user-immutable (`uchg`). A same-UID agent can no longer clear
|
|
102
|
+
the flag to delete or rewrite a hook — only root can. Soft mode keeps the
|
|
103
|
+
owner-clearable `uchg`. `_is_immutable` now recognises `schg` (so the guardian
|
|
104
|
+
never loops trying to "repair" a locked hook it cannot rewrite), `uninstall`
|
|
105
|
+
clears it via sudo (with an automatic escalation fallback), and if `schg` can't
|
|
106
|
+
be set it fails safe to `uchg` with a loud warning. Covered by
|
|
107
|
+
`tests/test_root_immutable.py`.
|
|
108
|
+
|
|
109
|
+
### Fixed
|
|
110
|
+
- **Pre-push new-branch range can no longer be poisoned (D4)**: for a brand-new
|
|
111
|
+
branch (remote side all-zeros) the hook derived the scan boundary from
|
|
112
|
+
`git rev-list … --not --remotes`, trusting local `refs/remotes/*`. Those refs are
|
|
113
|
+
writable by a same-UID agent, so forging `refs/remotes/x/y` at the payload commit
|
|
114
|
+
made git treat the dangerous content as "already on the remote" and shrank the
|
|
115
|
+
scanned diff to exclude it — while `git push` still shipped those commits. New
|
|
116
|
+
refs now always scan the entire pushed tree (`empty-tree..local_sha`), which
|
|
117
|
+
cannot be poisoned by local refs; existing-branch updates keep using the
|
|
118
|
+
trustworthy `remote_sha` git supplies from the real push negotiation. Covered by
|
|
119
|
+
`tests/test_prepush_newbranch.py`
|
|
120
|
+
(`test_new_branch_push_cannot_be_poisoned_by_fake_remote_ref`).
|
|
121
|
+
- **Uninstall leaves the repo pristine**: cleanup only removed an empty
|
|
122
|
+
`.guardian`, leaving behind empty `.deadpush` (e.g. `.deadpush/feedback/`) and
|
|
123
|
+
`.deadpush-quarantine` directories. It now prunes deadpush's own now-empty
|
|
124
|
+
bookkeeping dirs — but strictly non-destructively: a directory is removed only
|
|
125
|
+
when it has no remaining contents, so real feedback data, config, and
|
|
126
|
+
quarantined files are always preserved. Covered by
|
|
127
|
+
`tests/test_integration_lifecycle.py`
|
|
128
|
+
(`test_uninstall_leaves_repo_pristine`, `test_uninstall_preserves_user_data`).
|
|
129
|
+
- **Cross-platform uninstall**: `deadpush uninstall` invoked `launchctl`
|
|
130
|
+
unconditionally, which raised `FileNotFoundError` on Linux and made the command
|
|
131
|
+
exit non-zero. It now unloads/removes the correct service per platform (launchd
|
|
132
|
+
plist on macOS, systemd unit on Linux — the systemd unit was previously never
|
|
133
|
+
removed) and treats a missing/inactive service manager as a no-op.
|
|
134
|
+
- **Hardened-mode teardown parity on Linux**: hardened setup already supported
|
|
135
|
+
Linux (`useradd`/`groupadd`/`setfacl`), but uninstall only ran macOS
|
|
136
|
+
`dscl`/`chmod -N`, so it crashed on Linux and orphaned the `_deadpush`
|
|
137
|
+
account/ACLs. Added `teardown_hardened_environment`, a platform-aware,
|
|
138
|
+
best-effort reversal that revokes every `_deadpush` ACL (repo tree,
|
|
139
|
+
`.guardian`, and parent traverse dirs — a shared walk with setup so the two
|
|
140
|
+
can't drift) and deletes the account (macOS `dscl`, Linux `userdel`/`groupdel`),
|
|
141
|
+
revoking ACLs before removing the account. Covered by
|
|
142
|
+
`tests/test_hardened.py::TestTeardownHardenedEnvironment`.
|
|
143
|
+
|
|
144
|
+
### Changed
|
|
145
|
+
- **Wider git-hook file coverage**: the pre-commit/pre-push/post-commit hooks
|
|
146
|
+
gated content scanning on a small extension allowlist (~18 types), so an agent
|
|
147
|
+
could commit dangerous content in files the hooks never read — extensionless
|
|
148
|
+
and config files such as `Dockerfile`, `Makefile`, `.env`, `.npmrc`, shell
|
|
149
|
+
rc-files, and LLM-context files like `.cursorrules` / `.claude_instructions`
|
|
150
|
+
(the watchdog daemon already scanned these; the hooks did not). A shared
|
|
151
|
+
`is_enforceable_path()` now mirrors the daemon's coverage: a much wider set of
|
|
152
|
+
code/config/infra/text extensions plus sensitive-by-name and `.env*` /
|
|
153
|
+
`Dockerfile*` files, while binary formats (images, archives, compiled objects,
|
|
154
|
+
keystores) are always skipped. Covered by `tests/test_file_coverage.py`.
|
|
155
|
+
- **Robust hook/daemon entrypoint**: generated hooks, the launchd/systemd units,
|
|
156
|
+
and the shadow respawn command now launch via `deadpush_bootstrap` instead of
|
|
157
|
+
`-m deadpush.cli`, so they work in editable installs where macOS hides the
|
|
158
|
+
`.pth` file (previously the guardian and hooks failed to import deadpush from a
|
|
159
|
+
non-source working directory).
|
|
160
|
+
- **Single source of truth for version**: `pyproject.toml` derives the version
|
|
161
|
+
from `deadpush/__init__.py` (hatchling dynamic version); metadata and
|
|
162
|
+
`deadpush.__version__` can no longer drift.
|
|
163
|
+
|
|
164
|
+
### CI / tooling
|
|
165
|
+
- Ruff is now a **blocking** gate; the codebase is lint-clean.
|
|
166
|
+
- `uv.lock` is committed and CI runs `uv lock --check` + `uv sync --frozen` for
|
|
167
|
+
reproducible builds.
|
|
168
|
+
- CI test matrix runs on **macOS and Linux** across supported Python versions.
|
|
169
|
+
|
|
170
|
+
### Fixed
|
|
171
|
+
- **launchctl bootstrap bug**: `protect` no longer marks the guardian as
|
|
172
|
+
bootstrapped when `launchctl bootstrap` fails; it now falls back to a direct
|
|
173
|
+
daemon launch and verifies the result.
|
|
174
|
+
- `uninstall` now also removes the deadpush git hooks and the protection marker,
|
|
175
|
+
so a full uninstall truly leaves nothing behind.
|
|
176
|
+
- **Pre-push scanning of new branches**: pushing a brand-new branch (remote side
|
|
177
|
+
all-zeros) previously diffed the working tree against the tip and scanned
|
|
178
|
+
nothing, letting a first push bypass content enforcement. New commits are now
|
|
179
|
+
resolved against the boundary already on the remote (or the whole tree when
|
|
180
|
+
there is no shared history) and scanned. Covered by `tests/test_prepush_newbranch.py`.
|
|
181
|
+
- **Test isolation / macOS notification spam**: the autostart unit tests wrote
|
|
182
|
+
real plists into `~/Library/LaunchAgents` (path derived from `$HOME`, not the
|
|
183
|
+
temp repo), triggering a macOS "app can run in the background" notification per
|
|
184
|
+
run and littering hundreds of stale plists. They now use the isolated
|
|
185
|
+
`hardened_env` fixture.
|
|
186
|
+
|
|
187
|
+
### Packaging
|
|
188
|
+
- **Homebrew formula rewrite** (`Formula/deadpush.rb`): switched from a fragile
|
|
189
|
+
`pip3 install --prefix` to `virtualenv_install_with_resources` with runtime
|
|
190
|
+
dependencies pinned to the `uv.lock` versions (`click`, `pathspec`, `watchdog`),
|
|
191
|
+
a Homebrew-audit-clean `desc`, and a `--version` test. Passes `brew style`.
|
|
192
|
+
A release helper (`scripts/brew_release.sh`) fills the source `sha256` and runs
|
|
193
|
+
audit/install/test. Note: `brew install` requires the repo to be **public** with
|
|
194
|
+
the release tag pushed; the `sha256` is a placeholder until then.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## [1.0.0] - unreleased
|
|
199
|
+
|
|
200
|
+
_Historical notes previously drafted under a 1.0.0 heading; not yet released._
|
|
201
|
+
|
|
202
|
+
### Added
|
|
203
|
+
- **Log rotation**: RotatingFileHandler (10MB × 5 files) prevents unbounded log growth
|
|
204
|
+
- **Safety score JSON persistence**: Structured `safety_score.json` in state dir, auto-saved on every incident, read by MCP server
|
|
205
|
+
- **MCP Bearer token authentication**: Optional token auth for GuardianControlServer endpoints
|
|
206
|
+
- **Health endpoint**: `GET /health` on GuardianControlServer for launchd/systemd health checks
|
|
207
|
+
- **CLI: doctor**: Comprehensive health check (`deadpush doctor`)
|
|
208
|
+
- **CLI: uninstall**: Complete removal including hardened mode cleanup (`deadpush uninstall`)
|
|
209
|
+
- **CLI: init**: Guided first-time setup (`deadpush init --mode default|hardened --daemon`)
|
|
210
|
+
- **Hardened mode**: Full privilege separation via dedicated `_deadpush` user/group
|
|
211
|
+
- Dedicated `_deadpush` group (not wheel) — eliminates root escalation risk
|
|
212
|
+
- ACLs on repo (read) + `.guardian/` (write), hooks write access removed
|
|
213
|
+
- LaunchDaemon with `UserName=_deadpush`, state in `/var/db/deadpush/`
|
|
214
|
+
- `setup_hardened_environment()` one-time sudo setup
|
|
215
|
+
- **Lock/PID improvements**: Holder PID stored in lock file, start-time verification, `--force` cleanup
|
|
216
|
+
- **Daemon fork safety**: Handler and control server created post-fork, FD closure
|
|
217
|
+
- **Shadow process hardening**: Health checks via `ps`, exponential backoff, PID coordination
|
|
218
|
+
- **Lock manager**: Holder PID in lock file, `force_cleanup()`, `--force` CLI flag
|
|
219
|
+
- **Test scripts**: `scripts/soak_test.py` (multi-repo), `scripts/stress_test.py` (high-concurrency)
|
|
220
|
+
- **CI/CD**: GitHub Actions workflow for building wheels, publishing to PyPI, GitHub Releases
|
|
221
|
+
- **Homebrew formula**: Template formula for `brew install deadpush`
|
|
222
|
+
|
|
223
|
+
### Changed
|
|
224
|
+
- **Per-repo hardened flag**: Replaced global `_hardened_enabled` with explicit `hardened` parameter on all functions
|
|
225
|
+
- **Safety score storage**: Migrated from log parsing to structured JSON (`safety_score.json`)
|
|
226
|
+
- **MCP safety score tool**: Now reads structured JSON instead of log parsing
|
|
227
|
+
- **Lock manager**: Added holder PID tracking, force cleanup, `get_holder_pid()`
|
|
228
|
+
- **Daemon fork safety**: `GuardianHandler` and `GuardianControlServer` created post-fork, all inherited FDs closed
|
|
229
|
+
- **Shadow process**: Health checks via `ps`, exponential backoff (3s→60s), max 10 failures, PID file coordination
|
|
230
|
+
- **PID reuse protection**: Start time verification via `ps -o etimes=` + command line check
|
|
231
|
+
- **TOCTOU lock fix**: Holder PID stored, `--force` cleanup flag
|
|
232
|
+
|
|
233
|
+
### Security
|
|
234
|
+
- Removed `_deadpush` from wheel group — eliminated root escalation via sudoers
|
|
235
|
+
- Removed hook write ACL for `_deadpush` — guardian no longer modifies `.git/hooks/`
|
|
236
|
+
- Dedicated `_deadpush` group with GID 499
|
|
237
|
+
|
|
238
|
+
### Fixed
|
|
239
|
+
- PID reuse false positive in `DaemonManager.is_running()`
|
|
240
|
+
- TOCTOU race in lock acquisition
|
|
241
|
+
- Fork crash from thread-before-fork in daemon mode
|
|
242
|
+
- Duplicate `_require_auth` method in control handler
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## [0.2.1] - 2026-06-20
|
|
247
|
+
|
|
248
|
+
### Added
|
|
249
|
+
- Multi-agent Safety Score with burst detection
|
|
250
|
+
- Session tracking (events, recent files, duration)
|
|
251
|
+
- Per-repo scoped state files (PID, lock, port, suspend, plist)
|
|
252
|
+
- Launchd/systemd autostart config generation
|
|
253
|
+
- Shadow process for guardian respawn
|
|
254
|
+
- MCP server with stdio transport
|
|
255
|
+
- Agent-as-Adjudicator feedback loop
|
|
256
|
+
- Debris detection (LLM context files, vibe scratchpads, secrets, duplicates)
|
|
257
|
+
- Layer violation detection (architecture import rules)
|
|
258
|
+
- Smart ignore file auto-merge
|
|
259
|
+
|
|
260
|
+
### Changed
|
|
261
|
+
- Safety score now tracks multi-agent bursts
|
|
262
|
+
- Path-aware guardrail lowering for test/mock files
|
|
263
|
+
- Learned false positive patterns persisted to `.deadpush/learned_patterns.json`
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## [0.1.0] - 2026-06-10
|
|
268
|
+
|
|
269
|
+
### Added
|
|
270
|
+
- Initial release
|
|
271
|
+
- Real-time filesystem guardian with watchdog
|
|
272
|
+
- 7-category guardrail pipeline (security, secrets, prompt injection, debris, layers, destructive, sensitive config)
|
|
273
|
+
- Call graph dead code analysis with 8-factor scoring
|
|
274
|
+
- Git pre-push hook installation
|
|
275
|
+
- Quarantine system (safe restore via git)
|
|
276
|
+
- Static call graph resolution
|
|
277
|
+
- 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
|