Skip to content

Commit 645255b

Browse files
authored
Merge pull request #1 from Floe-Labs/feat/initial-floe-guard
Week-1 MLV: floe-guard local budget guardrail
2 parents 2595c6b + e97ad6b commit 645255b

21 files changed

Lines changed: 2027 additions & 0 deletions

.gitignore

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*.egg-info/
5+
.eggs/
6+
build/
7+
dist/
8+
.venv/
9+
venv/
10+
11+
# Tooling
12+
.pytest_cache/
13+
.ruff_cache/
14+
.mypy_cache/
15+
16+
# OS / editor
17+
.DS_Store
18+
.vscode/
19+
.idea/

CONTRIBUTING.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Contributing to floe-guard
2+
3+
`floe-guard` is a local budget guardrail for AI agents: it hard-stops an agent
4+
before its next LLM call when it would cross a spend ceiling. It runs in-process
5+
with no account. Hosted Floe is the upgrade path — enforcement moves server-side
6+
so the ceiling becomes un-bypassable and cross-vendor.
7+
8+
Contributions are welcome.
9+
10+
## Development setup
11+
12+
```bash
13+
git clone https://github.com/Floe-Labs/floe-guard.git
14+
cd floe-guard
15+
pip install -e ".[dev]"
16+
```
17+
18+
Run the checks before opening a PR:
19+
20+
```bash
21+
pytest # tests
22+
ruff check . # lint
23+
ruff format . # format
24+
```
25+
26+
The optional adapters need their extras to run/import:
27+
28+
```bash
29+
pip install -e ".[crewai]" # CrewAI adapter
30+
pip install -e ".[litellm]" # LiteLLM adapter
31+
```
32+
33+
## Contribution flow
34+
35+
1. Fork the repo and create a branch off `main` (e.g. `feat/your-change`).
36+
2. Make your change with tests, and keep `pytest` + `ruff` green.
37+
3. Open a **draft pull request** against `main` and describe what changed and why.
38+
39+
## Code style
40+
41+
- Python 3.10+ with type hints.
42+
- Formatting and linting are handled by `ruff` (config in `pyproject.toml`) — run
43+
`ruff format .` and `ruff check .` before pushing.
44+
- Keep the core (`src/floe_guard/`) dependency-free; framework integrations belong
45+
in `src/floe_guard/integrations/` behind an optional extra.
46+
- Prefer small, focused changes with tests that describe behavior.
47+
48+
## Areas we'd love help with
49+
50+
- **LangChain adapter** (callback-based enforcement).
51+
- **Vercel AI SDK adapter** (TypeScript middleware / port).
52+
- **Cost-map coverage** — keeping the bundled pricing map current and broad.
53+
54+
Open an issue first if you want to discuss a larger change.

README.md

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# floe-guard
2+
3+
**A local budget guardrail for AI agents.** It hard-stops your agent *before its
4+
next LLM call* when it would cross a spend ceiling — so a runaway loop dies at
5+
$0.10 instead of $4,000. No account, no signup, no network. Runs in your process.
6+
7+
```bash
8+
pip install floe-guard
9+
```
10+
11+
```python
12+
from floe_guard import BudgetGuard
13+
14+
guard = BudgetGuard(limit_usd=5.00) # your ceiling
15+
guard.check() # before each LLM call — raises if it'd cross
16+
response = call_your_llm(...) # your existing call
17+
guard.record("gpt-4o", response.usage.prompt_tokens, response.usage.completion_tokens)
18+
```
19+
20+
When the next call would cross the ceiling, the guard raises `BudgetExceeded` and
21+
prints:
22+
23+
```
24+
BUDGET EXCEEDED — call blocked
25+
spent so far: $5.001250 | ceiling: $5.000000
26+
The next call would cross your budget; floe-guard stopped your agent before it ran.
27+
```
28+
29+
_Animated demo coming — run `python examples/runaway_loop.py` to watch it stop a loop live._
30+
<!-- TODO: record docs/stop-the-loop.gif and restore the embed: ![stop the loop](docs/stop-the-loop.gif) -->
31+
32+
## See it stop a loop (no API key needed)
33+
34+
```bash
35+
python examples/runaway_loop.py
36+
```
37+
38+
This rigs a loop against a **stub LLM** — no real API key, no account, no network.
39+
It prices each fake `gpt-4o` call offline and the guard halts the loop after a few
40+
iterations. This is the reproducible "stop the loop" demo.
41+
42+
## How it works
43+
44+
The guard sits **in the call path**, not on an event bus. A passive listener is
45+
told about spend *after the fact* and can't halt anything — so enforcement has to
46+
be the thing standing in front of the next call:
47+
48+
- **`check()`** runs before each LLM call. It predicts the next call's cost from
49+
the last one and raises `BudgetExceeded` if that would cross your ceiling — the
50+
call never runs. (A running-total check also catches an overshoot if an estimate
51+
came in low.)
52+
- **`record(model, prompt_tokens, completion_tokens)`** runs after each response.
53+
It prices the tokens **offline** from a bundled
54+
[LiteLLM cost map](src/floe_guard/cost_map.json) and adds the USD to a running
55+
total.
56+
57+
### Unpriceable models fail closed
58+
59+
If a model isn't in the cost map and you didn't supply a price, the guard **warns
60+
loudly and refuses** (`UnpriceableModelError`) rather than silently treat it as
61+
free — *you can't cap spend you can't measure.* Give it a price to enforce it:
62+
63+
```python
64+
from floe_guard import BudgetGuard, ManualPrice
65+
66+
guard = BudgetGuard(
67+
limit_usd=5.00,
68+
price_overrides={"my-self-hosted-model": ManualPrice(1e-6, 2e-6)}, # USD/token
69+
)
70+
# or, set fail_closed=False to warn-and-skip for models you accept un-metered.
71+
```
72+
73+
## Framework adapters (optional extras)
74+
75+
### CrewAI
76+
77+
```bash
78+
pip install floe-guard[crewai]
79+
```
80+
81+
```python
82+
from crewai import Crew
83+
from floe_guard import BudgetGuard
84+
from floe_guard.integrations.crewai import guard_crew
85+
86+
guard = BudgetGuard(limit_usd=1.00)
87+
guard_crew(guard) # one line — enforces across the whole crew
88+
Crew(agents=[...], tasks=[...]).kickoff()
89+
```
90+
91+
CrewAI runs on LiteLLM, so one callback caps every agent and task under a single
92+
budget.
93+
94+
### LiteLLM
95+
96+
```bash
97+
pip install floe-guard[litellm]
98+
```
99+
100+
```python
101+
from floe_guard import BudgetGuard
102+
from floe_guard.integrations.litellm import guarded_completion
103+
104+
guard = BudgetGuard(limit_usd=1.00)
105+
response = guarded_completion(guard, model="gpt-4o", messages=[...])
106+
```
107+
108+
Prefer the LiteLLM-native callback? Register `budget_guard_callback(guard)` on
109+
`litellm.callbacks`.
110+
111+
### Coming next
112+
113+
LangChain (callback) and the Vercel AI SDK (TypeScript middleware) are next. Open
114+
an issue if you want one sooner.
115+
116+
## Honest about what this is
117+
118+
floe-guard is a **local, estimate-based** guardrail. It prices tokens from a
119+
vendored cost map *inside your process*:
120+
121+
- The cost map can drift as vendors change prices — refresh it like any snapshot.
122+
- It only sees the vendors you instrument.
123+
- A determined agent or a bug could route around an in-process check.
124+
125+
It's genuinely useful on its own, and it's honest about its limits. No inflated
126+
metrics, no "zero defaults" claims — it's a free local stop, not a vault.
127+
128+
## Upgrade to hosted Floe
129+
130+
When you need the ceiling to be **un-bypassable** and **cross-vendor**, hosted
131+
Floe moves enforcement server-side against a real credit line:
132+
133+
- **Un-bypassable** — enforced at the spend rail, not in your process.
134+
- **Cross-vendor** — one budget over LLM tokens *and* paid (x402) tool calls.
135+
- **Team budgets + analytics** — shared ceilings, per-agent isolation, spend history.
136+
137+
Set `FLOE_API_KEY` and floe-guard exposes a hook to delegate enforcement to
138+
hosted Floe (see [`src/floe_guard/hosted.py`](src/floe_guard/hosted.py) — wiring
139+
the live endpoint is in progress; the local guard is fully functional today).
140+
141+
**[dev-dashboard.floelabs.xyz](https://dev-dashboard.floelabs.xyz)** ·
142+
**[floelabs.xyz](https://floelabs.xyz)**
143+
144+
## Development
145+
146+
```bash
147+
pip install -e ".[dev]"
148+
pytest
149+
ruff check .
150+
```
151+
152+
## License
153+
154+
MIT — see [LICENSE](LICENSE).

SECURITY.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Security Policy
2+
3+
## Supported versions
4+
5+
Only the latest released version of `floe-guard` is supported with security fixes.
6+
Please upgrade to the latest version before reporting an issue.
7+
8+
## Reporting a vulnerability
9+
10+
**Do not open a public GitHub issue for security vulnerabilities.**
11+
12+
Report them privately by email to **security@floelabs.xyz**. If you do not get a
13+
response, use **hello@floelabs.xyz** as a fallback contact.
14+
15+
Please include:
16+
17+
- a description of the vulnerability and its impact,
18+
- steps to reproduce (a minimal proof of concept helps),
19+
- the affected version.
20+
21+
## What to expect
22+
23+
- We aim to acknowledge your report within **3 business days**.
24+
- We will keep you updated on our assessment and the fix.
25+
- Please give us a reasonable window to release a fix before any public disclosure.
26+
27+
Thank you for helping keep `floe-guard` and its users safe.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
TODO: record this GIF.
2+
3+
This is a placeholder for docs/stop-the-loop.gif — a short screen recording of
4+
`python examples/runaway_loop.py` running and the guard printing
5+
`BUDGET EXCEEDED — call blocked`. Replace this file with the recorded .gif
6+
(keep the filename so the README reference resolves).

examples/runaway_loop.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""Stop-the-loop demo — runs with NO API key and NO account.
2+
3+
A naive agent loop that calls an LLM forever. The only thing standing between you
4+
and a five-figure overnight bill is ``floe-guard``: it hard-stops the loop before
5+
the call that would cross your $0.10 ceiling.
6+
7+
Run it::
8+
9+
python examples/runaway_loop.py
10+
11+
The "LLM" here is a stub that returns fixed token usage — no network, no key, no
12+
crewai/litellm needed. The cost is computed offline from the bundled cost map,
13+
exactly as it would be for a real ``gpt-4o`` call.
14+
"""
15+
16+
from __future__ import annotations
17+
18+
from floe_guard import BudgetExceeded, BudgetGuard
19+
20+
MODEL = "gpt-4o"
21+
22+
23+
def stub_llm(prompt: str) -> dict[str, object]:
24+
"""A fake LLM call. No network, no API key — returns fixed token usage."""
25+
return {
26+
"model": MODEL,
27+
"text": "...thinking... let me call myself again...",
28+
"prompt_tokens": 1_000,
29+
"completion_tokens": 1_000,
30+
}
31+
32+
33+
def main() -> None:
34+
# $0.10 ceiling. gpt-4o at 1k in + 1k out ≈ $0.0125/call, so the guard should
35+
# stop the loop after a handful of iterations — well before any real damage.
36+
guard = BudgetGuard(limit_usd=0.10)
37+
38+
print(f"Starting a runaway loop with a ${guard.limit_usd:.2f} budget...\n")
39+
call = 0
40+
while True: # a real runaway loop never decides to stop on its own
41+
call += 1
42+
try:
43+
guard.check() # <-- the kill-switch: raises before the crossing call
44+
except BudgetExceeded:
45+
print(f"\nLoop stopped at call #{call}. The agent never got to spend past the budget.")
46+
break
47+
48+
response = stub_llm("keep going")
49+
cost = guard.record(
50+
str(response["model"]),
51+
int(response["prompt_tokens"]), # type: ignore[arg-type]
52+
int(response["completion_tokens"]), # type: ignore[arg-type]
53+
)
54+
print(f" call #{call}: +${cost:.4f} (running total ${guard.spent_usd:.4f})")
55+
56+
57+
if __name__ == "__main__":
58+
main()

pyproject.toml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "floe-guard"
7+
version = "0.1.0"
8+
description = "Local budget guardrail for AI agents — hard-stops a runaway loop before its next LLM call crosses a spend ceiling. No account, no network."
9+
readme = "README.md"
10+
requires-python = ">=3.10"
11+
license = { file = "LICENSE" }
12+
authors = [{ name = "Floe Labs" }]
13+
keywords = ["llm", "agents", "budget", "guardrail", "crewai", "litellm", "cost", "openai", "anthropic"]
14+
classifiers = [
15+
"Development Status :: 4 - Beta",
16+
"Intended Audience :: Developers",
17+
"License :: OSI Approved :: MIT License",
18+
"Programming Language :: Python :: 3",
19+
"Programming Language :: Python :: 3.10",
20+
"Programming Language :: Python :: 3.11",
21+
"Programming Language :: Python :: 3.12",
22+
"Topic :: Software Development :: Libraries :: Python Modules",
23+
]
24+
dependencies = []
25+
26+
[project.optional-dependencies]
27+
litellm = ["litellm>=1.0"]
28+
crewai = ["crewai>=0.30", "litellm>=1.0"]
29+
dev = ["pytest>=7.0", "ruff>=0.4"]
30+
31+
[project.urls]
32+
Homepage = "https://floelabs.xyz"
33+
Dashboard = "https://dev-dashboard.floelabs.xyz"
34+
Source = "https://github.com/Floe-Labs/floe-guard"
35+
36+
[tool.hatch.build.targets.wheel]
37+
packages = ["src/floe_guard"]
38+
39+
[tool.hatch.build.targets.wheel.force-include]
40+
"src/floe_guard/cost_map.json" = "floe_guard/cost_map.json"
41+
42+
[tool.ruff]
43+
line-length = 100
44+
target-version = "py310"
45+
src = ["src", "tests", "examples"]
46+
47+
[tool.ruff.lint]
48+
select = ["E", "F", "I", "UP", "B", "W"]
49+
50+
[tool.pytest.ini_options]
51+
testpaths = ["tests"]
52+
filterwarnings = ["error::floe_guard.errors.UnpriceableModelWarning"]

0 commit comments

Comments
 (0)