bouncer-core 0.1.1__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.
- bouncer_core-0.1.1/.github/workflows/ci.yml +51 -0
- bouncer_core-0.1.1/.github/workflows/release.yml +85 -0
- bouncer_core-0.1.1/.gitignore +18 -0
- bouncer_core-0.1.1/CONTRIBUTING.md +79 -0
- bouncer_core-0.1.1/LICENSE +21 -0
- bouncer_core-0.1.1/PKG-INFO +442 -0
- bouncer_core-0.1.1/README.md +428 -0
- bouncer_core-0.1.1/SECURITY.md +44 -0
- bouncer_core-0.1.1/benchmark/RESULTS.md +141 -0
- bouncer_core-0.1.1/benchmark/__init__.py +0 -0
- bouncer_core-0.1.1/benchmark/agentdojo_workspace.yaml +15 -0
- bouncer_core-0.1.1/benchmark/run_agentdojo.py +672 -0
- bouncer_core-0.1.1/bouncer-mcp/README.md +22 -0
- bouncer_core-0.1.1/bouncer-mcp/pyproject.toml +35 -0
- bouncer_core-0.1.1/bouncer-mcp/src/bouncer_mcp/__init__.py +3 -0
- bouncer_core-0.1.1/bouncer-mcp/src/bouncer_mcp/__main__.py +9 -0
- bouncer_core-0.1.1/bouncer-mcp/src/bouncer_mcp/cli.py +167 -0
- bouncer_core-0.1.1/bouncer-mcp/src/bouncer_mcp/proxy.py +383 -0
- bouncer_core-0.1.1/bouncer-mcp/src/bouncer_mcp/server.py +490 -0
- bouncer_core-0.1.1/bouncer-mcp/tests/test_cli.py +178 -0
- bouncer_core-0.1.1/bouncer-mcp/tests/test_proxy.py +351 -0
- bouncer_core-0.1.1/bouncer-mcp/tests/test_server.py +156 -0
- bouncer_core-0.1.1/docs/demo.png +0 -0
- bouncer_core-0.1.1/docs/manual-smoke.md +464 -0
- bouncer_core-0.1.1/examples/bouncer.yaml +28 -0
- bouncer_core-0.1.1/lhm.plugin.json +117 -0
- bouncer_core-0.1.1/pyproject.toml +49 -0
- bouncer_core-0.1.1/scripts/demo.py +67 -0
- bouncer_core-0.1.1/scripts/inspect_schema.py +35 -0
- bouncer_core-0.1.1/scripts/smoke_driver.py +80 -0
- bouncer_core-0.1.1/scripts/smoke_driver_policy.py +79 -0
- bouncer_core-0.1.1/scripts/smoke_engine.py +110 -0
- bouncer_core-0.1.1/src/bouncer/__init__.py +3 -0
- bouncer_core-0.1.1/src/bouncer/approvals.py +36 -0
- bouncer_core-0.1.1/src/bouncer/audit.py +31 -0
- bouncer_core-0.1.1/src/bouncer/benchmark_scoring.py +18 -0
- bouncer_core-0.1.1/src/bouncer/engine.py +215 -0
- bouncer_core-0.1.1/src/bouncer/heuristics.py +46 -0
- bouncer_core-0.1.1/src/bouncer/packs/filesystem.yaml +16 -0
- bouncer_core-0.1.1/src/bouncer/packs/gdrive.yaml +13 -0
- bouncer_core-0.1.1/src/bouncer/packs/github.yaml +23 -0
- bouncer_core-0.1.1/src/bouncer/packs/gmail.yaml +17 -0
- bouncer_core-0.1.1/src/bouncer/packs/slack.yaml +18 -0
- bouncer_core-0.1.1/src/bouncer/policy.py +102 -0
- bouncer_core-0.1.1/src/bouncer/taint.py +34 -0
- bouncer_core-0.1.1/src/bouncer/types.py +58 -0
- bouncer_core-0.1.1/tests/test_approvals.py +41 -0
- bouncer_core-0.1.1/tests/test_audit.py +22 -0
- bouncer_core-0.1.1/tests/test_benchmark_scorer.py +19 -0
- bouncer_core-0.1.1/tests/test_engine_constraints.py +113 -0
- bouncer_core-0.1.1/tests/test_engine_sink_gate.py +158 -0
- bouncer_core-0.1.1/tests/test_heuristics.py +32 -0
- bouncer_core-0.1.1/tests/test_packs.py +32 -0
- bouncer_core-0.1.1/tests/test_policy.py +74 -0
- bouncer_core-0.1.1/tests/test_smoke.py +6 -0
- bouncer_core-0.1.1/tests/test_taint.py +30 -0
- bouncer_core-0.1.1/tests/test_types.py +35 -0
- bouncer_core-0.1.1/uv.lock +1996 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.12", "3.13", "3.14"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
|
17
|
+
|
|
18
|
+
- name: Install uv
|
|
19
|
+
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
|
|
20
|
+
with:
|
|
21
|
+
enable-cache: true
|
|
22
|
+
|
|
23
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
24
|
+
run: uv python install ${{ matrix.python-version }}
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: uv sync --all-packages --python ${{ matrix.python-version }}
|
|
28
|
+
|
|
29
|
+
- name: Lint (ruff)
|
|
30
|
+
run: uv run ruff check .
|
|
31
|
+
|
|
32
|
+
- name: Test (pytest)
|
|
33
|
+
run: uv run pytest -q
|
|
34
|
+
|
|
35
|
+
- name: No-key benchmark path imports & scorer test
|
|
36
|
+
run: uv run python -m benchmark.run_agentdojo
|
|
37
|
+
|
|
38
|
+
# The split exists so a consumer of the engine alone never installs the
|
|
39
|
+
# MCP SDK -- and through it uvicorn, starlette, pydantic and pyjwt. That
|
|
40
|
+
# is a property of the dependency graph, so assert it against a real
|
|
41
|
+
# install rather than trusting the pyproject to stay honest.
|
|
42
|
+
- name: bouncer-core must not pull the MCP tree
|
|
43
|
+
run: |
|
|
44
|
+
uv venv /tmp/core-only --python ${{ matrix.python-version }}
|
|
45
|
+
VIRTUAL_ENV=/tmp/core-only uv pip install .
|
|
46
|
+
VIRTUAL_ENV=/tmp/core-only uv pip list > /tmp/core-only.txt
|
|
47
|
+
cat /tmp/core-only.txt
|
|
48
|
+
if grep -qiE '^(mcp|anyio|uvicorn|starlette|pydantic|pyjwt|mcp-types)[[:space:]]' /tmp/core-only.txt; then
|
|
49
|
+
echo "::error::bouncer-core has regained a dependency the split removed"
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Tag-driven. `bouncer-core` and `bouncer-mcp` are versioned and released
|
|
4
|
+
# together from this workspace; both are uploaded under PyPI Trusted Publishing
|
|
5
|
+
# (OIDC), so no long-lived API token exists in this repository's secrets.
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
tags: ["v*"]
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
build:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
|
18
|
+
- uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
|
19
|
+
with:
|
|
20
|
+
enable-cache: true
|
|
21
|
+
|
|
22
|
+
# Checked before the build, so a mistyped tag costs seconds rather than a
|
|
23
|
+
# failed upload. --frozen keeps this a pure read: no re-lock, no sync.
|
|
24
|
+
- name: The tag must match the version being published
|
|
25
|
+
run: |
|
|
26
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
27
|
+
for pkg in bouncer-core bouncer-mcp; do
|
|
28
|
+
version=$(uv version --package "$pkg" --short --frozen)
|
|
29
|
+
if [ "$tag" != "$version" ]; then
|
|
30
|
+
echo "::error::tag $GITHUB_REF_NAME does not match $pkg $version"
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
done
|
|
34
|
+
|
|
35
|
+
# Built into separate directories: a trusted-publishing token is minted
|
|
36
|
+
# per project, so each distribution is uploaded by its own step.
|
|
37
|
+
- name: Build both distributions
|
|
38
|
+
run: |
|
|
39
|
+
uv build --package bouncer-core --out-dir dist/core
|
|
40
|
+
uv build --package bouncer-mcp --out-dir dist/mcp
|
|
41
|
+
ls -la dist/core dist/mcp
|
|
42
|
+
|
|
43
|
+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
44
|
+
with:
|
|
45
|
+
name: distributions
|
|
46
|
+
path: dist/
|
|
47
|
+
|
|
48
|
+
# One job per distribution, each in its own environment. PyPI refuses a second
|
|
49
|
+
# pending trusted publisher whose owner, repository, workflow AND environment
|
|
50
|
+
# all match an existing one, so the two projects cannot share `pypi`.
|
|
51
|
+
publish-core:
|
|
52
|
+
needs: build
|
|
53
|
+
runs-on: ubuntu-latest
|
|
54
|
+
environment: pypi
|
|
55
|
+
permissions:
|
|
56
|
+
id-token: write # mints the OIDC token; nothing else is needed
|
|
57
|
+
steps:
|
|
58
|
+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
59
|
+
with:
|
|
60
|
+
name: distributions
|
|
61
|
+
path: dist/
|
|
62
|
+
|
|
63
|
+
- name: Publish bouncer-core
|
|
64
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
65
|
+
with:
|
|
66
|
+
packages-dir: dist/core
|
|
67
|
+
|
|
68
|
+
# After core, never alongside it: bouncer-mcp pins bouncer-core==<same version>,
|
|
69
|
+
# so publishing it first would briefly ship a package nobody can install.
|
|
70
|
+
publish-mcp:
|
|
71
|
+
needs: publish-core
|
|
72
|
+
runs-on: ubuntu-latest
|
|
73
|
+
environment: pypi-mcp
|
|
74
|
+
permissions:
|
|
75
|
+
id-token: write
|
|
76
|
+
steps:
|
|
77
|
+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
78
|
+
with:
|
|
79
|
+
name: distributions
|
|
80
|
+
path: dist/
|
|
81
|
+
|
|
82
|
+
- name: Publish bouncer-mcp
|
|
83
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
84
|
+
with:
|
|
85
|
+
packages-dir: dist/mcp
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# regenerated on every benchmark run (RESULTS.md is the curated, committed doc)
|
|
2
|
+
benchmark/audit.jsonl
|
|
3
|
+
benchmark/audit-*.jsonl
|
|
4
|
+
benchmark/results.json
|
|
5
|
+
benchmark/results-raw.json
|
|
6
|
+
benchmark/logs/
|
|
7
|
+
|
|
8
|
+
# local smoke-test scratch (contains machine-specific absolute paths)
|
|
9
|
+
smoke_work/
|
|
10
|
+
|
|
11
|
+
__pycache__/
|
|
12
|
+
.venv/
|
|
13
|
+
.pytest_cache/
|
|
14
|
+
.mypy_cache/
|
|
15
|
+
.ruff_cache/
|
|
16
|
+
|
|
17
|
+
# local launch/marketing notes — never publish to the OSS repo
|
|
18
|
+
docs/launch-posts.md
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Contributing to Bouncer
|
|
2
|
+
|
|
3
|
+
Thanks for helping build a deterministic safety layer for agent tool calls.
|
|
4
|
+
This is security-adjacent code — the bar is correctness and clarity over
|
|
5
|
+
cleverness. Small, well-tested PRs are far easier to accept than large ones.
|
|
6
|
+
|
|
7
|
+
## Development setup
|
|
8
|
+
|
|
9
|
+
Bouncer uses [uv](https://docs.astral.sh/uv/). From the repository root:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uv sync # install the project + dev tools (pytest, ruff)
|
|
13
|
+
uv run pytest -q # run the full test suite
|
|
14
|
+
uv run ruff check . # lint
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The benchmark harness is an optional extra (it pulls in AgentDojo):
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uv run --extra benchmark python -m benchmark.run_agentdojo # needs GEMINI_API_KEY
|
|
21
|
+
uv run python -m benchmark.run_agentdojo # no key: scorer test only
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
CI runs exactly `ruff check .` and `pytest -q` on Python 3.12 and 3.13. Get
|
|
25
|
+
both green locally before opening a PR.
|
|
26
|
+
|
|
27
|
+
## The one rule that isn't negotiable: fail closed
|
|
28
|
+
|
|
29
|
+
Bouncer is only useful if it never *silently* allows something it couldn't
|
|
30
|
+
reason about. Every ambiguous case must resolve to `ask` or `deny`, never a
|
|
31
|
+
silent `allow`. Concretely:
|
|
32
|
+
|
|
33
|
+
- A tool marked `exfiltrating` with no usable `sink_params` treats **every**
|
|
34
|
+
argument as a destination.
|
|
35
|
+
- An unknown/unpinned tool → `ask`, not `allow`.
|
|
36
|
+
- A malformed policy file → `PolicyError` at load time (the proxy refuses to
|
|
37
|
+
start), not a best-effort partial parse.
|
|
38
|
+
- A `deny` **must not** forward the call upstream. This invariant is load-
|
|
39
|
+
bearing; tests assert it, and any change near the decision path must keep it.
|
|
40
|
+
|
|
41
|
+
If your change adds a new decision branch, add a test that proves the
|
|
42
|
+
fail-closed direction, not just the happy path. See `tests/test_engine_sink_gate.py`
|
|
43
|
+
for the style (red test first, then the engine change).
|
|
44
|
+
|
|
45
|
+
## What makes a good PR
|
|
46
|
+
|
|
47
|
+
- **Tests with it.** New behavior needs a test; a bug fix needs a regression
|
|
48
|
+
test that fails before your change and passes after.
|
|
49
|
+
- **No LLM in the decision path.** The engine is deterministic by design.
|
|
50
|
+
Heuristics, packs, and taint rules are welcome; a model call inside
|
|
51
|
+
`ContractEngine.evaluate` is not.
|
|
52
|
+
- **Type hints on every function signature** (params and return).
|
|
53
|
+
- **Match the surrounding style.** Comment density, naming, and idiom should
|
|
54
|
+
read like the file you're editing. No docstrings on obvious functions.
|
|
55
|
+
- **Keep the diff scoped.** Don't refactor surrounding code as part of a fix.
|
|
56
|
+
|
|
57
|
+
## Adding or fixing a server pack
|
|
58
|
+
|
|
59
|
+
Packs live in `src/bouncer/packs/*.yaml` and map a server's tools to policies.
|
|
60
|
+
The most valuable contributions here are packs verified against a **real**
|
|
61
|
+
server's `tools/list`:
|
|
62
|
+
|
|
63
|
+
- Enumerate **every** destination-bearing argument as a `sink_param` — a
|
|
64
|
+
missed one is a silent exfiltration gap (see the sink-completeness note in
|
|
65
|
+
the README's limitations).
|
|
66
|
+
- Prefer verified tool/param names over documented-but-unchecked ones; note in
|
|
67
|
+
a comment how you verified (e.g. "verified live 2026-07-06").
|
|
68
|
+
- Add or update a test in `tests/test_packs.py`.
|
|
69
|
+
|
|
70
|
+
## Reporting security issues
|
|
71
|
+
|
|
72
|
+
Please do **not** open a public issue for a vulnerability in the enforcement
|
|
73
|
+
logic (e.g. a way to make an exfiltrating call resolve to `allow`). See
|
|
74
|
+
[SECURITY.md](SECURITY.md) for private disclosure.
|
|
75
|
+
|
|
76
|
+
## Commit messages
|
|
77
|
+
|
|
78
|
+
Conventional-commit style (`fix(engine): ...`, `feat(packs): ...`,
|
|
79
|
+
`docs: ...`). Explain the *why* in the body when it isn't obvious from the diff.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bouncer contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|