reinc 0.1.0__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.
- reinc-0.1.0/.github/workflows/build.yml +54 -0
- reinc-0.1.0/.github/workflows/ci.yml +56 -0
- reinc-0.1.0/.github/workflows/publish-pypi.yml +71 -0
- reinc-0.1.0/.github/workflows/publish-testpypi.yml +58 -0
- reinc-0.1.0/.gitignore +26 -0
- reinc-0.1.0/CLAUDE.md +292 -0
- reinc-0.1.0/CONTRIBUTING.md +201 -0
- reinc-0.1.0/LICENSE +21 -0
- reinc-0.1.0/PKG-INFO +263 -0
- reinc-0.1.0/README.md +230 -0
- reinc-0.1.0/docs/INTEGRATION.md +402 -0
- reinc-0.1.0/docs/POLICY.md +314 -0
- reinc-0.1.0/docs/ROADMAP.md +57 -0
- reinc-0.1.0/docs/THREAT_MODEL.md +93 -0
- reinc-0.1.0/install.sh +59 -0
- reinc-0.1.0/packaging/README.md +47 -0
- reinc-0.1.0/packaging/homebrew/reinc.rb +41 -0
- reinc-0.1.0/pyproject.toml +61 -0
- reinc-0.1.0/src/reinc/__init__.py +1 -0
- reinc-0.1.0/src/reinc/agents.py +38 -0
- reinc-0.1.0/src/reinc/audit.py +162 -0
- reinc-0.1.0/src/reinc/behavior.py +139 -0
- reinc-0.1.0/src/reinc/cli.py +112 -0
- reinc-0.1.0/src/reinc/config.py +298 -0
- reinc-0.1.0/src/reinc/dialects/__init__.py +23 -0
- reinc-0.1.0/src/reinc/dialects/antigravity.py +276 -0
- reinc-0.1.0/src/reinc/dialects/base.py +89 -0
- reinc-0.1.0/src/reinc/dialects/claude.py +103 -0
- reinc-0.1.0/src/reinc/dialects/cline.py +64 -0
- reinc-0.1.0/src/reinc/dialects/codex.py +159 -0
- reinc-0.1.0/src/reinc/dialects/cursor.py +238 -0
- reinc-0.1.0/src/reinc/dialects/devin.py +131 -0
- reinc-0.1.0/src/reinc/engine.py +88 -0
- reinc-0.1.0/src/reinc/hook.py +165 -0
- reinc-0.1.0/src/reinc/matching.py +113 -0
- reinc-0.1.0/src/reinc/models.py +142 -0
- reinc-0.1.0/src/reinc/output.py +119 -0
- reinc-0.1.0/src/reinc/policy.py +36 -0
- reinc-0.1.0/src/reinc/register.py +291 -0
- reinc-0.1.0/src/reinc/scoring.py +164 -0
- reinc-0.1.0/src/reinc/store.py +81 -0
- reinc-0.1.0/src/reinc/tui/__init__.py +0 -0
- reinc-0.1.0/src/reinc/tui/app.py +89 -0
- reinc-0.1.0/src/reinc/tui/format.py +41 -0
- reinc-0.1.0/tests/__init__.py +0 -0
- reinc-0.1.0/tests/test_audit.py +171 -0
- reinc-0.1.0/tests/test_bash_scoring.py +112 -0
- reinc-0.1.0/tests/test_behavior.py +166 -0
- reinc-0.1.0/tests/test_cli.py +180 -0
- reinc-0.1.0/tests/test_config.py +164 -0
- reinc-0.1.0/tests/test_dialect_antigravity.py +296 -0
- reinc-0.1.0/tests/test_dialect_claude.py +103 -0
- reinc-0.1.0/tests/test_dialect_cline.py +50 -0
- reinc-0.1.0/tests/test_dialect_codex.py +196 -0
- reinc-0.1.0/tests/test_dialect_cursor.py +209 -0
- reinc-0.1.0/tests/test_dialect_devin.py +156 -0
- reinc-0.1.0/tests/test_dialect_registry.py +27 -0
- reinc-0.1.0/tests/test_e2e.py +141 -0
- reinc-0.1.0/tests/test_engine.py +237 -0
- reinc-0.1.0/tests/test_hook.py +254 -0
- reinc-0.1.0/tests/test_matching.py +133 -0
- reinc-0.1.0/tests/test_models.py +48 -0
- reinc-0.1.0/tests/test_output.py +117 -0
- reinc-0.1.0/tests/test_personal.py +38 -0
- reinc-0.1.0/tests/test_policy.py +52 -0
- reinc-0.1.0/tests/test_register.py +258 -0
- reinc-0.1.0/tests/test_scoring.py +65 -0
- reinc-0.1.0/tests/test_store.py +37 -0
- reinc-0.1.0/tests/test_tui.py +134 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
name: Build
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
name: Build sdist + wheel
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v3
|
|
21
|
+
with:
|
|
22
|
+
enable-cache: true
|
|
23
|
+
cache-dependency-glob: "pyproject.toml"
|
|
24
|
+
|
|
25
|
+
- name: Set up Python
|
|
26
|
+
run: uv python install 3.12
|
|
27
|
+
|
|
28
|
+
- name: Build package
|
|
29
|
+
run: uv build
|
|
30
|
+
|
|
31
|
+
- name: Verify the package
|
|
32
|
+
run: |
|
|
33
|
+
uv run --python 3.12 python -c "
|
|
34
|
+
import zipfile, glob, sys
|
|
35
|
+
files = glob.glob('dist/*.whl')
|
|
36
|
+
if not files:
|
|
37
|
+
print('No wheel found', file=sys.stderr)
|
|
38
|
+
sys.exit(1)
|
|
39
|
+
with zipfile.ZipFile(files[0]) as z:
|
|
40
|
+
meta = [n for n in z.namelist() if n.endswith('METADATA')]
|
|
41
|
+
if not meta:
|
|
42
|
+
print('No METADATA in wheel', file=sys.stderr)
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
content = z.read(meta[0]).decode()
|
|
45
|
+
assert 'Name: reinc' in content, 'Package name mismatch'
|
|
46
|
+
print('Wheel OK:', files[0])
|
|
47
|
+
"
|
|
48
|
+
|
|
49
|
+
- name: Upload build artifacts
|
|
50
|
+
uses: actions/upload-artifact@v4
|
|
51
|
+
with:
|
|
52
|
+
name: dist
|
|
53
|
+
path: dist/
|
|
54
|
+
retention-days: 30
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
name: Test (Python ${{ matrix.python-version }})
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
strategy:
|
|
17
|
+
fail-fast: false
|
|
18
|
+
matrix:
|
|
19
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
|
|
24
|
+
- name: Install uv
|
|
25
|
+
uses: astral-sh/setup-uv@v3
|
|
26
|
+
with:
|
|
27
|
+
enable-cache: true
|
|
28
|
+
cache-dependency-glob: "pyproject.toml"
|
|
29
|
+
|
|
30
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
31
|
+
run: uv python install ${{ matrix.python-version }}
|
|
32
|
+
|
|
33
|
+
- name: Create venv
|
|
34
|
+
run: uv venv --python ${{ matrix.python-version }}
|
|
35
|
+
|
|
36
|
+
- name: Install dependencies
|
|
37
|
+
run: uv pip install -e ".[dev]" --python ${{ matrix.python-version }}
|
|
38
|
+
|
|
39
|
+
- name: Run tests
|
|
40
|
+
run: uv run --python ${{ matrix.python-version }} pytest -q
|
|
41
|
+
|
|
42
|
+
- name: Run ruff
|
|
43
|
+
run: uv run --python ${{ matrix.python-version }} ruff check src tests
|
|
44
|
+
|
|
45
|
+
# Fast single-version job that gates merge — don't let a 3.13-only fix
|
|
46
|
+
# break 3.11. The matrix above catches it, but this gives a clean signal.
|
|
47
|
+
gate:
|
|
48
|
+
name: All tests pass
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
needs: test
|
|
51
|
+
if: always()
|
|
52
|
+
steps:
|
|
53
|
+
- name: Check matrix status
|
|
54
|
+
if: ${{ needs.test.result != 'success' }}
|
|
55
|
+
run: exit 1
|
|
56
|
+
- run: echo "All matrix jobs passed"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
name: Build sdist + wheel
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v3
|
|
21
|
+
with:
|
|
22
|
+
enable-cache: true
|
|
23
|
+
cache-dependency-glob: "pyproject.toml"
|
|
24
|
+
|
|
25
|
+
- name: Set up Python
|
|
26
|
+
run: uv python install 3.12
|
|
27
|
+
|
|
28
|
+
- name: Build package
|
|
29
|
+
run: uv build
|
|
30
|
+
|
|
31
|
+
- name: Verify package metadata
|
|
32
|
+
run: |
|
|
33
|
+
uv run --python 3.12 python -c "
|
|
34
|
+
import zipfile, glob, sys
|
|
35
|
+
files = glob.glob('dist/*.whl')
|
|
36
|
+
if not files:
|
|
37
|
+
print('No wheel found', file=sys.stderr); sys.exit(1)
|
|
38
|
+
with zipfile.ZipFile(files[0]) as z:
|
|
39
|
+
meta = [n for n in z.namelist() if n.endswith('METADATA')]
|
|
40
|
+
content = z.read(meta[0]).decode()
|
|
41
|
+
assert 'Name: reinc' in content, 'Package name mismatch'
|
|
42
|
+
print('Wheel OK:', files[0])
|
|
43
|
+
"
|
|
44
|
+
|
|
45
|
+
- name: Upload dist
|
|
46
|
+
uses: actions/upload-artifact@v4
|
|
47
|
+
with:
|
|
48
|
+
name: dist
|
|
49
|
+
path: dist/
|
|
50
|
+
|
|
51
|
+
publish:
|
|
52
|
+
name: Publish to PyPI
|
|
53
|
+
runs-on: ubuntu-latest
|
|
54
|
+
needs: build
|
|
55
|
+
environment:
|
|
56
|
+
name: pypi
|
|
57
|
+
url: https://pypi.org/p/reinc
|
|
58
|
+
permissions:
|
|
59
|
+
id-token: write # OIDC trusted publishing
|
|
60
|
+
|
|
61
|
+
steps:
|
|
62
|
+
- name: Download dist
|
|
63
|
+
uses: actions/download-artifact@v4
|
|
64
|
+
with:
|
|
65
|
+
name: dist
|
|
66
|
+
path: dist/
|
|
67
|
+
|
|
68
|
+
- name: Publish to PyPI
|
|
69
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
70
|
+
with:
|
|
71
|
+
skip-existing: true # don't fail if this version was already pushed
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
name: Publish to TestPyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
name: Build sdist + wheel
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v3
|
|
21
|
+
with:
|
|
22
|
+
enable-cache: true
|
|
23
|
+
cache-dependency-glob: "pyproject.toml"
|
|
24
|
+
|
|
25
|
+
- name: Set up Python
|
|
26
|
+
run: uv python install 3.12
|
|
27
|
+
|
|
28
|
+
- name: Build package
|
|
29
|
+
run: uv build
|
|
30
|
+
|
|
31
|
+
- name: Upload dist
|
|
32
|
+
uses: actions/upload-artifact@v4
|
|
33
|
+
with:
|
|
34
|
+
name: dist
|
|
35
|
+
path: dist/
|
|
36
|
+
|
|
37
|
+
publish:
|
|
38
|
+
name: Publish to TestPyPI
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
needs: build
|
|
41
|
+
environment:
|
|
42
|
+
name: testpypi
|
|
43
|
+
url: https://test.pypi.org/p/reinc
|
|
44
|
+
permissions:
|
|
45
|
+
id-token: write # OIDC trusted publishing
|
|
46
|
+
|
|
47
|
+
steps:
|
|
48
|
+
- name: Download dist
|
|
49
|
+
uses: actions/download-artifact@v4
|
|
50
|
+
with:
|
|
51
|
+
name: dist
|
|
52
|
+
path: dist/
|
|
53
|
+
|
|
54
|
+
- name: Publish to TestPyPI
|
|
55
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
56
|
+
with:
|
|
57
|
+
repository-url: https://test.pypi.org/legacy/
|
|
58
|
+
skip-existing: true # don't fail if this version was already pushed
|
reinc-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Reinc runtime state — dogfooders run `reinc init` themselves
|
|
2
|
+
.reinc/
|
|
3
|
+
|
|
4
|
+
# Claude
|
|
5
|
+
.claude/
|
|
6
|
+
|
|
7
|
+
# Cursor
|
|
8
|
+
.cursor/
|
|
9
|
+
|
|
10
|
+
# Devin
|
|
11
|
+
.devin/
|
|
12
|
+
|
|
13
|
+
# Python
|
|
14
|
+
__pycache__/
|
|
15
|
+
*.py[cod]
|
|
16
|
+
*.egg-info/
|
|
17
|
+
.eggs/
|
|
18
|
+
build/
|
|
19
|
+
dist/
|
|
20
|
+
.venv/
|
|
21
|
+
venv/
|
|
22
|
+
|
|
23
|
+
# Tooling
|
|
24
|
+
.pytest_cache/
|
|
25
|
+
.ruff_cache/
|
|
26
|
+
.coverage
|
reinc-0.1.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project status
|
|
6
|
+
|
|
7
|
+
The full Claude Code path is implemented and runnable end-to-end: pure decision core,
|
|
8
|
+
sqlite audit, the `reinc hook` client, four wire dialects, agent registration, and a read-only
|
|
9
|
+
logging TUI. `reinc init/hook/register/unregister/monitor/allow/deny` all exist in `cli.py`.
|
|
10
|
+
**No daemon** — the hook reads/writes sqlite directly, reconstructing the behavioral tracker
|
|
11
|
+
from recent history on each call. **Five agents are fully integrated** (dialect + `reinc register`):
|
|
12
|
+
Claude Code, Codex, Cursor, Devin (`register devin` writes a **global** hook at
|
|
13
|
+
`~/.config/devin/config.json` covering the Devin **CLI**, the **Devin Local** agent, and **Windsurf**
|
|
14
|
+
(which runs Devin Local after the Cognition acquisition) — same hook contract — so Devin Desktop
|
|
15
|
+
and Windsurf are covered when they run Devin Local. Windsurf's legacy *Cascade* hooks (EOL
|
|
16
|
+
2026-07-01) and Devin *Cloud* are out of scope), and **Antigravity** (`register antigravity` writes a
|
|
17
|
+
named-hook entry at `~/.gemini/config/hooks.json`; PreToolUse gates via `{decision:allow|deny|ask}`,
|
|
18
|
+
PostInvocation reads the transcript JSONL for post-audit since PostToolUse carries no tool output).
|
|
19
|
+
A Cline dialect exists (wire only, tested) but has no `register` path yet. 396 tests pass; ruff is clean.
|
|
20
|
+
|
|
21
|
+
Implemented modules (`src/reinc/`):
|
|
22
|
+
`models.py` (domain types) · `matching.py` (tool:glob + command tokenizing) · `scoring.py`
|
|
23
|
+
(Layer 1 base + Layer 2 sensitivity, per-command bash scoring) · `behavior.py` (Layer 3
|
|
24
|
+
stateful detectors) · `policy.py` (precedence ladder) · `engine.py` (pipeline → `Decision`) ·
|
|
25
|
+
`config.py` (config.toml + personal.toml loader) · `store.py`/`audit.py` (sync sqlite) ·
|
|
26
|
+
`hook.py` (ephemeral client, reads/writes sqlite directly) · `register.py`/`agents.py` ·
|
|
27
|
+
`dialects/` · `tui/`.
|
|
28
|
+
|
|
29
|
+
## PostToolUse safety path (done — design in `docs/INTEGRATION.md` §3.1)
|
|
30
|
+
|
|
31
|
+
`reinc hook --post` is the observe-only post-execution path. It scans tool output for
|
|
32
|
+
prompt-injection echoes and secret leaks, persists flagged results to sqlite, and feeds the
|
|
33
|
+
behavioral tracker so subsequent pre calls score higher. **Post-path policy is toml-declared**
|
|
34
|
+
(`[post]` + `[post.scanners]` sections) — detection *patterns* (regex) stay in code, but the
|
|
35
|
+
*consequences* (bonus, feed_tracker, incident_window, incident_cap, enabled) are in config.
|
|
36
|
+
|
|
37
|
+
Key components:
|
|
38
|
+
- `output.py`: `scan_output(text, post) -> (flags, bonus, reasons)` — prompt-injection + secret
|
|
39
|
+
scanners. Bonuses and feed_tracker come from `PostConfig.scanner(name)`, not hardcoded.
|
|
40
|
+
- `Dialect.parse_post()`: `claude` (covers Codex), `devin`, `cursor`, `antigravity` impls.
|
|
41
|
+
Cursor fires three post events (`postToolUse`, `afterShellExecution`, `afterFileEdit`).
|
|
42
|
+
Antigravity uses `PostInvocation` (not PostToolUse) and reads the transcript JSONL; see below.
|
|
43
|
+
- `hook.py`: `evaluate_post()` parses the post payload, scans output+error, persists to sqlite
|
|
44
|
+
via `audit.record_result()`. Silent no-op when `[post] enabled=false` or on any error.
|
|
45
|
+
- `behavior.py`: `_post_incident()` detector sums bonuses from flagged results within
|
|
46
|
+
`incident_window` (default 300s), capped at `incident_cap` (default 50).
|
|
47
|
+
- Cross-call state flows through sqlite: each pre call's hook reads recent actions + flagged
|
|
48
|
+
results and rebuilds the `BehaviorTracker` — no daemon needed.
|
|
49
|
+
- All five dialects validated against official docs (Claude Code, Codex, Devin, Cursor, Antigravity).
|
|
50
|
+
|
|
51
|
+
## Pre-hook input scanning (done)
|
|
52
|
+
|
|
53
|
+
The pre-hook scans tool **input** (file content being written, command being run) for the same
|
|
54
|
+
prompt-injection / secret patterns as the post path — **before** execution. The scanner bonus
|
|
55
|
+
is added to the risk score **before** the band lookup, so writing "IGNORE ALL PREVIOUS
|
|
56
|
+
INSTRUCTIONS" to a file pushes the score from 25 → 65 → **deny** (blocked before the file is
|
|
57
|
+
written), not just caught when the file is read back later. Flags are persisted to the
|
|
58
|
+
`actions.flags` column and shown in `reinc monitor`.
|
|
59
|
+
|
|
60
|
+
Key components:
|
|
61
|
+
- `ToolCall.content`: new field — the scannable text extracted from tool args by each dialect.
|
|
62
|
+
`None` for read-only tools (read_file, list_dir, search_files).
|
|
63
|
+
- `output.py`: `scan_input(text, post) -> (flags, bonus, reasons)` — same scanners as
|
|
64
|
+
`scan_output`, same patterns. Returns bonus that the engine adds to the score.
|
|
65
|
+
- `engine.py`: `decide()` calls `scan_input(call.content, self.post)` after the policy check
|
|
66
|
+
but before the band lookup. The bonus is `min(100, score + input_bonus)`. When
|
|
67
|
+
`[post] enabled=false`, both input and output scanning are disabled (zero bonus).
|
|
68
|
+
- `Decision.flags`: tuple of flag names (e.g. `("prompt_injection",)`), persisted to
|
|
69
|
+
`actions.flags` by `audit.record()`. `recent_with_flags()` merges pre-hook flags
|
|
70
|
+
(actions.flags) with post-audit flags (results.flags) for the TUI.
|
|
71
|
+
- Each dialect has a `_CONTENT_FIELDS` map and `_extract_content()` helper:
|
|
72
|
+
- `antigravity`: `CodeContent` (write), `NewContent` (edit), `CommandLine` (bash)
|
|
73
|
+
- `claude` (covers Codex's Bash/Read/etc): `content` (write), `new_string`/`edits[]` (edit), `command` (bash)
|
|
74
|
+
- `codex` (apply_patch): patch text from `command` field (contains V4A headers + file content)
|
|
75
|
+
- `devin`: `content` (write), `new_string` (edit), `command` (bash)
|
|
76
|
+
- `cursor`: `content` (write), `command` (bash)
|
|
77
|
+
- `cline`: `content` (write), `diff` (edit), `command` (bash)
|
|
78
|
+
- Config: reuses `[post.scanners]` bonuses (`prompt_injection=40`, `secret_leak=30`). No
|
|
79
|
+
separate `[pre]` section — the same scanner config controls both paths.
|
|
80
|
+
- Field names verified against official docs for all five agents (Claude Code, Codex, Devin,
|
|
81
|
+
Cursor, Antigravity).
|
|
82
|
+
|
|
83
|
+
## Commands
|
|
84
|
+
|
|
85
|
+
Dev setup uses a venv with the `dev` extra (pytest, pytest-asyncio, ruff):
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
uv venv && uv pip install -e ".[dev]"
|
|
89
|
+
.venv/bin/pytest -q # full suite
|
|
90
|
+
.venv/bin/pytest tests/test_engine.py -q # one module
|
|
91
|
+
.venv/bin/pytest tests/test_engine.py::test_headless_blocks_high # one test
|
|
92
|
+
.venv/bin/ruff check src tests # lint (config in pyproject.toml)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Test-first: every core feature gets a test in `tests/test_<module>.py` before moving on.
|
|
96
|
+
Behavioral/scoring tests pass explicit `ts` timestamps into `ToolCall` rather than using wall
|
|
97
|
+
clock, so the pure core stays deterministic — keep new tests doing the same.
|
|
98
|
+
|
|
99
|
+
## What Reinc is
|
|
100
|
+
|
|
101
|
+
A local-first safety layer between AI coding agents and the machine. Every agent tool call
|
|
102
|
+
(read/write/edit/delete file, bash, http) is intercepted by the agent's own pre-tool hook,
|
|
103
|
+
scored for risk (0–100), and logged. Reinc never owns the human prompt: when a call needs a
|
|
104
|
+
human, Reinc returns `ask` and the **agent shows its own native approval UI**, where the user
|
|
105
|
+
already works. All data stays local; no servers, no telemetry. Targets Claude Code, Codex,
|
|
106
|
+
Cursor, Devin (CLI + Local + Windsurf, incl. Devin Desktop on Devin Local), Antigravity, Cline, Continue.
|
|
107
|
+
|
|
108
|
+
## Architecture — the load-bearing decisions
|
|
109
|
+
|
|
110
|
+
Read `docs/INTEGRATION.md` before touching anything integration-related; it is the source of
|
|
111
|
+
truth and supersedes the README's older "Agent integrations" section.
|
|
112
|
+
|
|
113
|
+
- **Interception is via each agent's pre-tool-execution HOOK, never via an MCP server.** MCP
|
|
114
|
+
*adds* tools to an agent; it does **not** gate the agent's native tools. Registering Reinc as an
|
|
115
|
+
MCP server does not intercept anything.
|
|
116
|
+
- **Reinc delegates the human prompt back to the agent via `ask`.** A medium-risk call returns
|
|
117
|
+
`permissionDecision: "ask"`, which makes the agent surface its own approval prompt. Reinc has
|
|
118
|
+
**no interactive TUI and no approval queue** — the only TUI (`reinc monitor`) is read-only logging.
|
|
119
|
+
- **Three declarative layers, one source of truth:** hooks *enforce*, `.reinc/config.toml` (+ the
|
|
120
|
+
gitignored `.reinc/personal.toml`) *declares* policy, the TUI *logs*. There is no learned/remembered
|
|
121
|
+
state that changes decisions — config is the whole truth. (Trust memory was removed.)
|
|
122
|
+
- **The field converged on Claude Code's hook contract** (stdin JSON `{tool_name, tool_input}` →
|
|
123
|
+
stdout `permissionDecision: allow|deny|ask` or exit code 2 = block), but the convergence is
|
|
124
|
+
looser than first assumed. Code is organized around *dialects*, not per-agent. Shipped dialects:
|
|
125
|
+
**`claude`** (Claude Code), **`codex`** (`CodexDialect(ClaudeDialect)` — same wire/render + config
|
|
126
|
+
schema as claude, but its own parse: Codex writes/edits files via `apply_patch`, with the patch in
|
|
127
|
+
`tool_input.command` and the path inside the V4A patch body, so the shared claude dialect left
|
|
128
|
+
every Codex file change unmapped → silent allow → unlogged; case-insensitive lookup. **Codex has
|
|
129
|
+
no native `ask`** — `permissionDecision:"ask"` is parsed-but-unsupported and fails OPEN, so codex
|
|
130
|
+
sets `supports_ask=False` and the decision pipeline downgrades warn-tier `ask`→`deny` *before the
|
|
131
|
+
audit write*, so the monitor logs the enforced deny), **`cursor`** (`{permission}` response,
|
|
132
|
+
`failClosed` register), **`devin`** (lowercase tool names `exec`/`read`/`write`/`edit`/`glob`/`grep`,
|
|
133
|
+
`{decision:block|ask}` + `hookSpecificOutput.additionalContext`, **native `ask`** so warn-tier
|
|
134
|
+
surfaces Devin's own prompt), and **`cline`** (Claude-family variant, `{cancel:true}`, wire-only so
|
|
135
|
+
far). Codex/Cursor/Devin/Cline each needed their own tool map and/or block signal — **none** reused
|
|
136
|
+
claude fully (Codex came closest: it reuses the render, not the parse). The `devin` dialect covers
|
|
137
|
+
the Devin **CLI**, the **Devin Local** agent, and **Windsurf** (which runs Devin Local after the
|
|
138
|
+
Cognition acquisition — same hook contract), so Devin Desktop and Windsurf are covered when they
|
|
139
|
+
run Devin Local. Out of scope: Windsurf's legacy *Cascade* hooks (a distinct exit-2-only contract,
|
|
140
|
+
EOL 2026-07-01 — not building it) and Devin *Cloud* (remote sandbox).
|
|
141
|
+
**`antigravity`** is a subprocess dialect (the CLI runs hooks as shell commands with JSON
|
|
142
|
+
stdin/stdout, like Claude — NOT the in-process Python SDK). Its wire contract nests tool info
|
|
143
|
+
under `{toolCall:{name,args}}` and emits `{decision:allow|deny|ask,reason}`. Post-audit is unique:
|
|
144
|
+
Antigravity's PostToolUse stdin carries no tool output (only `stepIdx` + `error`), so reinc
|
|
145
|
+
registers on **PostInvocation** instead, which provides `transcriptPath` + `initialNumSteps`.
|
|
146
|
+
The hook reads the transcript JSONL, skips `initialNumSteps` entries, and extracts `CODE_ACTION`
|
|
147
|
+
entries (tool results) for output scanning — restoring post-incident auditing with parity to
|
|
148
|
+
Claude/Devin. Config uses a named-hook format: `{reinc: {PreToolUse: [...], PostInvocation: [...]}}`
|
|
149
|
+
at `~/.gemini/config/hooks.json`.
|
|
150
|
+
- **Single-process model:** an ephemeral `reinc hook` process per tool call that parses
|
|
151
|
+
the agent's format, opens sqlite, reads recent history (reconstructing the behavioral
|
|
152
|
+
scorer's state), scores the call, writes the audit row, and emits the agent's response
|
|
153
|
+
format. No daemon, no socket — the hook is the whole system. Cross-call behavioral
|
|
154
|
+
state (recon, exfil, cascade, post-incident) flows through sqlite: each hook call
|
|
155
|
+
reads the last ~300s of actions for that agent and rebuilds the tracker. `reinc monitor`
|
|
156
|
+
is a read-only TUI that polls sqlite every 0.5s — it doesn't score or intercept.
|
|
157
|
+
- **`reinc core` is agent-agnostic and pure** (policy engine, scorer, audit). All dialect/agent
|
|
158
|
+
knowledge lives in the hook adapters. Keep this boundary clean — it's the OSS value.
|
|
159
|
+
|
|
160
|
+
## Invariants that fall out of the agent matrix (don't violate these)
|
|
161
|
+
|
|
162
|
+
- **Fail closed.** Cursor hooks fail-OPEN by default; `reinc register` must set `failClosed: true`.
|
|
163
|
+
A crashed Reinc must never silently allow. `evaluate()` also denies on any parse/eval exception.
|
|
164
|
+
- **`ask` is the human path, not a blocking hook.** Because the agent (not Reinc) owns the prompt,
|
|
165
|
+
the hook returns immediately and there is no human-vs-hook-timeout coupling to manage. Hook
|
|
166
|
+
timeouts are fixed and short; don't reintroduce a "hook.timeout > policy.timeout" rule.
|
|
167
|
+
- **Don't assume input transform.** Antigravity can only allow/block, not rewrite tool input.
|
|
168
|
+
Any "sanitize instead of block" feature must degrade to block on that agent. (Windsurf's
|
|
169
|
+
legacy Cascade was also allow/block only, but current Windsurf runs Devin Local which supports
|
|
170
|
+
`additionalContext`.)
|
|
171
|
+
- **There is no `delete_file` tool on most agents** — deletes arrive as `Bash(rm …)`. Destructive-
|
|
172
|
+
action detection must live in bash parsing, not in a tool name.
|
|
173
|
+
- **No token/cost from hooks.** A tool-call hook sees args, not LLM token usage. Don't reintroduce
|
|
174
|
+
`token_budget`/`cost_budget` or `$cost` displays — they are not computable this way.
|
|
175
|
+
- **Guardrail, not sandbox.** Hooks run in the agent's trust domain and documented bypasses exist.
|
|
176
|
+
Never claim airtight containment. An OS-sandbox floor is an optional later layer (`THREAT_MODEL.md`).
|
|
177
|
+
- **`reinc hook` is the hot path — keep it import-light.** It runs as a fresh subprocess on every
|
|
178
|
+
tool call. `cli.py` must NOT import the TUI (`textual`) at module top; `ReincMonitor` is imported
|
|
179
|
+
lazily inside `_run_monitor`. Don't reintroduce heavy top-level imports on the hook path.
|
|
180
|
+
|
|
181
|
+
## Policy model
|
|
182
|
+
|
|
183
|
+
Project policy lives in `.reinc/config.toml` (committed to git so teammates share rules). Personal
|
|
184
|
+
overrides go in `.reinc/personal.toml` (gitignored), appended via `reinc allow <pattern>` /
|
|
185
|
+
`reinc deny <pattern>`. `.reinc/db/` is also gitignored. Precedence:
|
|
186
|
+
`never_allow` → `block` → `warn` → `allow` → risk scorer + thresholds (no trust tiers). Risk score =
|
|
187
|
+
`min(100, round((base × sensitivity_multiplier + behavior_bonus) × agent_trust_multiplier))`. The
|
|
188
|
+
agent_trust multiplier (0.7/1.0/1.3) scales the score only; it never overrides explicit rules.
|
|
189
|
+
**`bash` base is per-command** (not flat 35): read-only (`ls`, `cat`, `git status`) → 8, network
|
|
190
|
+
(`curl`, `ssh`) → 45, write/sys (`rm`, `pip install`, `git push`) → 35, unknown → 35 (fail safe).
|
|
191
|
+
See `scoring.py:_bash_score`. Rules use `tool:glob` patterns with per-tool `*` semantics.
|
|
192
|
+
**`docs/POLICY.md` is the full rule reference** (syntax, tool list, precedence, recipes); the `reinc init` default config is commented.
|
|
193
|
+
|
|
194
|
+
## Known issues / open work
|
|
195
|
+
|
|
196
|
+
- Matching is a hand-rolled glob→regex in `matching.py` (not `pathspec`), because rules need
|
|
197
|
+
per-tool `*` semantics — see the decision note in `docs/INTEGRATION.md`. Don't reintroduce a
|
|
198
|
+
glob library without revisiting that.
|
|
199
|
+
- Bash rules now also match an argv-normalized form (`normalize_command` in `matching.py`):
|
|
200
|
+
basename of the executable + collapsed whitespace, so `/bin/rm` and `rm -rf /` no longer
|
|
201
|
+
evade `bash:rm -rf /`. Residual gaps (documented, not closed — guardrail not sandbox): flag
|
|
202
|
+
reorder/bundling (`rm -fr`, `rm -r -f`), wrappers (`sudo`/`env`/`xargs rm`), and shell operators
|
|
203
|
+
(`x && rm -rf /`). A rule still needs the right pattern; normalization only canonicalizes form.
|
|
204
|
+
- Policy path rules now match the full path **or** its basename (`rule_matches` in `matching.py`),
|
|
205
|
+
mirroring the scorer — so `write_file:.env*` / `*.pem` block nested/absolute paths
|
|
206
|
+
(`/tmp/proj/.env`), not just a bare `.env`. Found via live Claude Code testing: the agent writes
|
|
207
|
+
full paths, so the old basename-blind matcher let `block` fall through to the scorer (→ `ask`
|
|
208
|
+
instead of `deny`). Slash-containing patterns stay anchored to the full path.
|
|
209
|
+
- **Abs-vs-CWD-relative normalization fixed** (`project_relative_candidate` in `matching.py`, used by
|
|
210
|
+
both `rule_matches` and `scoring._matches_path`). Found by dogfooding this repo: `[allow]` rules
|
|
211
|
+
and `[paths]` groups are written project-relative (`edit_file:src/**`, `tests/**`), but Claude
|
|
212
|
+
Code sends **absolute** paths for file tools — a slash-containing pattern only matches the full
|
|
213
|
+
path (basename fallback can't help, a basename has no slash), so every such rule silently never
|
|
214
|
+
matched and fell through to the scorer. This was the actual cause of most dogfooding friction —
|
|
215
|
+
declared `[allow]` rules for `src/**`/`tests/**` never fired. Fix: when the pattern isn't itself
|
|
216
|
+
rooted (no leading `/` or `~`) and the target is absolute, also try the path relative to `cwd`
|
|
217
|
+
(reinc's hook always runs with cwd == project root — same assumption `config.py` makes resolving
|
|
218
|
+
`db_path`). Still open: symlink/TOCTOU, case sensitivity.
|
|
219
|
+
- **Behavioral/scorer reasons weren't reaching the user.** `score_call` (`scoring.py`) always built a
|
|
220
|
+
human-readable `RiskScore.reasons` tuple (base score, sensitivity, behavior-detector text like
|
|
221
|
+
`"post-incident: recent secret_leak flagged in output"`), but `engine.py`'s `Decision.reason` only
|
|
222
|
+
ever said `"score N (band)"` — the detailed why was computed and silently discarded. Worse: when
|
|
223
|
+
the pre-hook input-scan bonus applied, the code rebuilt `RiskScore` with no `reasons=` arg,
|
|
224
|
+
**wiping** the original reasons rather than just omitting them from the message. Both fixed —
|
|
225
|
+
`Decision.reason` now appends `score.reasons`, and the input-bonus path preserves + extends them.
|
|
226
|
+
- **Post-execution output scanning had no way to avoid re-flagging the reinc repo's own source.**
|
|
227
|
+
`engine.py`/`models.py`/`README.md`/`docs/*.md` documented the injection scanner by quoting a
|
|
228
|
+
literal trigger phrase (`"IGNORE ALL PREVIOUS INSTRUCTIONS"`), which made reading reinc's own
|
|
229
|
+
code/docs during dogfooding trip the `prompt_injection` scanner and poison the next 300s of
|
|
230
|
+
session via `_post_incident` (`behavior.py`) — a self-inflicted false positive, not a real
|
|
231
|
+
attack. Fixed by rephrasing the six occurrences to describe the pattern referentially instead of
|
|
232
|
+
quoting it. Test fixtures (`tests/test_*.py`) still use the literal strings deliberately — that's
|
|
233
|
+
what they're testing — so reading the test suite itself can still self-trigger; not fixed (a
|
|
234
|
+
`[post] skip_paths` exemption was discussed but shelved as unnecessary scope for now).
|
|
235
|
+
- **`results.output`/`results.error` were persisted to sqlite in plaintext even when flagged
|
|
236
|
+
`secret_leak`.** A secret gets scanned, flagged, and then the raw value sits right next to the
|
|
237
|
+
flag forever in `.reinc/db/history.sqlite` — which `reinc monitor` reads live and isn't itself
|
|
238
|
+
encrypted. Fixed: `hook.py:_record_result` now redacts matched secret material (`redact_secrets`
|
|
239
|
+
in `output.py`, regex-based `re.sub` → `[REDACTED:secret_leak]`) before persisting, when
|
|
240
|
+
`secret_leak` is among the flags. Prompt-injection text is left intact (not sensitive, useful for
|
|
241
|
+
triage). `actions.target` (e.g. a bash command with a secret embedded as an arg) is intentionally
|
|
242
|
+
**not** redacted — Antigravity's call-id-less result correlation matches on exact
|
|
243
|
+
`(agent, kind, target)`, so altering the persisted target would break that lookup; this is a
|
|
244
|
+
known residual gap, not an oversight.
|
|
245
|
+
- Claude, Codex, Cursor, and Devin are fully integrated (dialect + register). Cline has a wire
|
|
246
|
+
dialect but no `register` (its hooks are executable scripts dropped in `.clinerules/hooks/`, not
|
|
247
|
+
a JSON config — convention not yet pinned). Codex now gates `apply_patch` writes/edits/deletes
|
|
248
|
+
(patch in `tool_input.command`, path parsed from the V4A body), not just Bash, and degrades
|
|
249
|
+
warn-tier `ask`→`deny` since Codex PreToolUse has no native ask (fail-open otherwise) — all
|
|
250
|
+
confirmed against developers.openai.com/codex/hooks; residual: multi-file patches scored on the
|
|
251
|
+
first path. Devin's
|
|
252
|
+
tool vocab + native `ask` were confirmed against live hook stdin and the CLI hooks docs (lowercase
|
|
253
|
+
`exec`/`read`/`write`/`edit`/`glob`/`grep`). Cursor is **IDE/cloud-first**: it handles the
|
|
254
|
+
per-action `beforeShellExecution`/`beforeReadFile` events plus a `matcher`-scoped `preToolUse` for
|
|
255
|
+
writes/deletes, using Cursor's **real** tool names (`Shell`/`Read`/`Write`/`Grep`/`Delete`, not
|
|
256
|
+
Claude's `Edit`/`MultiEdit`/`Search`/`List`). `ask` is only enforced on `beforeShellExecution`,
|
|
257
|
+
else it fails safe to deny — note `preToolUse` doesn't enforce `ask` **even in the IDE** (per
|
|
258
|
+
Cursor's docs). **Cursor CLI caveat:** the CLI fires only a subset of hooks (`beforeShellExecution`
|
|
259
|
+
is the lone pre-exec gate), so file writes/edits/reads can't be blocked pre-exec there — only
|
|
260
|
+
audited via `afterFileEdit`/`postToolUse`; full blocking works in IDE/cloud. The `preToolUse`
|
|
261
|
+
file-tool field names are still best-effort (verified against cursor.com/docs/hooks.md).
|
|
262
|
+
Windsurf is covered by the `devin` dialect (it runs Devin Local after the Cognition acquisition).
|
|
263
|
+
Antigravity is fully integrated (subprocess CLI hooks, not the in-process Python SDK); its
|
|
264
|
+
PostInvocation transcript-read path is verified against a live session — `initialNumSteps` is a
|
|
265
|
+
`step_index` threshold (NOT a line number; transcript step indices have gaps), and all tool-result
|
|
266
|
+
types are scanned (CODE_ACTION, VIEW_FILE, LIST_DIRECTORY, RUN_COMMAND — not just CODE_ACTION).
|
|
267
|
+
Results link to actions by (agent, kind, target) fallback since Antigravity has no `call_id`.
|
|
268
|
+
`project_root` extracts `workspacePaths[0]` from the payload so config loads from the project
|
|
269
|
+
dir, not `~/.gemini/config/` (where Antigravity runs hooks). The transcript schema is
|
|
270
|
+
undocumented and may change.
|
|
271
|
+
- **Self-hardening done; see `docs/THREAT_MODEL.md`.** `register` pins reinc's *absolute* path in
|
|
272
|
+
the injected hook command (no bare-`reinc` PATH hijack); `register`'s reinc-ownership check
|
|
273
|
+
matches on the `hook --agent <name>` marker, not the executable path, so absolute-path hooks
|
|
274
|
+
still unregister. `init` sets `.reinc/` to `0700`. The sqlite db uses WAL mode with a 5s
|
|
275
|
+
busy_timeout for concurrent hook processes.
|
|
276
|
+
- `[project.urls]` points to `cabernect/reinc` (Homepage/Issues), matching the real git remote —
|
|
277
|
+
but that GitHub repo hasn't been pushed yet (confirmed via `git ls-remote`: "Repository not
|
|
278
|
+
found"), and the `reinc` package name isn't registered on PyPI (checked 2026-07-01: name is
|
|
279
|
+
free). README's PyPI/brew/curl install lines are aspirational until both exist.
|
|
280
|
+
- `strict` mode asks on low/medium bands and denies on high (score > `block_above`).
|
|
281
|
+
`interactive` mode allows low, asks medium, denies high — `block_above` is honored in both.
|
|
282
|
+
`permissive` mode allows low/medium, asks high (no auto-deny). `headless` allows low/medium,
|
|
283
|
+
denies high (no human available for `ask`).
|
|
284
|
+
- The CLI command layer and the process boundary are now covered: `tests/test_cli.py` (click
|
|
285
|
+
`CliRunner` — stdin→wire output per dialect, exit codes, fail-closed, register/init/allow/deny)
|
|
286
|
+
and `tests/test_e2e.py` (black-box subprocess incl. a sqlite exfil round-trip). TUI quit is
|
|
287
|
+
fixed + tested: Textual binds ctrl+c to a
|
|
288
|
+
`system` binding → `action_help_quit`, which only *shows* a "press ctrl+q" hint (and ctrl+q is
|
|
289
|
+
eaten by terminals as XON/XOFF). `ReincMonitor` overrides `action_help_quit` to `self.exit()` so
|
|
290
|
+
ctrl+c actually quits; `q` is the shown fallback. NOTE: a passing Pilot `press("ctrl+c")` test
|
|
291
|
+
alone is not proof of real-terminal behavior — validate against the actual action the terminal
|
|
292
|
+
invokes. Remaining TUI gap: the live feed/table rendering isn't asserted.
|