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.
Files changed (58) hide show
  1. bouncer_core-0.1.1/.github/workflows/ci.yml +51 -0
  2. bouncer_core-0.1.1/.github/workflows/release.yml +85 -0
  3. bouncer_core-0.1.1/.gitignore +18 -0
  4. bouncer_core-0.1.1/CONTRIBUTING.md +79 -0
  5. bouncer_core-0.1.1/LICENSE +21 -0
  6. bouncer_core-0.1.1/PKG-INFO +442 -0
  7. bouncer_core-0.1.1/README.md +428 -0
  8. bouncer_core-0.1.1/SECURITY.md +44 -0
  9. bouncer_core-0.1.1/benchmark/RESULTS.md +141 -0
  10. bouncer_core-0.1.1/benchmark/__init__.py +0 -0
  11. bouncer_core-0.1.1/benchmark/agentdojo_workspace.yaml +15 -0
  12. bouncer_core-0.1.1/benchmark/run_agentdojo.py +672 -0
  13. bouncer_core-0.1.1/bouncer-mcp/README.md +22 -0
  14. bouncer_core-0.1.1/bouncer-mcp/pyproject.toml +35 -0
  15. bouncer_core-0.1.1/bouncer-mcp/src/bouncer_mcp/__init__.py +3 -0
  16. bouncer_core-0.1.1/bouncer-mcp/src/bouncer_mcp/__main__.py +9 -0
  17. bouncer_core-0.1.1/bouncer-mcp/src/bouncer_mcp/cli.py +167 -0
  18. bouncer_core-0.1.1/bouncer-mcp/src/bouncer_mcp/proxy.py +383 -0
  19. bouncer_core-0.1.1/bouncer-mcp/src/bouncer_mcp/server.py +490 -0
  20. bouncer_core-0.1.1/bouncer-mcp/tests/test_cli.py +178 -0
  21. bouncer_core-0.1.1/bouncer-mcp/tests/test_proxy.py +351 -0
  22. bouncer_core-0.1.1/bouncer-mcp/tests/test_server.py +156 -0
  23. bouncer_core-0.1.1/docs/demo.png +0 -0
  24. bouncer_core-0.1.1/docs/manual-smoke.md +464 -0
  25. bouncer_core-0.1.1/examples/bouncer.yaml +28 -0
  26. bouncer_core-0.1.1/lhm.plugin.json +117 -0
  27. bouncer_core-0.1.1/pyproject.toml +49 -0
  28. bouncer_core-0.1.1/scripts/demo.py +67 -0
  29. bouncer_core-0.1.1/scripts/inspect_schema.py +35 -0
  30. bouncer_core-0.1.1/scripts/smoke_driver.py +80 -0
  31. bouncer_core-0.1.1/scripts/smoke_driver_policy.py +79 -0
  32. bouncer_core-0.1.1/scripts/smoke_engine.py +110 -0
  33. bouncer_core-0.1.1/src/bouncer/__init__.py +3 -0
  34. bouncer_core-0.1.1/src/bouncer/approvals.py +36 -0
  35. bouncer_core-0.1.1/src/bouncer/audit.py +31 -0
  36. bouncer_core-0.1.1/src/bouncer/benchmark_scoring.py +18 -0
  37. bouncer_core-0.1.1/src/bouncer/engine.py +215 -0
  38. bouncer_core-0.1.1/src/bouncer/heuristics.py +46 -0
  39. bouncer_core-0.1.1/src/bouncer/packs/filesystem.yaml +16 -0
  40. bouncer_core-0.1.1/src/bouncer/packs/gdrive.yaml +13 -0
  41. bouncer_core-0.1.1/src/bouncer/packs/github.yaml +23 -0
  42. bouncer_core-0.1.1/src/bouncer/packs/gmail.yaml +17 -0
  43. bouncer_core-0.1.1/src/bouncer/packs/slack.yaml +18 -0
  44. bouncer_core-0.1.1/src/bouncer/policy.py +102 -0
  45. bouncer_core-0.1.1/src/bouncer/taint.py +34 -0
  46. bouncer_core-0.1.1/src/bouncer/types.py +58 -0
  47. bouncer_core-0.1.1/tests/test_approvals.py +41 -0
  48. bouncer_core-0.1.1/tests/test_audit.py +22 -0
  49. bouncer_core-0.1.1/tests/test_benchmark_scorer.py +19 -0
  50. bouncer_core-0.1.1/tests/test_engine_constraints.py +113 -0
  51. bouncer_core-0.1.1/tests/test_engine_sink_gate.py +158 -0
  52. bouncer_core-0.1.1/tests/test_heuristics.py +32 -0
  53. bouncer_core-0.1.1/tests/test_packs.py +32 -0
  54. bouncer_core-0.1.1/tests/test_policy.py +74 -0
  55. bouncer_core-0.1.1/tests/test_smoke.py +6 -0
  56. bouncer_core-0.1.1/tests/test_taint.py +30 -0
  57. bouncer_core-0.1.1/tests/test_types.py +35 -0
  58. 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.