boundedllm 0.4.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 (102) hide show
  1. boundedllm-0.4.0/.dockerignore +11 -0
  2. boundedllm-0.4.0/.env.example +41 -0
  3. boundedllm-0.4.0/.gitattributes +42 -0
  4. boundedllm-0.4.0/.github/workflows/release.yml +109 -0
  5. boundedllm-0.4.0/.github/workflows/security.yml +204 -0
  6. boundedllm-0.4.0/.gitignore +62 -0
  7. boundedllm-0.4.0/.gitleaks.toml +67 -0
  8. boundedllm-0.4.0/CHANGELOG.md +175 -0
  9. boundedllm-0.4.0/Dockerfile +54 -0
  10. boundedllm-0.4.0/LICENSE +202 -0
  11. boundedllm-0.4.0/OPERATIONS.md +179 -0
  12. boundedllm-0.4.0/PKG-INFO +373 -0
  13. boundedllm-0.4.0/QUICKSTART.md +202 -0
  14. boundedllm-0.4.0/README.md +309 -0
  15. boundedllm-0.4.0/SECURITY.md +101 -0
  16. boundedllm-0.4.0/pyproject.toml +110 -0
  17. boundedllm-0.4.0/scripts/load_check.py +218 -0
  18. boundedllm-0.4.0/src/boundedllm/__init__.py +86 -0
  19. boundedllm-0.4.0/src/boundedllm/adapters/__init__.py +6 -0
  20. boundedllm-0.4.0/src/boundedllm/adapters/http_model.py +45 -0
  21. boundedllm-0.4.0/src/boundedllm/adapters/otlp.py +104 -0
  22. boundedllm-0.4.0/src/boundedllm/adapters/sql/__init__.py +136 -0
  23. boundedllm-0.4.0/src/boundedllm/adapters/sql/alembic.ini +39 -0
  24. boundedllm-0.4.0/src/boundedllm/adapters/sql/migrations/env.py +59 -0
  25. boundedllm-0.4.0/src/boundedllm/adapters/sql/migrations/script.py.mako +24 -0
  26. boundedllm-0.4.0/src/boundedllm/adapters/sql/migrations/versions/20260916_71f5e126461a_baseline_schema.py +183 -0
  27. boundedllm-0.4.0/src/boundedllm/adapters/sql/schema.py +123 -0
  28. boundedllm-0.4.0/src/boundedllm/adapters/sql/store.py +1206 -0
  29. boundedllm-0.4.0/src/boundedllm/audit.py +156 -0
  30. boundedllm-0.4.0/src/boundedllm/authz.py +42 -0
  31. boundedllm-0.4.0/src/boundedllm/budget.py +20 -0
  32. boundedllm-0.4.0/src/boundedllm/cli.py +390 -0
  33. boundedllm-0.4.0/src/boundedllm/config.py +171 -0
  34. boundedllm-0.4.0/src/boundedllm/context.py +26 -0
  35. boundedllm-0.4.0/src/boundedllm/contrib/__init__.py +22 -0
  36. boundedllm-0.4.0/src/boundedllm/contrib/anthropic.py +122 -0
  37. boundedllm-0.4.0/src/boundedllm/contrib/ledger.py +181 -0
  38. boundedllm-0.4.0/src/boundedllm/contrib/pgvector.py +250 -0
  39. boundedllm-0.4.0/src/boundedllm/egress.py +103 -0
  40. boundedllm-0.4.0/src/boundedllm/engine.py +327 -0
  41. boundedllm-0.4.0/src/boundedllm/errors.py +33 -0
  42. boundedllm-0.4.0/src/boundedllm/identity.py +125 -0
  43. boundedllm-0.4.0/src/boundedllm/limits.py +83 -0
  44. boundedllm-0.4.0/src/boundedllm/main.py +200 -0
  45. boundedllm-0.4.0/src/boundedllm/middleware.py +87 -0
  46. boundedllm-0.4.0/src/boundedllm/model_gateway.py +52 -0
  47. boundedllm-0.4.0/src/boundedllm/models.py +122 -0
  48. boundedllm-0.4.0/src/boundedllm/network.py +27 -0
  49. boundedllm-0.4.0/src/boundedllm/normalize.py +32 -0
  50. boundedllm-0.4.0/src/boundedllm/output_firewall.py +87 -0
  51. boundedllm-0.4.0/src/boundedllm/parsing.py +26 -0
  52. boundedllm-0.4.0/src/boundedllm/ports.py +118 -0
  53. boundedllm-0.4.0/src/boundedllm/prompts.py +14 -0
  54. boundedllm-0.4.0/src/boundedllm/retrieval.py +39 -0
  55. boundedllm-0.4.0/src/boundedllm/risk.py +54 -0
  56. boundedllm-0.4.0/src/boundedllm/support/__init__.py +44 -0
  57. boundedllm-0.4.0/src/boundedllm/support/authz.py +16 -0
  58. boundedllm-0.4.0/src/boundedllm/support/gateway.py +48 -0
  59. boundedllm-0.4.0/src/boundedllm/support/models.py +49 -0
  60. boundedllm-0.4.0/src/boundedllm/support/policy.py +67 -0
  61. boundedllm-0.4.0/src/boundedllm/support/projections.py +8 -0
  62. boundedllm-0.4.0/src/boundedllm/support/schema.py +45 -0
  63. boundedllm-0.4.0/src/boundedllm/support/store.py +257 -0
  64. boundedllm-0.4.0/src/boundedllm/telemetry.py +82 -0
  65. boundedllm-0.4.0/src/boundedllm/tool_gateway.py +33 -0
  66. boundedllm-0.4.0/testApp/.env.example +11 -0
  67. boundedllm-0.4.0/testApp/.gitignore +26 -0
  68. boundedllm-0.4.0/testApp/README.md +121 -0
  69. boundedllm-0.4.0/testApp/knowledge/roadshield_auto_policy.md +81 -0
  70. boundedllm-0.4.0/testApp/pyproject.toml +42 -0
  71. boundedllm-0.4.0/testApp/roadshield/__init__.py +1 -0
  72. boundedllm-0.4.0/testApp/roadshield/agent.py +65 -0
  73. boundedllm-0.4.0/testApp/roadshield/attacks.py +137 -0
  74. boundedllm-0.4.0/testApp/roadshield/auth.py +76 -0
  75. boundedllm-0.4.0/testApp/roadshield/claude.py +109 -0
  76. boundedllm-0.4.0/testApp/roadshield/config.py +123 -0
  77. boundedllm-0.4.0/testApp/roadshield/documents.py +118 -0
  78. boundedllm-0.4.0/testApp/roadshield/knowledge.py +23 -0
  79. boundedllm-0.4.0/testApp/roadshield/main.py +492 -0
  80. boundedllm-0.4.0/testApp/roadshield/prompts.py +27 -0
  81. boundedllm-0.4.0/testApp/roadshield/schema.py +83 -0
  82. boundedllm-0.4.0/testApp/roadshield/seed.py +217 -0
  83. boundedllm-0.4.0/testApp/roadshield/static/app.js +288 -0
  84. boundedllm-0.4.0/testApp/roadshield/static/index.html +85 -0
  85. boundedllm-0.4.0/testApp/roadshield/static/styles.css +6 -0
  86. boundedllm-0.4.0/testApp/roadshield/store.py +338 -0
  87. boundedllm-0.4.0/testApp/roadshield-claims-update-poisoned.pdf +74 -0
  88. boundedllm-0.4.0/testApp/run.py +49 -0
  89. boundedllm-0.4.0/testApp/scripts/create_attack_pdf.py +150 -0
  90. boundedllm-0.4.0/testApp/scripts/run_live_attacks.py +68 -0
  91. boundedllm-0.4.0/testApp/tests/test_app.py +444 -0
  92. boundedllm-0.4.0/testApp/tests/test_claude.py +116 -0
  93. boundedllm-0.4.0/testApp/uv.lock +903 -0
  94. boundedllm-0.4.0/tests/conftest.py +14 -0
  95. boundedllm-0.4.0/tests/harness.py +122 -0
  96. boundedllm-0.4.0/tests/test_adversarial.py +986 -0
  97. boundedllm-0.4.0/tests/test_contrib.py +335 -0
  98. boundedllm-0.4.0/tests/test_documentation.py +79 -0
  99. boundedllm-0.4.0/tests/test_money_concurrency.py +323 -0
  100. boundedllm-0.4.0/tests/test_postgres.py +316 -0
  101. boundedllm-0.4.0/tests/test_security.py +750 -0
  102. boundedllm-0.4.0/uv.lock +2757 -0
