Skip to content

Latest commit

 

History

13 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

seekrit — Python SDK

Read-path SDK for seekrit. Authenticate with a service token, resolve your environment, and get decrypted secrets — the API only ever returns ciphertext; decryption happens in your process.

This repo is a read-only mirror published from seekrit's monorepo so the code that holds your token and decrypts plaintext is auditable. Don't commit here — it's overwritten on each sync. Issues and PRs welcome.

Install

pip install seekrit

Requires Python 3.9+. The only dependency is cryptography.

Usage

import seekrit

client = seekrit.Client()            # token from $SEEKRIT_TOKEN
secrets = client.resolve()           # {"DATABASE_URL": "postgres://…", …}

db_url = client.get("DATABASE_URL")
api_key = client.get("API_KEY", default="")

Load everything into the process environment:

import os, seekrit
seekrit.Client().into_env()          # existing os.environ vars win by default
print(os.environ["DATABASE_URL"])

Configuration

Argument Env var Default
token SEEKRIT_TOKEN — (required)
api_url SEEKRIT_API_URL https://api.seekrit.dev
overrides {}
timeout 30.0 (seconds)

A service token binds to a single app environment (plus its composed group slices). To pull a different environment slice of a composed group, pass overrides (the ?with= override):

seekrit.Client(overrides={"shared": "dev"}).resolve()

Errors

  • SeekritApiError — non-2xx from the API; has .status and .code ("unauthorized", "forbidden", "not_found", …).
  • SeekritCryptoError — a token or ciphertext could not be parsed/decrypted.
  • SeekritError — base class (also covers network failures).

The client is fail-closed: any resolve or decrypt failure raises rather than returning partial results.

Notebooks

seekrit.load() is the one-call form: resolve, load os.environ, done. Put it at the top of a notebook or script.

import seekrit

seekrit.load()

It's built around the two ways a notebook leaks a credential:

  • No token in a cell. load() takes the token from $SEEKRIT_TOKEN, and when there isn't one it asks through a password prompt (ipykernel routes getpass to the notebook frontend) — so the token stays in kernel memory instead of being saved into the .ipynb. Pass prompt=False to never ask, or set SEEKRIT_TOKEN for headless runs like papermill.
  • No values in cell outputs. load() returns the names it loaded and the scope they came from — never the values — so displaying it in a cell writes a summary into the notebook file and nothing more.
loaded = seekrit.load()
loaded                       # <seekrit: 7 secrets loaded from acme/analytics/staging: API_KEY, …>
len(loaded)                  # 7
"DATABASE_URL" in loaded     # True
os.environ["DATABASE_URL"]   # the value lives here, not on the result

Re-running the cell refreshes: load() defaults to override=True, unlike into_env(), so a rotated secret takes effect on a re-run rather than being skipped as already-set. Pass override=False to keep what the environment already has (those names are then listed in loaded.skipped).

This guards the summary, not your own cells — print(os.environ["API_KEY"]) still writes a secret into the notebook. Strip outputs before committing.

Hold a placeholder instead of a key

seekrit.transport substitutes {{seekrit:NAME}} placeholders into outbound requests, so a provider key is never in your source, your .env, or os.environ:

pip install 'seekrit[httpx]'
import httpx
from openai import OpenAI
from seekrit.transport import SeekritTransport

client = OpenAI(
    api_key="{{seekrit:OPENAI_API_KEY}}",
    http_client=httpx.Client(
        transport=SeekritTransport(allow={"api.openai.com": ["OPENAI_API_KEY"]}),
    ),
)

One transport covers every Python agent toolkit, because they all reach the network through the same http_client=: LangChain's ChatOpenAI, Pydantic AI's OpenAIProvider, the OpenAI Agents SDK's set_default_openai_client, LlamaIndex's OpenAI. Use AsyncSeekritTransport for the async client.

The allowlist is the boundary, and it is default-deny: a name that is not permitted toward that host, method, and path is refused, and so is a name that did not resolve. Neither sends the request.

A refusal answers with the same 403 the proxy answers with, carrying x-seekrit-refusal and the secret's name but never its value. That is on purpose: a provider SDK wraps anything its HTTP layer raises into an opaque connection error and retries it, so raising would turn a denied placeholder into "Connection error" after six attempts. Pass refusal="raise" to get the typed error instead.

LangChain middleware

pip install 'seekrit[langchain]' adds agent middleware that scopes credentials to a single tool call:

from langchain.agents import create_agent
from seekrit.langchain import SeekritCredentials

agent = create_agent(
    model=model,
    tools=[refund, search],
    context_schema=Context,
    middleware=[
        SeekritCredentials(
            scope=lambda ctx: {"tenants": ctx.tenant},
            tools={"refund": ["STRIPE_SECRET_KEY"]},
        ),
    ],
)

refund may substitute the Stripe key; search may substitute nothing. scope also picks which tenant's secrets to resolve, per request, without rebuilding the model. Pair it with require_scope=True on the transport so a lost context fails closed. Details: https://seekrit.dev/docs/guides/agent-proxy/in-process.

Pydantic AI

pip install 'seekrit[pydantic-ai]' adds a WrapperToolset that scopes each tool call:

from pydantic_ai import Agent
from pydantic_ai.toolsets import FunctionToolset
from seekrit.pydantic_ai import SeekritToolset, Scope, use_scope

agent = Agent(
    "openai:gpt-5.6-terra",
    deps_type=Deps,
    toolsets=[
        SeekritToolset(
            FunctionToolset([refund, search]),
            scope=lambda deps: {"tenants": deps.tenant},
            tools={"refund": ["STRIPE_SECRET_KEY"]},
        )
    ],
)

# A toolset only wraps tools; wrap the run to cover the model call too.
with use_scope(Scope(overrides={"tenants": tenant})):
    result = await agent.run(prompt, deps=Deps(tenant=tenant))

Because it runs in your process, this is a weaker boundary than the egress proxy. What it does buy: the value exists only inside one HTTP call, so it never reaches model context, a tool result, or a trace exporter — nor an environment-scraping bug in a dependency.

Hermes Agent

Installing this package registers two secret sources for Hermes Agent, so the agent's provider credentials arrive from seekrit at startup instead of sitting in ~/.hermes/.env. Both are inert until a secrets: section enables one.

secrets:
  sources: [seekrit]
  seekrit:
    enabled: true

seekrit is the bulk source — one environment, whole. seekrit_refs is the mapped one, for explicit VAR: skt://NAME bindings, renames, and reading more than one environment. Full guide: seekrit.dev/docs/guides/ai-agents/hermes.

Secret references

A secret's value may reference another with ${OTHER_SECRET}. References are stored literally and expanded here, after the layers are merged — so a reference picks up whichever layer won that name, and rotating the referenced secret updates every value that uses it. $${OTHER_SECRET} is a literal; an unknown name is left as written; a reference cycle raises. Full rules: seekrit.dev/docs/guides/references.

client = seekrit.Client(interpolate=False)   # get the stored text instead

Zero-knowledge

GET /v1/resolve returns ciphertext plus a data-encryption key wrapped to your token's public key. This SDK recovers the token's private key, unwraps the DEK (ECDH P-256 → HKDF-SHA256 → AES-256-GCM), and decrypts each secret (AES-256-GCM, AAD-bound to environmentId/NAME) — the exact scheme used by the CLI, seekrit run, and every other seekrit client. See seekrit.dev/docs.

License

MIT

About

Read-path Python SDK for seekrit — resolve and decrypt secrets with a service token. Ships httpx transport plus LangChain and Pydantic AI adapters.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages