Skip to content

Commit f257b5e

Browse files
author
seekrit-bot
committed
sync: mileszim/seekrit@b2ab672910dd22924cbb3b5133ff62d05ac841f1
1 parent f65d3e1 commit f257b5e

7 files changed

Lines changed: 1025 additions & 1 deletion

File tree

‎README.md‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,42 @@ with use_scope(Scope(overrides={"tenants": tenant})):
195195
result = await agent.run(prompt, deps=Deps(tenant=tenant))
196196
```
197197

198+
### CrewAI
199+
200+
`pip install 'seekrit[crewai]'` adds CrewAI's own interception contract, plus
201+
hooks that scope credentials per **agent** as well as per tool — the bound that
202+
matters when several agents share one process:
203+
204+
```python
205+
from crewai import LLM
206+
from seekrit.crewai import SeekritCredentials, SeekritInterceptor, ensure_intercepted
207+
208+
llm = ensure_intercepted(
209+
LLM(
210+
model="openai/gpt-5.6-terra",
211+
api_key="{{seekrit:OPENAI_API_KEY}}",
212+
interceptor=SeekritInterceptor(
213+
allow={
214+
"api.openai.com": ["OPENAI_API_KEY"],
215+
"api.stripe.com": ["STRIPE_SECRET_KEY"],
216+
},
217+
require_scope=True,
218+
),
219+
)
220+
)
221+
222+
with SeekritCredentials(
223+
agents={"Researcher": ["OPENAI_API_KEY"]},
224+
tools={"refund": ["OPENAI_API_KEY", "STRIPE_SECRET_KEY"]},
225+
):
226+
crew.kickoff()
227+
```
228+
229+
Both lists are exhaustive and are intersected, so the `Researcher` cannot reach
230+
Stripe even by calling `refund`, and an agent added later starts with nothing.
231+
`ensure_intercepted` is worth the line: a model string that routes to CrewAI's
232+
LiteLLM fallback accepts an interceptor and never calls it.
233+
198234
Because it runs in your process, this is a weaker boundary than the
199235
[egress proxy](https://seekrit.dev/docs/guides/agent-proxy). What it does buy:
200236
the value exists only inside one HTTP call, so it never reaches model context, a

‎pyproject.toml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ langchain = ["httpx>=0.24", "langchain>=1.0"]
3030
# `seekrit.pydantic_ai` — a WrapperToolset that scopes each tool call. Brings
3131
# httpx along for the same reason.
3232
pydantic-ai = ["httpx>=0.24", "pydantic-ai>=1.0"]
33+
# `seekrit.crewai` — a BaseInterceptor that substitutes into a CrewAI
34+
# provider's outbound request, plus hooks that scope credentials per agent and
35+
# per tool. httpx again: the interceptor rewrites an httpx.Request.
36+
crewai = ["httpx>=0.24", "crewai>=1.15"]
3337

3438
# `seekrit.hermes` — Hermes Agent secret sources. No extra: the module imports
3539
# Hermes lazily (only inside `register()`), so there is no third-party dependency

‎src/seekrit/_scope.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,19 @@ def use_scope(scope: Optional[Scope]) -> Iterator[None]:
5555
yield
5656
finally:
5757
_current.reset(token)
58+
59+
60+
def set_scope(scope: Optional[Scope]) -> None:
61+
"""Install ``scope`` with no matching reset, replacing whatever was there.
62+
63+
:func:`use_scope` is the right tool wherever the framework hands you the
64+
call to wrap. Some do not: CrewAI dispatches ``before_llm_call`` and
65+
``after_llm_call`` as two independent hooks with no ``finally`` between
66+
them, so there is nowhere to hold a ``contextvars.Token`` and nothing
67+
guarantees the closing hook runs at all. Setting unconditionally is what
68+
makes that safe — every ``before`` hook overwrites the previous value, so a
69+
scope stranded by a raising call narrows nothing it should not; the next
70+
call installs its own, and :mod:`seekrit.crewai` also clears at the end of a
71+
run. See that module for the whole argument.
72+
"""
73+
_current.set(scope)

0 commit comments

Comments
 (0)