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.
- boundedllm-0.4.0/.dockerignore +11 -0
- boundedllm-0.4.0/.env.example +41 -0
- boundedllm-0.4.0/.gitattributes +42 -0
- boundedllm-0.4.0/.github/workflows/release.yml +109 -0
- boundedllm-0.4.0/.github/workflows/security.yml +204 -0
- boundedllm-0.4.0/.gitignore +62 -0
- boundedllm-0.4.0/.gitleaks.toml +67 -0
- boundedllm-0.4.0/CHANGELOG.md +175 -0
- boundedllm-0.4.0/Dockerfile +54 -0
- boundedllm-0.4.0/LICENSE +202 -0
- boundedllm-0.4.0/OPERATIONS.md +179 -0
- boundedllm-0.4.0/PKG-INFO +373 -0
- boundedllm-0.4.0/QUICKSTART.md +202 -0
- boundedllm-0.4.0/README.md +309 -0
- boundedllm-0.4.0/SECURITY.md +101 -0
- boundedllm-0.4.0/pyproject.toml +110 -0
- boundedllm-0.4.0/scripts/load_check.py +218 -0
- boundedllm-0.4.0/src/boundedllm/__init__.py +86 -0
- boundedllm-0.4.0/src/boundedllm/adapters/__init__.py +6 -0
- boundedllm-0.4.0/src/boundedllm/adapters/http_model.py +45 -0
- boundedllm-0.4.0/src/boundedllm/adapters/otlp.py +104 -0
- boundedllm-0.4.0/src/boundedllm/adapters/sql/__init__.py +136 -0
- boundedllm-0.4.0/src/boundedllm/adapters/sql/alembic.ini +39 -0
- boundedllm-0.4.0/src/boundedllm/adapters/sql/migrations/env.py +59 -0
- boundedllm-0.4.0/src/boundedllm/adapters/sql/migrations/script.py.mako +24 -0
- boundedllm-0.4.0/src/boundedllm/adapters/sql/migrations/versions/20260916_71f5e126461a_baseline_schema.py +183 -0
- boundedllm-0.4.0/src/boundedllm/adapters/sql/schema.py +123 -0
- boundedllm-0.4.0/src/boundedllm/adapters/sql/store.py +1206 -0
- boundedllm-0.4.0/src/boundedllm/audit.py +156 -0
- boundedllm-0.4.0/src/boundedllm/authz.py +42 -0
- boundedllm-0.4.0/src/boundedllm/budget.py +20 -0
- boundedllm-0.4.0/src/boundedllm/cli.py +390 -0
- boundedllm-0.4.0/src/boundedllm/config.py +171 -0
- boundedllm-0.4.0/src/boundedllm/context.py +26 -0
- boundedllm-0.4.0/src/boundedllm/contrib/__init__.py +22 -0
- boundedllm-0.4.0/src/boundedllm/contrib/anthropic.py +122 -0
- boundedllm-0.4.0/src/boundedllm/contrib/ledger.py +181 -0
- boundedllm-0.4.0/src/boundedllm/contrib/pgvector.py +250 -0
- boundedllm-0.4.0/src/boundedllm/egress.py +103 -0
- boundedllm-0.4.0/src/boundedllm/engine.py +327 -0
- boundedllm-0.4.0/src/boundedllm/errors.py +33 -0
- boundedllm-0.4.0/src/boundedllm/identity.py +125 -0
- boundedllm-0.4.0/src/boundedllm/limits.py +83 -0
- boundedllm-0.4.0/src/boundedllm/main.py +200 -0
- boundedllm-0.4.0/src/boundedllm/middleware.py +87 -0
- boundedllm-0.4.0/src/boundedllm/model_gateway.py +52 -0
- boundedllm-0.4.0/src/boundedllm/models.py +122 -0
- boundedllm-0.4.0/src/boundedllm/network.py +27 -0
- boundedllm-0.4.0/src/boundedllm/normalize.py +32 -0
- boundedllm-0.4.0/src/boundedllm/output_firewall.py +87 -0
- boundedllm-0.4.0/src/boundedllm/parsing.py +26 -0
- boundedllm-0.4.0/src/boundedllm/ports.py +118 -0
- boundedllm-0.4.0/src/boundedllm/prompts.py +14 -0
- boundedllm-0.4.0/src/boundedllm/retrieval.py +39 -0
- boundedllm-0.4.0/src/boundedllm/risk.py +54 -0
- boundedllm-0.4.0/src/boundedllm/support/__init__.py +44 -0
- boundedllm-0.4.0/src/boundedllm/support/authz.py +16 -0
- boundedllm-0.4.0/src/boundedllm/support/gateway.py +48 -0
- boundedllm-0.4.0/src/boundedllm/support/models.py +49 -0
- boundedllm-0.4.0/src/boundedllm/support/policy.py +67 -0
- boundedllm-0.4.0/src/boundedllm/support/projections.py +8 -0
- boundedllm-0.4.0/src/boundedllm/support/schema.py +45 -0
- boundedllm-0.4.0/src/boundedllm/support/store.py +257 -0
- boundedllm-0.4.0/src/boundedllm/telemetry.py +82 -0
- boundedllm-0.4.0/src/boundedllm/tool_gateway.py +33 -0
- boundedllm-0.4.0/testApp/.env.example +11 -0
- boundedllm-0.4.0/testApp/.gitignore +26 -0
- boundedllm-0.4.0/testApp/README.md +121 -0
- boundedllm-0.4.0/testApp/knowledge/roadshield_auto_policy.md +81 -0
- boundedllm-0.4.0/testApp/pyproject.toml +42 -0
- boundedllm-0.4.0/testApp/roadshield/__init__.py +1 -0
- boundedllm-0.4.0/testApp/roadshield/agent.py +65 -0
- boundedllm-0.4.0/testApp/roadshield/attacks.py +137 -0
- boundedllm-0.4.0/testApp/roadshield/auth.py +76 -0
- boundedllm-0.4.0/testApp/roadshield/claude.py +109 -0
- boundedllm-0.4.0/testApp/roadshield/config.py +123 -0
- boundedllm-0.4.0/testApp/roadshield/documents.py +118 -0
- boundedllm-0.4.0/testApp/roadshield/knowledge.py +23 -0
- boundedllm-0.4.0/testApp/roadshield/main.py +492 -0
- boundedllm-0.4.0/testApp/roadshield/prompts.py +27 -0
- boundedllm-0.4.0/testApp/roadshield/schema.py +83 -0
- boundedllm-0.4.0/testApp/roadshield/seed.py +217 -0
- boundedllm-0.4.0/testApp/roadshield/static/app.js +288 -0
- boundedllm-0.4.0/testApp/roadshield/static/index.html +85 -0
- boundedllm-0.4.0/testApp/roadshield/static/styles.css +6 -0
- boundedllm-0.4.0/testApp/roadshield/store.py +338 -0
- boundedllm-0.4.0/testApp/roadshield-claims-update-poisoned.pdf +74 -0
- boundedllm-0.4.0/testApp/run.py +49 -0
- boundedllm-0.4.0/testApp/scripts/create_attack_pdf.py +150 -0
- boundedllm-0.4.0/testApp/scripts/run_live_attacks.py +68 -0
- boundedllm-0.4.0/testApp/tests/test_app.py +444 -0
- boundedllm-0.4.0/testApp/tests/test_claude.py +116 -0
- boundedllm-0.4.0/testApp/uv.lock +903 -0
- boundedllm-0.4.0/tests/conftest.py +14 -0
- boundedllm-0.4.0/tests/harness.py +122 -0
- boundedllm-0.4.0/tests/test_adversarial.py +986 -0
- boundedllm-0.4.0/tests/test_contrib.py +335 -0
- boundedllm-0.4.0/tests/test_documentation.py +79 -0
- boundedllm-0.4.0/tests/test_money_concurrency.py +323 -0
- boundedllm-0.4.0/tests/test_postgres.py +316 -0
- boundedllm-0.4.0/tests/test_security.py +750 -0
- boundedllm-0.4.0/uv.lock +2757 -0
|
@@ -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
|
+
]
|