@@ -0,0 +1,11 @@
1
+ # Keep local environments, build data, and the attack simulator out of images.
2
+ .git
3
+ .venv
4
+ .uv-cache
5
+ .pytest_cache
6
+ .ruff_cache
7
+ .dist-check
8
+ dist
9
+ __pycache__
10
+ *.db*
11
+ testApp
@@ -0,0 +1,41 @@
1
+ # Copy into your secret manager configuration; never commit a populated .env.
2
+ # Every GUARD_* value below is read only at startup and never from a request.
3
+
4
+ GUARD_ENVIRONMENT=development
5
+ GUARD_DATABASE_URL=sqlite:///guard.db
6
+
7
+ # Signs the tamper-evident ledger. At least 32 bytes from a CSPRNG, generated
8
+ # inside the secret manager or an HSM. Production refuses to start without it.
9
+ GUARD_AUDIT_KEY=replace-with-at-least-32-random-bytes
10
+ GUARD_AUDIT_KEY_ID=security-ledger-2026-q3
11
+
12
+ # Retain the previous signing key for the whole rotation window. Pending
13
+ # approvals and idempotency digests are verified against it, so removing it too
14
+ # early strands unredeemed approvals and turns in-flight retries into conflicts.
15
+ # GUARD_AUDIT_PREVIOUS_KEYS={"security-ledger-2026-q2":"old-key-at-least-32-bytes-long"}
16
+
17
+ # Optional long-lived key used only for pseudonymization. Set it if analysts must
18
+ # follow one subject across a signing-key rotation; it must never rotate with the
19
+ # signing key. It is a re-identification risk of its own, so scope it as tightly.
20
+ # GUARD_AUDIT_PSEUDONYM_KEY=replace-with-at-least-32-random-bytes
21
+
22
+ # Identity. The package verifies tokens against this JWKS and never fetches a
23
+ # jku or x5u value supplied by a token header.
24
+ GUARD_ISSUER=https://id.example.com/
25
+ GUARD_AUDIENCE=secure-ai-api
26
+ GUARD_JWKS_URL=https://id.example.com/.well-known/jwks.json
27
+ GUARD_ALLOWED_TENANTS='["tenant-a"]'
28
+
29
+ # Model transport. Required for the bundled HTTP provider; a host embedding the
30
+ # library with its own vendor SDK does not need it.
31
+ GUARD_MODEL_URL=https://model.example.com/v1/complete
32
+ # GUARD_MODEL_API_KEY=replace-from-secret-manager
33
+
34
+ # Security event delivery to an OpenTelemetry collector or SIEM gateway.
35
+ # GUARD_OTEL_LOGS_ENDPOINT=https://otel-collector.example.com/v1/logs
36
+ # GUARD_OTEL_AUTHORIZATION=Bearer replace-from-secret-manager
37
+
38
+ # DLP. "pattern" is a test baseline and is refused in production. Use "presidio"
39
+ # for the bundled integration, or "reviewed" to supply your own evaluated
40
+ # Scanner to create_app where Presidio's footprint is unacceptable.
41
+ # GUARD_DLP_BACKEND=presidio
@@ -0,0 +1,42 @@
1
+ # Line-ending and binary policy.
2
+ #
3
+ # This exists because git silently corrupted a binary test fixture. With
4
+ # core.autocrlf=true on Windows and no rule here, git applied CRLF conversion to
5
+ # roadshield-claims-update-poisoned.pdf, adding 74 bytes. The file was fine in
6
+ # the working tree and broken for everyone who cloned, and the failure surfaced
7
+ # as "PDF could not be safely parsed" in CI rather than as anything resembling a
8
+ # line-ending problem. Git for Windows also ships a default attribute marking
9
+ # PDFs `diff: astextplain`, which makes them look text-like.
10
+
11
+ # Normalize text to LF in the repository regardless of the committer's platform.
12
+ # Checkout still converts for Windows users who want CRLF locally.
13
+ * text=auto eol=lf
14
+
15
+ # Never touch these. The list is deliberately broad: the cost of a missing entry
16
+ # is a corrupted file that passes locally, and the cost of a spurious one is none.
17
+ *.pdf binary
18
+ *.png binary
19
+ *.jpg binary
20
+ *.jpeg binary
21
+ *.gif binary
22
+ *.ico binary
23
+ *.webp binary
24
+ *.zip binary
25
+ *.gz binary
26
+ *.tar binary
27
+ *.whl binary
28
+ *.db binary
29
+ *.sqlite binary
30
+ *.sst binary
31
+ *.pem binary
32
+ *.p12 binary
33
+ *.pfx binary
34
+ *.pyc binary
35
+
36
+ # Shell scripts must keep LF even when checked out on Windows, or they fail to
37
+ # run in a Linux container with a confusing "bad interpreter" error.
38
+ *.sh text eol=lf
39
+ Dockerfile text eol=lf
40
+
41
+ # Lockfiles are generated; collapsing their diffs keeps reviews readable.
42
+ uv.lock linguist-generated=true
@@ -0,0 +1,109 @@
1
+ # Publish to PyPI on a version tag, using Trusted Publishing.
2
+ #
3
+ # There is no API token anywhere in this workflow or in repository secrets. PyPI
4
+ # verifies a short-lived OIDC identity issued by GitHub for this exact repository,
5
+ # workflow, and environment, and mints a token valid for minutes. A leaked repo
6
+ # secret is the usual way packages get hijacked; this removes the secret.
7
+ #
8
+ # One-time setup on PyPI, before the first tag:
9
+ # 1. https://pypi.org/manage/account/publishing/ -> Add a pending publisher
10
+ # PyPI project name : boundedllm
11
+ # Owner : taskul
12
+ # Repository : boundedllm
13
+ # Workflow : release.yml
14
+ # Environment : pypi
15
+ # 2. In GitHub: Settings -> Environments -> New environment -> "pypi".
16
+ # Add yourself as a required reviewer so a tag cannot publish unattended.
17
+ #
18
+ # Then: git tag v0.4.0 && git push origin v0.4.0
19
+ name: release
20
+ on:
21
+ push:
22
+ tags: ["v*"]
23
+
24
+ permissions:
25
+ contents: read
26
+
27
+ jobs:
28
+ # Build once. The same artifacts are tested, published, and attached to the
29
+ # release, so what people install is what CI checked.
30
+ build:
31
+ runs-on: ubuntu-latest
32
+ timeout-minutes: 15
33
+ steps:
34
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
35
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
36
+ with: { enable-cache: true }
37
+
38
+ # The tag must match the packaged version, or users get a release whose
39
+ # contents disagree with its name.
40
+ - name: Tag matches package version
41
+ run: |
42
+ TAG="${GITHUB_REF_NAME#v}"
43
+ PKG=$(uv run --no-project python -c "import tomllib,pathlib; print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version'])")
44
+ test "$TAG" = "$PKG" || { echo "tag $TAG != version $PKG"; exit 1; }
45
+ grep -q "## \[$PKG\]" CHANGELOG.md || { echo "no CHANGELOG entry for $PKG"; exit 1; }
46
+
47
+ - run: uv build
48
+ # twine check catches a README that will not render on PyPI, which is only
49
+ # discoverable after an irreversible upload otherwise.
50
+ - run: uvx twine check --strict dist/*
51
+
52
+ # Install the built wheel in a clean environment and use it. A package that
53
+ # imports only from the source tree passes every other job and fails here.
54
+ - name: Installed wheel is importable and runnable
55
+ run: |
56
+ uv venv /tmp/fresh
57
+ VIRTUAL_ENV=/tmp/fresh uv pip install dist/*.whl
58
+ /tmp/fresh/bin/python -c "import boundedllm; print('import ok', boundedllm.__version__)"
59
+ VIRTUAL_ENV=/tmp/fresh uv pip install "$(ls dist/*.whl)[sql]"
60
+ /tmp/fresh/bin/boundedllm demo > /dev/null && echo "cli ok"
61
+
62
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
63
+ with:
64
+ name: dist
65
+ path: dist/
66
+
67
+ pypi:
68
+ needs: build
69
+ runs-on: ubuntu-latest
70
+ timeout-minutes: 10
71
+ environment:
72
+ name: pypi
73
+ url: https://pypi.org/p/boundedllm
74
+ permissions:
75
+ # The OIDC token PyPI verifies. Nothing else needs write access.
76
+ id-token: write
77
+ steps:
78
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
79
+ with:
80
+ name: dist
81
+ path: dist/
82
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
83
+
84
+ github-release:
85
+ needs: pypi
86
+ runs-on: ubuntu-latest
87
+ timeout-minutes: 10
88
+ permissions:
89
+ contents: write
90
+ # Signs the artifacts so a downloader can prove they came from this workflow.
91
+ id-token: write
92
+ attestations: write
93
+ steps:
94
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
95
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
96
+ with:
97
+ name: dist
98
+ path: dist/
99
+ - uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2
100
+ with:
101
+ subject-path: dist/*
102
+ - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
103
+ with:
104
+ files: dist/*
105
+ generate_release_notes: true
106
+ body: |
107
+ Install: `pip install boundedllm==${{ github.ref_name }}`
108
+
109
+ See [CHANGELOG.md](CHANGELOG.md) for what changed and whether it breaks.
@@ -0,0 +1,204 @@
1
+ # Every prompt, policy, dependency, and code change runs deterministic checks.
2
+ #
3
+ # Actions are pinned to commit SHAs, not tags. A tag is mutable, so an upstream
4
+ # account compromise would otherwise reach a workflow that holds repository
5
+ # credentials. The comment after each SHA records the human-readable version.
6
+ # Re-resolve them with a tool rather than by hand:
7
+ # go install github.com/suzuki-shunsuke/pinact/cmd/pinact@latest && pinact run
8
+ name: security
9
+ on: [push, pull_request]
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ library:
15
+ runs-on: ubuntu-latest
16
+ timeout-minutes: 15
17
+ steps:
18
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
19
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
20
+ with: { enable-cache: true }
21
+ - run: uv sync --locked --dev --all-extras
22
+ - run: uv run pytest -q
23
+ # The quickstart example is executed by tests/test_documentation.py.
24
+ - run: uv run ruff check src tests
25
+ - run: uv run ruff format --check src tests
26
+ # pip-audit's --locked flag reads a PEP 751 pylock.toml, not uv.lock, so it
27
+ # failed to resolve anything to audit. Export the locked runtime set and
28
+ # audit that instead: it is the exact dependency set the wheel ships.
29
+ - run: uv export --no-dev --no-emit-project --all-extras --format requirements-txt -o requirements.lock.txt
30
+ - run: uv run pip-audit -r requirements.lock.txt --disable-pip
31
+
32
+ # The core's whole claim is that a host keeps its own storage. If importing it
33
+ # ever needs a database driver again, this job fails before anything else does.
34
+ core-has-no-storage-dependency:
35
+ runs-on: ubuntu-latest
36
+ timeout-minutes: 10
37
+ steps:
38
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
39
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
40
+ with: { enable-cache: true }
41
+ # Deliberately no extras: install the package exactly as `pip install
42
+ # boundedllm` would, then use it.
43
+ - run: uv venv && uv pip install .
44
+ - run: |
45
+ uv run --no-project python - <<'PY'
46
+ import importlib.util
47
+ for forbidden in ("sqlalchemy", "fastapi", "httpx", "jwt", "pydantic_settings"):
48
+ assert importlib.util.find_spec(forbidden) is None, f"{forbidden} reached the core install"
49
+ import boundedllm
50
+ from boundedllm import ChatRequest, Guard, Limits, Principal # noqa: F401
51
+ print("core import surface is storage-free:", boundedllm.__version__)
52
+ PY
53
+
54
+ # PostgreSQL is where production runs and was the least-exercised path.
55
+ postgres:
56
+ runs-on: ubuntu-latest
57
+ timeout-minutes: 20
58
+ services:
59
+ postgres:
60
+ image: postgres:17
61
+ env:
62
+ POSTGRES_PASSWORD: postgres
63
+ POSTGRES_DB: guard_test
64
+ options: >-
65
+ --health-cmd "pg_isready -U postgres"
66
+ --health-interval 5s --health-timeout 5s --health-retries 10
67
+ ports: ["5432:5432"]
68
+ env:
69
+ TEST_POSTGRES_URL: postgresql+psycopg://postgres:postgres@localhost:5432/guard_test
70
+ GUARD_DATABASE_URL: postgresql+psycopg://postgres:postgres@localhost:5432/guard_test
71
+ GUARD_AUDIT_KEY: ci-only-not-a-secret-at-least-32-bytes-long
72
+ steps:
73
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
74
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
75
+ with: { enable-cache: true }
76
+ - run: uv sync --locked --dev --all-extras
77
+ # Migrations must apply cleanly and then report no drift from the models.
78
+ # Drift is how a schema quietly stops matching the ACL predicates built on it.
79
+ - run: uv run boundedllm migrate
80
+ - run: uv run alembic -c src/boundedllm/adapters/sql/alembic.ini check
81
+ - run: uv run boundedllm enable-rls
82
+ # RLS isolation, the contrib adapters' ACL predicates, and the concurrent
83
+ # audit-chain test all need a real database.
84
+ # RLS isolation, the contrib adapters' ACL predicates, the concurrent
85
+ # audit chain, and the monetary paths all need a database that actually
86
+ # honours with_for_update(). On SQLite those locks are silent no-ops.
87
+ - run: uv run pytest -q tests/test_postgres.py tests/test_contrib.py tests/test_money_concurrency.py
88
+
89
+ attack-lab:
90
+ runs-on: ubuntu-latest
91
+ timeout-minutes: 15
92
+ steps:
93
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
94
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
95
+ with: { enable-cache: true }
96
+ - run: uv sync --locked --dev
97
+ working-directory: testApp
98
+ - run: uv run pytest -q
99
+ working-directory: testApp
100
+ - run: uv run ruff check roadshield tests scripts run.py
101
+ working-directory: testApp
102
+ - run: uv run ruff format --check roadshield tests scripts run.py
103
+ working-directory: testApp
104
+
105
+ image:
106
+ runs-on: ubuntu-latest
107
+ timeout-minutes: 15
108
+ steps:
109
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
110
+ - run: docker build -t boundedllm:ci .
111
+ # Run it the way the Dockerfile documents: read-only tree, tmpfs, no extra
112
+ # capabilities. A container that only starts when it can write to itself is
113
+ # not the artifact we intend to ship.
114
+ - run: |
115
+ docker run -d --name guard-ci -p 8000:8000 \
116
+ --read-only --tmpfs /tmp:rw,noexec,nosuid,size=64m \
117
+ --cap-drop ALL --security-opt no-new-privileges \
118
+ -e GUARD_AUDIT_KEY=ci-only-not-a-secret-at-least-32-bytes-long \
119
+ -e GUARD_DATABASE_URL=sqlite:////tmp/guard.db \
120
+ -e GUARD_MODEL_URL=https://model.example.com/v1/complete \
121
+ boundedllm:ci
122
+ for i in $(seq 1 30); do
123
+ if curl -fsS http://127.0.0.1:8000/health/live; then echo; break; fi
124
+ if [ "$i" = "30" ]; then echo "never became live"; docker logs guard-ci; exit 1; fi
125
+ sleep 2
126
+ done
127
+ test "$(docker inspect -f '{{.Config.User}}' boundedllm:ci)" = "app" \
128
+ || { echo "image does not run as the unprivileged app user"; exit 1; }
129
+ - if: always()
130
+ run: docker rm -f guard-ci || true
131
+
132
+ # A dependency inventory that ships with the release, so a downstream consumer
133
+ # can answer "are we affected" without rebuilding the image.
134
+ sbom:
135
+ runs-on: ubuntu-latest
136
+ timeout-minutes: 10
137
+ steps:
138
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
139
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
140
+ with: { enable-cache: true }
141
+ - run: uv sync --locked --all-extras --no-dev
142
+ - uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0
143
+ with:
144
+ path: .
145
+ format: cyclonedx-json
146
+ artifact-name: sbom.cyclonedx.json
147
+
148
+ codeql:
149
+ runs-on: ubuntu-latest
150
+ timeout-minutes: 20
151
+ permissions:
152
+ contents: read
153
+ security-events: write
154
+ steps:
155
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
156
+ - uses: github/codeql-action/init@faaca9a8f6edddba5725ffe5adefdab6669a2eca # v3
157
+ with:
158
+ languages: python
159
+ queries: security-extended
160
+ - uses: github/codeql-action/analyze@faaca9a8f6edddba5725ffe5adefdab6669a2eca # v3
161
+
162
+ secrets:
163
+ runs-on: ubuntu-latest
164
+ timeout-minutes: 10
165
+ steps:
166
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
167
+ with:
168
+ # Full history: a key removed in a later commit is still a leaked key.
169
+ fetch-depth: 0
170
+ # .gitleaks.toml extends the defaults. Verified against synthetic
171
+ # high-entropy credentials: the defaults catch AWS, GitHub, Stripe and
172
+ # Slack but have no Anthropic rule, which is this repository's most likely
173
+ # leak. Do not drop the config and assume useDefault covers it.
174
+ - uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2
175
+ env:
176
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
177
+ GITLEAKS_CONFIG: .gitleaks.toml
178
+ GITLEAKS_ENABLE_UPLOAD_ARTIFACT: "false"
179
+
180
+ # Not a benchmark. This asserts the ceilings actually bind under concurrency,
181
+ # because a quota that only holds in a single-threaded test is not a quota.
182
+ load:
183
+ runs-on: ubuntu-latest
184
+ timeout-minutes: 20
185
+ services:
186
+ postgres:
187
+ image: postgres:17
188
+ env:
189
+ POSTGRES_PASSWORD: postgres
190
+ POSTGRES_DB: guard_load
191
+ options: >-
192
+ --health-cmd "pg_isready -U postgres"
193
+ --health-interval 5s --health-timeout 5s --health-retries 10
194
+ ports: ["5432:5432"]
195
+ env:
196
+ GUARD_DATABASE_URL: postgresql+psycopg://postgres:postgres@localhost:5432/guard_load
197
+ GUARD_AUDIT_KEY: ci-only-not-a-secret-at-least-32-bytes-long
198
+ steps:
199
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
200
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
201
+ with: { enable-cache: true }
202
+ - run: uv sync --locked --dev --all-extras
203
+ - run: uv run boundedllm migrate
204
+ - run: uv run python scripts/load_check.py
@@ -0,0 +1,62 @@
1
+ # What must never reach a commit, and why.
2
+ #
3
+ # This is a security package, so the cost of a mistake here is asymmetric: a
4
+ # missing cache entry is noise, a committed key is an incident. Patterns are
5
+ # grouped by consequence rather than by tool, and the secret group is first.
6
+
7
+ # --- Credentials and key material -------------------------------------------
8
+ # .env.example is the committed template and is deliberately excluded from the
9
+ # exclusion. Everything else shaped like an environment file stays local.
10
+ .env
11
+ .env.*
12
+ !.env.example
13
+ *.pem
14
+ *.key
15
+ *.p12
16
+ *.pfx
17
+ # Certificate stores exported for local troubleshooting.
18
+ *.sst
19
+ # The lab's development pepper. It keeps local password hashes and sessions
20
+ # valid across restarts, which makes it a credential even though it is synthetic.
21
+ .roadshield-dev-secret
22
+
23
+ # --- Local databases --------------------------------------------------------
24
+ # These hold seeded customer records, conversations, and the signed audit
25
+ # ledger. Synthetic today, but the habit of committing them is the problem.
26
+ *.db
27
+ *.db-shm
28
+ *.db-wal
29
+
30
+ # --- Environments and caches ------------------------------------------------
31
+ .venv/
32
+ venv/
33
+ .uv-cache/
34
+ __pycache__/
35
+ *.py[cod]
36
+ .pytest_cache/
37
+ .ruff_cache/
38
+ .mypy_cache/
39
+
40
+ # --- Build output -----------------------------------------------------------
41
+ dist/
42
+ build/
43
+ *.egg-info/
44
+ .dist-check/
45
+
46
+ # --- Generated by CI, never committed ---------------------------------------
47
+ # The workflow regenerates these on every run. A stale copy in the repository
48
+ # would look authoritative while describing an older dependency set.
49
+ requirements.lock.txt
50
+ sbom*.json
51
+ results.sarif
52
+ gitleaks-report.json
53
+ .coverage
54
+ htmlcov/
55
+
56
+ # --- Working notes ----------------------------------------------------------
57
+ # A personal design document and its plain-text extraction. Kept local: it was
58
+ # written as private notes rather than for readers, and it describes the
59
+ # pre-0.3.0 architecture, so publishing it would point people at a design this
60
+ # package no longer has. README.md is the authoritative description.
61
+ llm-security-field-guide.html
62
+ guide-extracted.txt
@@ -0,0 +1,67 @@
1
+ # Secret-scanning rules, extending the gitleaks defaults.
2
+ #
3
+ # The defaults were verified against synthetic high-entropy credentials and do
4
+ # detect AWS, GitHub, Stripe, and Slack tokens. They do **not** detect an
5
+ # Anthropic API key: as of gitleaks v8.30.1 there is no rule for the
6
+ # `sk-ant-...` format, and the `generic-api-key` heuristic did not fire on it
7
+ # either, though it did fire on an OpenAI key in the same file.
8
+ #
9
+ # That matters more here than it would elsewhere. This repository ships an
10
+ # example application wired to Claude, so an Anthropic key in a `.env` is the
11
+ # single most likely credential to be committed by accident. Relying on the
12
+ # default ruleset would have left exactly that case unscanned.
13
+ #
14
+ # The provider rules below are written as explicit prefixes rather than entropy
15
+ # heuristics, because a prefix match has no false-negative cliff at short
16
+ # lengths and these formats are unambiguous.
17
+
18
+ [extend]
19
+ useDefault = true
20
+
21
+ [[rules]]
22
+ id = "anthropic-api-key"
23
+ description = "Anthropic API key (sk-ant-...)"
24
+ # Covers both the standard api keys and admin keys, whose middle segment differs.
25
+ regex = '''sk-ant-(?:api|admin)[0-9]{0,3}-[A-Za-z0-9_\-]{32,200}'''
26
+ keywords = ["sk-ant-"]
27
+
28
+ [[rules]]
29
+ id = "openai-api-key-explicit"
30
+ description = "OpenAI API key (sk-... / sk-proj-...)"
31
+ # The default config catches this only via the generic heuristic, which depends
32
+ # on the surrounding assignment. An explicit rule removes that dependency.
33
+ regex = '''sk-(?:proj-)?[A-Za-z0-9_\-]{32,200}'''
34
+ keywords = ["sk-proj-", "sk-"]
35
+
36
+ [[rules]]
37
+ id = "guard-audit-key"
38
+ description = "This package's own audit signing key"
39
+ # GUARD_AUDIT_KEY signs the tamper-evident ledger. A leaked one lets an attacker
40
+ # forge audit records, so it is a credential in its own right even though it has
41
+ # no vendor prefix to match on.
42
+ regex = '''(?i)GUARD_AUDIT(?:_PSEUDONYM)?_KEY\s*[=:]\s*["']?([A-Za-z0-9_\-+/=]{32,})'''
43
+ keywords = ["GUARD_AUDIT_KEY", "GUARD_AUDIT_PSEUDONYM_KEY"]
44
+
45
+ [allowlist]
46
+ description = "Documented placeholders and test fixtures that are not credentials"
47
+ paths = [
48
+ # Dependency caches and build output are not part of the repository.
49
+ '''\.venv/''',
50
+ '''\.uv-cache/''',
51
+ '''\.dist-check/''',
52
+ '''uv\.lock''',
53
+ ]
54
+ regexes = [
55
+ # The committed example file and the docs deliberately show the shape of a
56
+ # secret without containing one.
57
+ '''replace-with-at-least-32-random-bytes''',
58
+ '''replace-me-32-bytes-minimum-secret''',
59
+ '''ci-only-not-a-secret-at-least-32-bytes-long''',
60
+ '''old-key-at-least-32-bytes-long''',
61
+ '''test-secret-never-logged''',
62
+ # tests/test_adversarial.py asserts that secret-shaped model output is blocked
63
+ # before release. The fixture has to look like a credential for the test to
64
+ # mean anything. Allowlisted by exact value rather than by exempting the tests
65
+ # directory, so a genuine credential committed there would still be reported.
66
+ '''sk-live-4f9a2b7c1d8e6f3a0b5c9d2e''',
67
+ ]