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.
- i_insist-0.2.0/.github/workflows/publish.yml +189 -0
- i_insist-0.2.0/.gitignore +7 -0
- i_insist-0.2.0/.python-version +1 -0
- i_insist-0.2.0/AGENTS.md +17 -0
- i_insist-0.2.0/PKG-INFO +317 -0
- i_insist-0.2.0/README.md +307 -0
- i_insist-0.2.0/docs/decisions/001-approval.md +26 -0
- i_insist-0.2.0/docs/decisions/002-distribution.md +17 -0
- i_insist-0.2.0/examples/config-protection.toml +4 -0
- i_insist-0.2.0/examples/project/.i-insist/protected-folders.toml +5 -0
- i_insist-0.2.0/examples/project/tools/protect_folders.py +13 -0
- i_insist-0.2.0/pyproject.toml +36 -0
- i_insist-0.2.0/scripts/prepare_release.py +81 -0
- i_insist-0.2.0/src/i_insist/__init__.py +1 -0
- i_insist-0.2.0/src/i_insist/__main__.py +3 -0
- i_insist-0.2.0/src/i_insist/approval.py +110 -0
- i_insist-0.2.0/src/i_insist/cli.py +128 -0
- i_insist-0.2.0/src/i_insist/config-protection.toml +4 -0
- i_insist-0.2.0/src/i_insist/events.py +119 -0
- i_insist-0.2.0/src/i_insist/install.py +244 -0
- i_insist-0.2.0/src/i_insist/protect_config.py +61 -0
- i_insist-0.2.0/src/i_insist/rules.py +147 -0
- i_insist-0.2.0/tests/test_cli.py +670 -0
- i_insist-0.2.0/tests/test_release.py +48 -0
- i_insist-0.2.0/uv.lock +108 -0
|
@@ -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 @@
|
|
|
1
|
+
3.12
|
i_insist-0.2.0/AGENTS.md
ADDED
|
@@ -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.
|
i_insist-0.2.0/PKG-INFO
ADDED
|
@@ -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`.
|