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.
Files changed (69) hide show
  1. reinc-0.1.0/.github/workflows/build.yml +54 -0
  2. reinc-0.1.0/.github/workflows/ci.yml +56 -0
  3. reinc-0.1.0/.github/workflows/publish-pypi.yml +71 -0
  4. reinc-0.1.0/.github/workflows/publish-testpypi.yml +58 -0
  5. reinc-0.1.0/.gitignore +26 -0
  6. reinc-0.1.0/CLAUDE.md +292 -0
  7. reinc-0.1.0/CONTRIBUTING.md +201 -0
  8. reinc-0.1.0/LICENSE +21 -0
  9. reinc-0.1.0/PKG-INFO +263 -0
  10. reinc-0.1.0/README.md +230 -0
  11. reinc-0.1.0/docs/INTEGRATION.md +402 -0
  12. reinc-0.1.0/docs/POLICY.md +314 -0
  13. reinc-0.1.0/docs/ROADMAP.md +57 -0
  14. reinc-0.1.0/docs/THREAT_MODEL.md +93 -0
  15. reinc-0.1.0/install.sh +59 -0
  16. reinc-0.1.0/packaging/README.md +47 -0
  17. reinc-0.1.0/packaging/homebrew/reinc.rb +41 -0
  18. reinc-0.1.0/pyproject.toml +61 -0
  19. reinc-0.1.0/src/reinc/__init__.py +1 -0
  20. reinc-0.1.0/src/reinc/agents.py +38 -0
  21. reinc-0.1.0/src/reinc/audit.py +162 -0
  22. reinc-0.1.0/src/reinc/behavior.py +139 -0
  23. reinc-0.1.0/src/reinc/cli.py +112 -0
  24. reinc-0.1.0/src/reinc/config.py +298 -0
  25. reinc-0.1.0/src/reinc/dialects/__init__.py +23 -0
  26. reinc-0.1.0/src/reinc/dialects/antigravity.py +276 -0
  27. reinc-0.1.0/src/reinc/dialects/base.py +89 -0
  28. reinc-0.1.0/src/reinc/dialects/claude.py +103 -0
  29. reinc-0.1.0/src/reinc/dialects/cline.py +64 -0
  30. reinc-0.1.0/src/reinc/dialects/codex.py +159 -0
  31. reinc-0.1.0/src/reinc/dialects/cursor.py +238 -0
  32. reinc-0.1.0/src/reinc/dialects/devin.py +131 -0
  33. reinc-0.1.0/src/reinc/engine.py +88 -0
  34. reinc-0.1.0/src/reinc/hook.py +165 -0
  35. reinc-0.1.0/src/reinc/matching.py +113 -0
  36. reinc-0.1.0/src/reinc/models.py +142 -0
  37. reinc-0.1.0/src/reinc/output.py +119 -0
  38. reinc-0.1.0/src/reinc/policy.py +36 -0
  39. reinc-0.1.0/src/reinc/register.py +291 -0
  40. reinc-0.1.0/src/reinc/scoring.py +164 -0
  41. reinc-0.1.0/src/reinc/store.py +81 -0
  42. reinc-0.1.0/src/reinc/tui/__init__.py +0 -0
  43. reinc-0.1.0/src/reinc/tui/app.py +89 -0
  44. reinc-0.1.0/src/reinc/tui/format.py +41 -0
  45. reinc-0.1.0/tests/__init__.py +0 -0
  46. reinc-0.1.0/tests/test_audit.py +171 -0
  47. reinc-0.1.0/tests/test_bash_scoring.py +112 -0
  48. reinc-0.1.0/tests/test_behavior.py +166 -0
  49. reinc-0.1.0/tests/test_cli.py +180 -0
  50. reinc-0.1.0/tests/test_config.py +164 -0
  51. reinc-0.1.0/tests/test_dialect_antigravity.py +296 -0
  52. reinc-0.1.0/tests/test_dialect_claude.py +103 -0
  53. reinc-0.1.0/tests/test_dialect_cline.py +50 -0
  54. reinc-0.1.0/tests/test_dialect_codex.py +196 -0
  55. reinc-0.1.0/tests/test_dialect_cursor.py +209 -0
  56. reinc-0.1.0/tests/test_dialect_devin.py +156 -0
  57. reinc-0.1.0/tests/test_dialect_registry.py +27 -0
  58. reinc-0.1.0/tests/test_e2e.py +141 -0
  59. reinc-0.1.0/tests/test_engine.py +237 -0
  60. reinc-0.1.0/tests/test_hook.py +254 -0
  61. reinc-0.1.0/tests/test_matching.py +133 -0
  62. reinc-0.1.0/tests/test_models.py +48 -0
  63. reinc-0.1.0/tests/test_output.py +117 -0
  64. reinc-0.1.0/tests/test_personal.py +38 -0
  65. reinc-0.1.0/tests/test_policy.py +52 -0
  66. reinc-0.1.0/tests/test_register.py +258 -0
  67. reinc-0.1.0/tests/test_scoring.py +65 -0
  68. reinc-0.1.0/tests/test_store.py +37 -0
  69. 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.