Enkrypt AI Python SDK
A Python SDK with Guardrails, Code of Conduct Policies, Endpoints (Models), Deployments, AI Proxy, Datasets, Red Team, Skill Scanner, etc. functionality for API interactions.
See documentation at https://docs.enkryptai.com/libraries/python/introduction
See https://pypi.org/project/enkryptai-sdk
Start a red team run
import os
from enkryptai_sdk import RedTeamClient, RTModelConfig, RTRedteamRequest
client = RedTeamClient(api_key=os.environ["ENKRYPTAI_API_KEY"])
run = client.run_redteam(RTRedteamRequest(
target=RTModelConfig.hosted(
endpoint="https://api.openai.com/v1/chat/completions",
api_key=os.environ["OPENAI_API_KEY"],
model_name="gpt-4o",
),
risk_categories={"safety_harm": {"attack_config": ["basic"]}},
run_name="nightly probe",
))
print(run.run_id) # rt-<uuid> -- save this
print(client.run_url(run.run_id)) # watch it in the dashboard
A run takes anywhere from about half an hour to several hours, so starting it
and reading it are usually different sittings. run_id is all you need to pick
it back up from a fresh process:
status = client.wait_for_run(run.run_id, on_progress=print)
report = client.get_run_results(run.run_id)
wait_for_run has no timeout by default and interrupting it does nothing to
the run. For a live feed instead of polling, client.iter_run_events(run_id)
yields decoded events and reconnects on its own, resuming where it left off —
which matters, because a run that lasts hours will outlive its connection.
Jailbreak benchmark run
run_jailbreak (or run_jailbreak_with_saved_model for a saved model) starts
a red team run over the fixed 60-prompt jailbreak_bench dataset. The body is
just the target and an optional run_name; the attacks are picked by the
service from the target's modalities, and the run is read back like any other:
from enkryptai_sdk import RTJailbreakRequest
run = client.run_jailbreak(RTJailbreakRequest(
target=RTModelConfig.hosted(...), # any target, as above
run_name="jailbreak bench",
))
# or, against a saved model (no target in the body):
run = client.run_jailbreak_with_saved_model(None, "my-model", "v1")
One run, three id spellings.
rt-<uuid>is what the endpoints want; the bare uuid (job_id_for(run_id), also onstatus.job_id) is what relay logs and compliance reports use. And a finished run reportsFinishedfrom one endpoint andcompletedfrom another —status.stateandstatus.is_terminalfold both into one.
Against a model on your own machine
Same script, one different target — plus a bridge (see below):
target=RTModelConfig.via_relay(
bridge_id="my-laptop",
endpoint="http://localhost:11434/v1/chat/completions",
model_name="llama3",
),
From the command line
pip install enkryptai-sdk also installs an enkryptai command:
export ENKRYPTAI_API_KEY=<your-enkrypt-api-key>
enkryptai redteam run --endpoint https://api.openai.com/v1/chat/completions \
--model gpt-4o --provider openai --target-api-key env:OPENAI_API_KEY \
--category safety_harm --attack basic
enkryptai redteam logs rt-... # the engine's own log for a run
run submits, waits, and prints the results summary. Pass --no-wait to
return as soon as the run is queued. The target comes from exactly one of:
--relay [--bridge-id ID] |
a model, chatbot or agent behind a relay bridge |
--endpoint URL --model NAME |
an endpoint Enkrypt can reach directly |
--saved-model NAME[:VERSION] |
a model saved on the platform |
target: in --config |
any target, written out in full |
Flags cover the common settings (--category, --attack, --max-prompts,
--comprehensiveness, --run-name). Everything else (sub-categories,
context, seed prompts, tools, modalities) goes in a JSON or YAML file passed
with --config; its redteam: section is the same body RTRedteamRequest
builds. Flags override the file. --dump-config prints the effective
config and sends nothing, so you can start from flags and save the result
as a file:
enkryptai redteam run --relay --category security_fraud --attack tap --dump-config > run.json
enkryptai redteam run --config run.json
Note: target flags cover the common fields:
--model,--system-type,--system-prompt,--tools, plus--providerand--target-api-keyfor a direct endpoint. For any other target field, such asinput_modalities,output_modalitiesor headers your endpoint needs, write the fulltarget:in--config. Starting from--dump-configoutput saves typing it out.
Keys never go in the file: write env:VAR and export VAR. YAML needs
pip install "enkryptai-sdk[yaml]".
Relay bridge
The SDK also ships the Enkrypt Sentry Relay bridge: a tiny
in-network process that lets red-team jobs running in Enkrypt's cloud
reach an LLM that lives inside your private network -- without opening
any inbound ports. The bridge is pure network plumbing: it does no
LLM work itself, just maintains one outbound WSS connection to
api.enkryptai.com:443, receives OpenAI-shaped chat.completions
requests over it, forwards them to your local LLM, and pushes the
response back. (Industry analogues for the same role are Twingate /
Zscaler Connector and Cloudflare's tunnel.)
Try it in two minutes
You need only an Enkrypt API key; the demo brings its own stand-in model.
pip install enkryptai-sdk
export ENKRYPTAI_API_KEY=<your-enkrypt-api-key>
enkryptai-relay demo
This starts a stand-in model on 127.0.0.1 and a bridge in front of it,
then waits until Enkrypt reports the bridge connected. In a second
terminal:
enkryptai redteam run --relay
That runs the smallest campaign (safety_harm, basic, one prompt, Low) through the bridge. The first terminal prints each attack as it reaches the model, and the second prints the results summary:
status: finished
summary: {"overall_asr": 0, "total_attempts": 40, ...}
enkryptai-relay demo --redteam does both in one terminal and exits.
Your own model, chatbot or agent
The target can be anything that accepts and returns OpenAI
chat.completions payloads: a model server (Ollama, vLLM, LM Studio,
llama.cpp), your own chatbot backend, or an agent. If it speaks a different
shape, a translation hook can
convert it.
# A model server: /v1/chat/completions is added for you
enkryptai-relay connect --port 11434
# A chatbot or agent on its own route: the path is used as given
enkryptai-relay connect --target http://localhost:8000/agent/chat --system-type agent
enkryptai redteam run --relay --category safety_harm --attack basic --max-prompts 5
# an agent also needs the tools it can call: add --tools tools.json
connect takes the model name from GET /v1/models, and --model sets it
yourself. An endpoint without /v1/models (typical for agents) gets
"default" in the payload's model field. --system-type
(foundation_model, chatbot or agent) tells the run what it is
testing. An agent also needs its tools listed (--tools FILE, a JSON/YAML
list of {name, description, parameters}, or tools: in the config
file). The bridge is limited to that host, and a bridge id is generated
if you don't give one. It prints the full --bridge-id ...
command for starting runs from another machine. redteam run --relay finds
the running bridge through ~/.enkryptai/relay/current.json, which
connect and demo write while they run, along with the endpoint, model
name and system type.
Note: a real chatbot or agent often needs more than these flags set, for example
input_modalitiesfor image input, or headers your endpoint checks. Write the full relay target in--configinstead of usingtarget: relay. The shape is in Turning the relay on for a run; headers go inmetadata.relay.target_headers, asenv:VARfor secrets. A literalAuthorization,X-API-Key,Cookieor other credential header in the file is refused; the variable holds the whole value,Bearer ...included.
Both commands can share one config file, each reading its own section:
relay:
bridge_id: acme-support-bot
target: http://127.0.0.1:8000/agent/chat
system_type: agent
tools:
- {name: lookup_order, description: Fetch an order by id}
redteam:
target: relay # the endpoint in `relay:` above
risk_categories:
security_fraud: {attack_config: {basic: {}, tap: {}}}
enkryptai-relay connect --config enkrypt.yaml
enkryptai redteam run --config enkrypt.yaml
A commented template ships with the SDK: copy_example('enkrypt.example.yaml', './enkrypt.yaml') (see Translation hooks
for copy_example).
Running the bridge as a service
export ENKRYPT_API_KEY=<your-enkrypt-api-key>
enkryptai-relay --bridge-id my-laptop --target http://localhost:11434
Two required values, and no user id: the gateway authenticates your API
key and tells the relay whose bridge this is. The bridge id is the value
you also pass as bridge_id on the red-team target — it is the one value
the two sides must agree on.
Every flag has an environment-variable equivalent, which is what you want under systemd, docker or k8s. Flags win when both are set:
export RELAY_BRIDGE_ID=my-laptop
export ENKRYPT_API_KEY=<your-enkrypt-api-key>
export TARGET_BASE_URL=http://localhost:11434 # your local LLM
# Optional:
# export BRIDGE_HOOKS_MODULE=my_company.relay_hooks # custom translation
# export RELAY_TARGET_ALLOWED_HOSTS=local-llm.corp # host allow-list
enkryptai-relay
Run enkryptai-relay --help for the full list. Prefer ENKRYPT_API_KEY
over --api-key, which lands in your shell history. The commands also
accept ENKRYPTAI_API_KEY, the name the rest of the SDK uses.
Keep the bridge up for the whole run: a red team run lasts from about half an hour to several hours, and if the bridge drops the run pauses and eventually fails. Run it as a service, not in the terminal you are about to close.
Programmatic API
from enkryptai_sdk import RelayBridge
RelayBridge(
bridge_id="my-laptop",
api_key="<your-enkrypt-api-key>",
target_base_url="http://localhost:11434",
).run()
Arguments are keyword-only — positional construction raises
TypeError rather than silently rebinding fields.
Translation hooks (non-OpenAI local LLMs)
The relay wire format is OpenAI chat.completions end-to-end. If your
local LLM doesn't already speak OpenAI (Anthropic, Bedrock, Vertex,
proprietary shape, ...) write a Python module that exports two
coroutines and point BRIDGE_HOOKS_MODULE at its dotted path:
async def before_request(payload: dict) -> dict:
return translate_openai_to_local(payload)
async def after_response(local_response: dict) -> dict:
return translate_local_to_openai(local_response)
The bridge validates inputs/outputs against the official openai SDK
Pydantic types at both boundaries, so a buggy hook surfaces as a
structured error to the red-team worker instead of corrupted traffic.
Nothing about your local LLM's shape has to be known by, or deployed to,
Enkrypt's cloud.
A worked OpenAI ↔ Anthropic Messages API example ships inside the
SDK at enkryptai_sdk.relay.examples.hooks_example. Either point the
bridge at it directly (smoke test) or copy it into your own repo to
edit:
# Smoke test (no copy):
export BRIDGE_HOOKS_MODULE=enkryptai_sdk.relay.examples.hooks_example
enkryptai-relay
# Or, copy the template next to your own code:
python -c "from enkryptai_sdk.relay.examples import copy_example; \
copy_example('hooks_example.py', './my_hooks.py')"
export BRIDGE_HOOKS_MODULE=my_hooks
PYTHONPATH=. enkryptai-relay
A bridge.env.example env-file template ships alongside it and can
be copied the same way (copy_example('bridge.env.example', './bridge.env')). See
src/enkryptai_sdk/relay/examples/README.md
for the full list.
Turning the relay on for a run
Routing is switched on by the red-team request, not by the bridge.
RTModelConfig.via_relay builds that target for you:
from enkryptai_sdk import RTModelConfig
target = RTModelConfig.via_relay(
bridge_id="my-laptop", # == --bridge-id
endpoint="https://local-llm.corp/v1/chat/completions", # as the bridge sees it
model_name="their-internal-model",
# Credentials your local LLM needs. They go here, never in api_key --
# the bridge is what authenticates to your LLM, so passing api_key raises.
target_headers={"Authorization": "Bearer customer-side-internal-key"},
)
which serialises to the wire shape below. Write it by hand if you prefer:
{
"target": {
"endpoint": "https://local-llm.corp/v1/chat/completions",
"api_key": "",
"model_name": "their-internal-model",
"connect_via_relay": true,
"metadata": {
"relay": {
"bridge_id": "my-laptop",
"target_endpoint": "https://local-llm.corp/v1/chat/completions",
"model_name": "their-internal-model"
}
}
},
"risk_categories": { "safety_harm": { "attack_config": { "basic": {} } } }
}
metadata.relay.bridge_id, metadata.relay.target_endpoint and
metadata.relay.model_name are all required; target.api_key may be
empty because the bridge is what authenticates to your LLM (put those
credentials in metadata.relay.target_headers). Note that
connect_via_relay stays at the root of the target — only the relay
block itself lives under metadata. A bare target.relay block is the
older spelling and is still accepted, so existing integrations keep
working; write target.metadata.relay in new ones. Ready-to-send bodies
with a field-by-field reference are in
docs/relay/examples/.
Further reading
docs/relay/ covers the architecture and config
reference (README), how to run both sides on one
laptop (LOCAL_TESTING), deploying the
cloud side (INFRA_RUNBOOK), and why the
relay is shaped this way (DESIGN).
Scan an agent skill
The Skill Scanner checks an agent "skill" (a directory in a git repo) for
security threats. Submitting is asynchronous — you get a scan_id back and
poll it; a scan typically takes 30-90 seconds.
import os
from enkryptai_sdk import SkillScannerClient
client = SkillScannerClient(api_key=os.getenv("ENKRYPTAI_API_KEY"))
queued = client.scan({
"git_url": "https://github.com/affaan-m/ECC.git",
"skill_path": ".agents/skills/api-design",
# Recommended: pins the checkout, and lets an identical repeat scan come
# back from cache instead of re-running the scanner.
"commit": "2bc924aa11bb22cc33dd44ee55ff6677889900aa",
})
print(queued.scan_id, queued.status) # -> "...", "queued"
# Blocks until the scan is done (default budget 15 min, polls every 5s).
record = client.wait_for_scan(queued.scan_id)
if record.succeeded:
print(record.verdict, record.risk_level, record.findings_count, record.stars)
print(client.get_report(record.scan_id)) # the full skill-sentinel report
else:
print("scan failed:", record.error)
# Your own scans, newest first.
for item in client.list_scans(status="succeeded", limit=10).items:
print(item.scan_id, item.repo, item.skill_name, item.verdict)
Two things worth knowing before you build on it:
- You see your organization's scans. The gateway derives the owning
identity from your API key. For an org or project key that is the
organization, so every member sees every scan the org has run, whichever
project their key belongs to; an individual account sees its own. A scan
belonging to a different organization is a
403. There is deliberately nouser_emailparameter to pass — sending one is a400. - A failed scan is a result, not an exception.
wait_for_scanreturns the record for afailedscan withrecord.errorexplaining why; only running out of time raises (SkillScannerTimeoutError).
force=True on scan() bypasses the dedup cache and forces a fresh scan.
Run a compliance scan
A compliance scan continuously reads a provider workspace's export log files, runs every message through one of your Guardrails policies, and indexes what it finds. You create the scan; the platform runs the timer.
import os
from enkryptai_sdk import ComplianceClient
client = ComplianceClient(api_key=os.getenv("ENKRYPTAI_API_KEY"))
# Probe the key and the workspace before committing to a scan. A proposed
# cursor also returns a backfill estimate -- worth quoting before you start,
# because volume varies by orders of magnitude between workspaces.
probe = client.test_connection({
"compliance_api_key": os.getenv("COMPLIANCE_API_KEY"),
"workspace_id": "00000000-0000-0000-0000-000000000000",
"proposed_cursor": "2026-09-01T00:00:00Z",
})
print(probe.key_valid, [p.event_type for p in probe.event_types if p.supported])
client.add_scan({
"scan_name": "chatgpt-enterprise",
"guardrails_name": "my-guardrail",
"provider": "openai",
"workspace_id": "00000000-0000-0000-0000-000000000000",
"event_types": ["CONVERSATION_MESSAGE"],
"cursor_end_time": "2026-09-01T00:00:00Z",
"compliance_api_key": os.getenv("COMPLIANCE_API_KEY"),
})
# How fresh the scan is: lag_s is how far behind the provider's clock it runs.
scan = client.get_scan("chatgpt-enterprise")
print(scan.scan.status, scan.lag_s)
# What it has covered, and one message's text.
files = client.list_log_files("chatgpt-enterprise", per_page=20)
print(files.pagination.total_count, files.log_files[0].flagged_count)
print(client.get_message("chatgpt-enterprise", "EVENT_ID").text)
client.pause_scan("chatgpt-enterprise")
client.start_scan("chatgpt-enterprise") # idempotent; re-resolves the key
Four things worth knowing before you build on it:
- Every path requires the
governance_officerrole. Not an org admin, not a project admin, and not the org owner unless the role was granted to them explicitly. The role resolves through an organization, so an individual account cannot hold it and these paths are unreachable on one. - A scan is addressed by name within your API key's project, so there is no id to carry around.
providerandworkspace_idare immutable once the scan exists — changing either would orphan the cursor and everything already indexed, somodify_scanrejects them. Delete and recreate instead.compliance_api_keyis write-only. No read returns it, not even masked. Send a fresh one throughmodify_scanto clear aneeds_reconnect, thenstart_scan.
A bad key from test_connection is a 200 with key_valid=False, not an
error. get_message fetches the text live and raises
ComplianceMessageExpiredError (HTTP 410) once the provider ages the file out
of its retention window -- expected for old messages, not a failure.
Copyright, License and Terms of Use
A compliance scan continuously reads a provider workspace's export log files, runs every message through one of your Guardrails policies, and indexes what it finds. You create the scan; the platform runs the timer.
import os
from enkryptai_sdk import ComplianceClient
client = ComplianceClient(api_key=os.getenv("ENKRYPTAI_API_KEY"))
# Probe the key and the workspace before committing to a scan. A proposed
# cursor also returns a backfill estimate -- worth quoting before you start,
# because volume varies by orders of magnitude between workspaces.
probe = client.test_connection({
"compliance_api_key": os.getenv("COMPLIANCE_API_KEY"),
"workspace_id": "00000000-0000-0000-0000-000000000000",
"proposed_cursor": "2026-09-01T00:00:00Z",
})
print(probe.key_valid, [p.event_type for p in probe.event_types if p.supported])
client.add_scan({
"scan_name": "chatgpt-enterprise",
"guardrails_name": "my-guardrail",
"provider": "openai",
"workspace_id": "00000000-0000-0000-0000-000000000000",
"event_types": ["CONVERSATION_MESSAGE"],
"cursor_end_time": "2026-09-01T00:00:00Z",
"compliance_api_key": os.getenv("COMPLIANCE_API_KEY"),
})
# How fresh the scan is: lag_s is how far behind the provider's clock it runs.
scan = client.get_scan("chatgpt-enterprise")
print(scan.scan.status, scan.lag_s)
# What it has covered, and one message's text.
files = client.list_log_files("chatgpt-enterprise", per_page=20)
print(files.pagination.total_count, files.log_files[0].flagged_count)
print(client.get_message("chatgpt-enterprise", "EVENT_ID").text)
client.pause_scan("chatgpt-enterprise")
client.start_scan("chatgpt-enterprise") # idempotent; re-resolves the key
Four things worth knowing before you build on it:
- Every path requires the
governance_officerrole. Not an org admin, not a project admin, and not the org owner unless the role was granted to them explicitly. The role resolves through an organization, so an individual account cannot hold it and these paths are unreachable on one. - A scan is addressed by name within your API key's project, so there is no id to carry around.
providerandworkspace_idare immutable once the scan exists — changing either would orphan the cursor and everything already indexed, somodify_scanrejects them. Delete and recreate instead.compliance_api_keyis write-only. No read returns it, not even masked. Send a fresh one throughmodify_scanto clear aneeds_reconnect, thenstart_scan.
A bad key from test_connection is a 200 with key_valid=False, not an
error. get_message fetches the text live and raises
ComplianceMessageExpiredError (HTTP 410) once the provider ages the file out
of its retention window -- expected for old messages, not a failure.
License and Terms of Use
The source code in this repository -- the enkryptai-sdk Python package, its
examples and tests -- is released under the MIT License.
Copyright (c) 2025-2026 Enkrypt AI, Inc.
The MIT License covers this client code only. It does not grant any right to the Enkrypt AI platform, APIs or hosted services the SDK talks to, or to the models, detectors, policies and data behind them. Using those requires an Enkrypt AI account and API key, and is governed by the Enkrypt AI Terms of Use and any agreement you have with Enkrypt AI.
Enkrypt AI and the Enkrypt AI logo are trademarks of Enkrypt AI, Inc. The MIT License does not grant permission to use them, except as needed to describe where the software came from.
Metadata
Release files for enkryptai-sdk 1.0.47
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| enkryptai_sdk-1.0.47.tar.gz | 233.7 kB | Without provenance | Details for enkryptai_sdk-1.0.47.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| enkryptai_sdk-1.0.47-py3-none-any.whl | Python 3 | none | any | Details for enkryptai_sdk-1.0.47-py3-none-any.whl |
Total release size: 383.9 kB
Release files / enkryptai_sdk-1.0.47.tar.gz
| Download URL | enkryptai_sdk-1.0.47.tar.gz |
|---|---|
| Size | 233.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8b57b9a47eb0b19f1d8c99257eeeb0e3a33ef2f886f9f6dee391053f36796ba4
|
|
BLAKE2b-256 checksum How to use checksums |
7182e6ac1ff748980e804293cfa798aed4e83d352fe8853f455d99db726fa691
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.17
|
Release files / enkryptai_sdk-1.0.47-py3-none-any.whl
| Download URL | enkryptai_sdk-1.0.47-py3-none-any.whl |
|---|---|
| Size | 150.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2baa7dd43cf3a42089fb3cffa068e3ae99d1663e76d55d9595e5fbd073de54fd
|
|
BLAKE2b-256 checksum How to use checksums |
f594ed34bf60850b90f2e1f88e590d8d4a86fd20e91fdc07f3f159d53d1c90f9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.17
|
Security for enkryptai-sdk 1.0.47
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.