i-insist 0.2.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.
@@ -0,0 +1,189 @@
1
+ name: Publish release
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ concurrency:
10
+ group: publish-release-${{ github.ref }}
11
+ cancel-in-progress: false
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ jobs:
17
+ quality:
18
+ name: Run quality gates
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - name: Check out source
22
+ uses: actions/checkout@v5
23
+
24
+ - name: Install uv
25
+ uses: astral-sh/setup-uv@v7
26
+
27
+ - name: Set up Python
28
+ uses: actions/setup-python@v6
29
+ with:
30
+ python-version-file: .python-version
31
+
32
+ - name: Install locked dependencies
33
+ run: uv sync --locked --all-groups
34
+
35
+ - name: Run all quality gates
36
+ run: |
37
+ uv run pytest
38
+ uv run ruff check .
39
+ uv run ruff format --check .
40
+ uv build
41
+
42
+ prepare:
43
+ name: Prepare release version
44
+ needs: quality
45
+ if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
46
+ runs-on: ubuntu-latest
47
+ permissions:
48
+ contents: write
49
+ outputs:
50
+ publish: ${{ steps.version.outputs.state != 'stale' }}
51
+ version: ${{ steps.version.outputs.version }}
52
+ sha: ${{ steps.commit.outputs.sha }}
53
+ steps:
54
+ - name: Check out current main
55
+ uses: actions/checkout@v5
56
+ with:
57
+ ref: main
58
+ fetch-depth: 2
59
+
60
+ - name: Install uv
61
+ uses: astral-sh/setup-uv@v7
62
+
63
+ - name: Set up Python
64
+ uses: actions/setup-python@v6
65
+ with:
66
+ python-version-file: .python-version
67
+
68
+ - name: Prepare release version
69
+ id: version
70
+ env:
71
+ SOURCE_SHA: ${{ github.sha }}
72
+ run: python scripts/prepare_release.py --source "$SOURCE_SHA"
73
+
74
+ - name: Verify release version
75
+ if: steps.version.outputs.state != 'stale'
76
+ run: |
77
+ uv sync --locked --all-groups
78
+
79
+ - name: Commit release version
80
+ id: commit
81
+ if: steps.version.outputs.state != 'stale'
82
+ env:
83
+ RELEASE_STATE: ${{ steps.version.outputs.state }}
84
+ RELEASE_VERSION: ${{ steps.version.outputs.version }}
85
+ SOURCE_SHA: ${{ github.sha }}
86
+ run: |
87
+ if [[ "$RELEASE_STATE" == 'new' ]]; then
88
+ git config user.name 'github-actions[bot]'
89
+ git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
90
+ git add pyproject.toml uv.lock
91
+ git commit --allow-empty -m "Release $RELEASE_VERSION" -m "Source-Commit: $SOURCE_SHA"
92
+ git push origin HEAD:main
93
+ fi
94
+ echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
95
+
96
+ build:
97
+ name: Build distributions
98
+ needs: [prepare, quality]
99
+ if: needs.prepare.outputs.publish == 'true'
100
+ runs-on: ubuntu-latest
101
+ steps:
102
+ - name: Check out source
103
+ uses: actions/checkout@v5
104
+ with:
105
+ ref: ${{ needs.prepare.outputs.sha }}
106
+
107
+ - name: Install uv
108
+ uses: astral-sh/setup-uv@v7
109
+
110
+ - name: Set up Python
111
+ uses: actions/setup-python@v6
112
+ with:
113
+ python-version-file: .python-version
114
+
115
+ - name: Build package
116
+ run: uv build
117
+
118
+ - name: Validate distribution metadata
119
+ run: uvx --from twine twine check --strict dist/*
120
+
121
+ - name: Upload distributions
122
+ uses: actions/upload-artifact@v7
123
+ with:
124
+ name: dist
125
+ path: |
126
+ dist/*.tar.gz
127
+ dist/*.whl
128
+ if-no-files-found: error
129
+
130
+ publish:
131
+ name: Publish distributions
132
+ needs: [prepare, build]
133
+ runs-on: ubuntu-latest
134
+ environment: pypi
135
+ permissions:
136
+ id-token: write
137
+ contents: read
138
+ steps:
139
+ - name: Download distributions
140
+ uses: actions/download-artifact@v8
141
+ with:
142
+ name: dist
143
+ path: dist/
144
+
145
+ - name: Publish to PyPI
146
+ uses: pypa/gh-action-pypi-publish@release/v1
147
+ with:
148
+ skip-existing: true
149
+
150
+ release:
151
+ name: Create GitHub release
152
+ needs: [prepare, publish]
153
+ runs-on: ubuntu-latest
154
+ permissions:
155
+ contents: write
156
+ steps:
157
+ - name: Check out source and tags
158
+ uses: actions/checkout@v5
159
+ with:
160
+ fetch-depth: 0
161
+
162
+ - name: Download distributions
163
+ uses: actions/download-artifact@v8
164
+ with:
165
+ name: dist
166
+ path: dist/
167
+
168
+ - name: Create tag and release
169
+ env:
170
+ GH_TOKEN: ${{ github.token }}
171
+ RELEASE_SHA: ${{ needs.prepare.outputs.sha }}
172
+ VERSION: ${{ needs.prepare.outputs.version }}
173
+ run: |
174
+ tag="v${VERSION}"
175
+ if git rev-parse --verify --quiet "refs/tags/${tag}" >/dev/null; then
176
+ tag_sha="$(git rev-list -n 1 "${tag}")"
177
+ if [[ "${tag_sha}" != "${RELEASE_SHA}" ]]; then
178
+ echo "${tag} already points to ${tag_sha}, expected ${RELEASE_SHA}" >&2
179
+ exit 1
180
+ fi
181
+ fi
182
+ if gh release view "${tag}" >/dev/null 2>&1; then
183
+ echo "GitHub release ${tag} already exists"
184
+ exit 0
185
+ fi
186
+ gh release create "${tag}" dist/* \
187
+ --generate-notes \
188
+ --target "${RELEASE_SHA}" \
189
+ --title "${tag}"
@@ -0,0 +1,7 @@
1
+ .venv/
2
+ __pycache__/
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ dist/
6
+ *.egg-info/
7
+ .coverage
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,17 @@
1
+ # Development
2
+
3
+ Keep provider policy out of the runner. Harness-specific payload handling belongs
4
+ in adapters; checkers receive the documented neutral event.
5
+
6
+ Use `uv`. Before committing, run:
7
+
8
+ ```sh
9
+ uv run pytest
10
+ uv run ruff check .
11
+ uv run ruff format --check .
12
+ uv build
13
+ ```
14
+
15
+ Changes to configuration, approval scope, or the checker protocol need matching
16
+ behavior tests and README updates. Do not enable new guards in the development
17
+ session or migrate other plugins without including that work in the task scope.
@@ -0,0 +1,317 @@
1
+ Metadata-Version: 2.5
2
+ Name: i-insist
3
+ Version: 0.2.0
4
+ Summary: Agent tool guards with provider-owned rules and human overrides.
5
+ Project-URL: Repository, https://github.com/crypdick/i-insist
6
+ Project-URL: Issues, https://github.com/crypdick/i-insist/issues
7
+ Author: Ricardo Decal
8
+ Requires-Python: >=3.12
9
+ Description-Content-Type: text/markdown
10
+
11
+ # i-insist
12
+
13
+ Block agent tool calls using your own checks. Say **“I insist”** to override them.
14
+
15
+ Rules live in `~/.i-insist/*.toml` globally and `.i-insist/*.toml` within a
16
+ directory or repository. Each rule runs a program you own and displays your
17
+ message when that program returns `true`. Codex and Claude Code integrations use
18
+ public lifecycle hooks. Other harnesses can use the neutral JSON interface.
19
+
20
+ ## Install
21
+
22
+ Requires Python 3.12 or newer. Hook installation supports Linux and macOS.
23
+
24
+ ```sh
25
+ uv tool install 'git+https://github.com/crypdick/i-insist@main'
26
+ i-insist install codex
27
+ i-insist install claude
28
+ ```
29
+
30
+ Run the install command only for the harnesses you use. It registers
31
+ `PreToolUse`, `UserPromptSubmit`, and `SessionStart` hooks in:
32
+
33
+ - Codex: `~/.codex/hooks.json`, or `$CODEX_HOME/hooks.json`.
34
+ - Claude Code: `~/.claude/settings.json`, or `$CLAUDE_CONFIG_DIR/settings.json`.
35
+
36
+ The installer preserves other settings and hooks, keeps existing file permissions
37
+ and symlinks, and can be run repeatedly without duplicate registrations. Writes
38
+ are atomic; concurrent i-insist installers coordinate through a lock file beside
39
+ the configuration. Start a new harness session after installation and review
40
+ Codex hooks through `/hooks` when required by your settings.
41
+
42
+ Update the shared runner through uv:
43
+
44
+ ```sh
45
+ uv tool upgrade i-insist
46
+ ```
47
+
48
+ Re-run `i-insist install <harness>` when an update changes hook registrations.
49
+ There is no separately installed native plugin or second copy of the runner.
50
+
51
+ For a checkout under development:
52
+
53
+ ```sh
54
+ uv tool install --reinstall .
55
+ ```
56
+
57
+ To remove the integration, unregister hooks before uninstalling the tool:
58
+
59
+ ```sh
60
+ i-insist uninstall codex
61
+ i-insist uninstall claude
62
+ uv tool uninstall i-insist
63
+ ```
64
+
65
+ Uninstall removes only i-insist's hook handlers. It leaves provider rules,
66
+ approval caches, and unrelated configuration intact.
67
+
68
+ No rules are installed automatically. Existing guards from other plugins remain
69
+ independent until their providers migrate them.
70
+
71
+ ## Add a rule
72
+
73
+ Track these files in a repository, or put equivalent TOML under `~/.i-insist/`:
74
+
75
+ ```text
76
+ my-project/
77
+ ├── .i-insist/
78
+ │ └── protected-folders.toml
79
+ └── tools/
80
+ └── protect_folders.py
81
+ ```
82
+
83
+ ```toml
84
+ [[rules]]
85
+ id = "my-plugin/protected-folders"
86
+ checker = ["python3", "../tools/protect_folders.py"]
87
+ message = "These originals require your permission to edit."
88
+ # enabled = false
89
+ # timeout = 10
90
+ ```
91
+
92
+ `enabled` defaults to `true`. A disabled rule's checker does not run.
93
+ `timeout` is the checker's time limit in seconds; it defaults to 10 and must be
94
+ positive. Each TOML can contain multiple `[[rules]]`. IDs must be unique within
95
+ their file. Invalid configuration blocks execution rather than silently omitting
96
+ a check, including invalid fields on disabled rules.
97
+
98
+ Checker locations are unrestricted. `.i-insist/scripts/` is an optional
99
+ convention, not a requirement. The checker is an argument list executed directly,
100
+ without a shell. Use `sh` explicitly if you want a shell script. The checker runs
101
+ from its TOML file's directory, so relative script paths resolve there. The
102
+ intercepted tool's working directory is passed in the event as `cwd`.
103
+
104
+ Discovery combines:
105
+
106
+ 1. `~/.i-insist/*.toml`.
107
+ 2. `.i-insist/*.toml` in ancestors of the tool's working directory, outermost first.
108
+ 3. The same ancestry for explicit file targets, in target order. This protects
109
+ direct edits made from outside the target directory.
110
+
111
+ Directories are resolved and deduplicated; each directory's TOML files run in
112
+ filename order, with rules in declaration order. Discovery does not recursively
113
+ scan `.i-insist/` subdirectories. Paths use their physical, symlink-resolved
114
+ locations. Global and local rules accumulate; a local disabled rule does not
115
+ disable a global rule with the same ID. The first matching rule supplies the
116
+ block message, unchanged.
117
+
118
+ Shell text is opaque to discovery: `cd`, `git -C`, shell write targets, and paths
119
+ inside custom tool arguments are not inferred. Put such policies at a scope the
120
+ invocation reaches, or resolve their targets in your provider's checker.
121
+
122
+ ## Write a checker
123
+
124
+ Read one JSON object on stdin. Print exactly one JSON boolean on stdout and exit
125
+ with status 0: `true` blocks; `false` allows the next check. Write any debugging
126
+ output to stderr. Checker crashes, invalid output, missing executables, and
127
+ timeouts block with an error. On POSIX, timeout cleanup kills the checker process
128
+ group, including its children unless they detach into another session.
129
+
130
+ ```python
131
+ import json
132
+ import sys
133
+ from pathlib import Path
134
+
135
+
136
+ def should_block(event: dict) -> bool:
137
+ return any("_sources" in Path(path).parts for path in event["paths"])
138
+
139
+
140
+ print(json.dumps(should_block(json.load(sys.stdin))))
141
+ ```
142
+
143
+ This example protects **direct file edits**. It does not parse shell writes.
144
+ A runnable copy is in [examples/project](examples/project).
145
+
146
+ Checker input:
147
+
148
+ ```json
149
+ {
150
+ "kind": "file_write",
151
+ "command": null,
152
+ "cwd": "/repo",
153
+ "paths": ["/repo/_sources/original.txt"],
154
+ "harness": "claude",
155
+ "tool_name": "Write",
156
+ "tool_input": {"file_path": "_sources/original.txt", "content": "replacement"}
157
+ }
158
+ ```
159
+
160
+ | Field | Meaning |
161
+ | --- | --- |
162
+ | `kind` | `shell`, `file_write`, `file_edit`, or `other` |
163
+ | `command` | Shell text, or `null` for other tools |
164
+ | `cwd` | Absolute working directory of the intercepted operation |
165
+ | `paths` | Absolute explicit file targets; empty when targets are unknown |
166
+ | `harness` | Adapter identity, such as `codex` or `claude` |
167
+ | `tool_name` | Original tool name |
168
+ | `tool_input` | Original tool arguments, unchanged, including unknown fields |
169
+
170
+ Adapters recognize shell calls and common direct editing tools, including
171
+ `Write`, `Edit`, `MultiEdit`, `NotebookEdit`, and `apply_patch`. Patch targets
172
+ include additions, updates, deletions, and both sides of renames. Unknown tools
173
+ still reach every applicable checker as `other`; custom policies can inspect
174
+ their original names and arguments.
175
+
176
+ Providers own their TOML, checker programs, and dependencies. Installers should
177
+ update only their own files, preserve user changes such as `enabled = false`,
178
+ and remove their registrations on uninstall. Existing plugin-specific policy
179
+ configuration can stay where it is; the provider's checker reads it.
180
+
181
+ ## Protecting configuration
182
+
183
+ `i-insist install` installs `~/.i-insist/i-insist.toml`. Its checker blocks
184
+ explicit file edits under `.i-insist` and common shell mutations that name those
185
+ directories. Human approval is required for changes, including disabling this
186
+ rule. The same rule ships in [examples/config-protection.toml](examples/config-protection.toml).
187
+ Existing configuration is preserved on repeated installation.
188
+
189
+ The shell check recognizes common file commands, redirections, in-place `sed`,
190
+ and interpreter commands naming `.i-insist`. Opaque scripts, dynamically
191
+ constructed paths, and commands that change directories internally can evade
192
+ it. This remains a cooperative guard, not a filesystem sandbox.
193
+
194
+ ## Provider dependency setup
195
+
196
+ Providers can install a missing runner with `uv tool install` and then call
197
+ `i-insist ensure`. It registers all three hooks for Codex/Claude installations
198
+ found on `PATH` or through existing user configuration. Explicit disabled hooks
199
+ cause a nonzero exit. It checks user settings, Codex per-hook disable state, and
200
+ conservatively rejects disable flags in the current directory's ancestry.
201
+
202
+ Registration is not proof of live execution: Codex hook trust, managed policy,
203
+ command-line overrides, and a running session's snapshot remain harness-owned.
204
+ Restart and review `/hooks` after installation. `ensure` never creates trust
205
+ records or clears explicit disable settings. Providers must stop migration when
206
+ it fails and keep existing guards until setup succeeds.
207
+
208
+ ## Human overrides
209
+
210
+ Rules may set `overridable = false` (default: `true`). These rules still run
211
+ after human approval, including shell environment overrides. `enabled = false`
212
+ still disables a rule. Invalid configuration still blocks approved calls.
213
+
214
+ Send `I insist` on its own line, optionally followed by `.` or `!`, for example:
215
+
216
+ ```text
217
+ I insist.
218
+ Update both protected files.
219
+ ```
220
+
221
+ Matching ignores case. Quoted lines, fenced code, and indented code examples do
222
+ not grant approval. A phrase embedded in another sentence does not match.
223
+
224
+ `UserPromptSubmit` records approval for the current response. Subsequent tool
225
+ calls in that response bypass these rules, including direct file edits and custom
226
+ tools. A new user message replaces the approval with that message's decision.
227
+ Starting or resuming a session clears approval; compaction preserves it. Codex
228
+ also requires the same `turn_id`, so approval does not transfer to another turn.
229
+ The agent remains responsible for respecting the scope you described.
230
+
231
+ For a single shell tool invocation, the agent may use this explicit prefix only
232
+ after human authorization:
233
+
234
+ ```sh
235
+ HUMAN_PERMISSION_GRANTED=1 some-command
236
+ ```
237
+
238
+ This skips checks for that shell **tool call**, including any chained commands in
239
+ it; it does not persist to later calls. Only a leading assignment is recognized.
240
+ Mentions in arguments or comments and inherited/exported environment variables
241
+ do not grant approval. Approval never overrides another plugin or the harness's
242
+ own permission policy.
243
+
244
+ The approval cache stores a boolean and optional turn ID, not your prompt text,
245
+ under `$XDG_CACHE_HOME/i-insist/approvals/` (default `~/.cache/i-insist/approvals/`).
246
+ Cache keys separate harnesses and sessions. Missing or corrupt records do not
247
+ grant approval. No approval IDs, synthetic tool calls, or transcript parsing are
248
+ used. See [the approval decision](docs/decisions/001-approval.md).
249
+
250
+ ## Other harnesses
251
+
252
+ Send a normalized event to:
253
+
254
+ ```sh
255
+ i-insist check < event.json
256
+ ```
257
+
258
+ The result is `{"blocked": false, "message": null}` or
259
+ `{"blocked": true, "message": "your configured message"}`. Both normal decisions
260
+ exit 0; input/configuration/checker failures return `blocked: true` and exit 2.
261
+ The caller must honor both the decision and execution failures. This endpoint
262
+ recognizes the shell marker but does not read Codex/Claude approval state.
263
+
264
+ An adapter owns native tool normalization, message approval, and converting the
265
+ decision into its harness's blocking response. Checkers need no changes when
266
+ another harness provides the same neutral event.
267
+
268
+ Hook coverage depends on the harness. For example, Codex does not issue a fresh
269
+ `PreToolUse` for `write_stdin` or hosted web tools. If the harness never calls a
270
+ hook, this package cannot intercept that action. A missing CLI or a harness-level
271
+ hook timeout can also prevent the runner from issuing a denial. Keep providers
272
+ fast enough to fit the enclosing hook timeout (600 seconds in generated registrations).
273
+ This is a cooperative agent guard, not a sandbox: registered checkers themselves
274
+ execute code with your account's privileges.
275
+
276
+ Public hook references: [Codex](https://learn.chatgpt.com/docs/hooks) and
277
+ [Claude Code](https://code.claude.com/docs/en/hooks).
278
+
279
+ ## Develop
280
+
281
+ ```sh
282
+ uv sync --locked
283
+ uv run pytest
284
+ uv run ruff check .
285
+ uv run ruff format --check .
286
+ uv build
287
+ ```
288
+
289
+ Tests exercise installed hook commands and actual checker subprocesses using
290
+ temporary homes, projects, and approval caches. They do not enable hooks in your
291
+ active harness.
292
+
293
+ ## Publishing
294
+
295
+ `.github/workflows/publish.yml` runs tests, lint, formatting, and a package build
296
+ on pull requests. Each successful push to `main` publishes a release to PyPI
297
+ and creates a GitHub release with the wheel and source distribution attached.
298
+ Manual workflow dispatch retries a release without needing another commit.
299
+
300
+ The workflow preserves an unpublished version from `pyproject.toml`; otherwise,
301
+ it increments the latest stable PyPI patch version. Use `uv version --bump minor`
302
+ or `uv version --bump major` for an intentional version change. The release
303
+ commit updates `pyproject.toml` and `uv.lock` together. Superseded runs are
304
+ skipped, and retries reuse their release commit and already uploaded files.
305
+
306
+ Publishing uses [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/)
307
+ and the GitHub environment `pypi`, restricted to `main`. No PyPI API token is
308
+ stored in GitHub. To configure a first publication, add a pending publisher at
309
+ [PyPI account publishing](https://pypi.org/manage/account/publishing/) with:
310
+
311
+ - Project: `i-insist`
312
+ - GitHub owner: `crypdick`
313
+ - Repository: `i-insist`
314
+ - Workflow filename: `publish.yml`
315
+ - Environment: `pypi`
316
+
317
+ After the first release, install from PyPI with `uv tool install i-insist`.