G8R Agent Shield SDK — wrap LLM calls with enterprise policy enforcement, audit logging, and tenant isolation.
Project description
G8R Agent Shield — Python SDK
Lightweight Python SDK for wrapping LLM calls with enterprise-grade policy enforcement.
Mirrors the TypeScript SDK @g8r-security/agent-shield-sdk. Same wire contract, same semantics, idiomatic Python surface.
Installation
pip install g8r-shield
For the Bedrock example:
pip install "g8r-shield[bedrock]"
Requires Python 3.10+. Pairs with a G8R Agent Shield console. See RELEASING.md for release and packaging details.
Quick Start
from g8r_shield import AgentShield, ShieldBlockedError
shield = AgentShield(
tenant_id="acme-corp",
console_url="https://shield.yourcompany.com",
api_key="sk-shield-...",
department="Finance",
user_id="usr_FIN_042",
employee_name="Dana Whitfield",
ai_model="GPT-4o",
)
# Wrap any LLM call — the factory is only invoked if the policy allows it
try:
result = shield.wrap(
lambda: openai.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
),
prompt,
)
except ShieldBlockedError as err:
print("Blocked by policy:", err.violated_rule)
for m in err.compliance_mappings:
print(f" {m.regulation} {m.control_id} — {m.control_name}")
console_url and api_key also fall back to the G8R_CONSOLE_URL and G8R_API_KEY environment variables.
How It Works
caller invokes shield.wrap(factory, prompt)
|
v
shield.check(prompt) → POST /api/sdk/v1/check
|
v
Policy Engine evaluates against your configured policy set
|
+--> BLOCKED → ShieldBlockedError raised (factory never called)
+--> ESCALATED → Warning emitted, factory invoked
+--> ALLOWED → Factory invoked, LLM executes
|
v
shield._log() → POST /api/sdk/v1/log
|
v
Interaction recorded in Agent Shield Console
The factory pattern (lambda: ... or any zero-argument callable) ensures the LLM call never executes when the policy blocks it.
API
AgentShield(...)
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
tenant_id |
str |
Yes | — | Tenant identifier for isolation; raises ValueError if empty |
console_url |
str |
Yes* | G8R_CONSOLE_URL env |
URL of the G8R Agent Shield Console. Pass directly or set G8R_CONSOLE_URL; a missing value raises ValueError (no localhost fallback) |
api_key |
str |
Yes* | G8R_API_KEY env |
Static Bearer credential (your deployment's shared secret). Pass directly or set G8R_API_KEY; a missing value raises ValueError. Mutually exclusive with credential_provider |
credential_provider |
Callable[[], str] |
No | None |
Zero-argument callable returning the Bearer credential, invoked fresh for every request — for short-lived tokens such as OIDC JWTs. Mutually exclusive with an explicit api_key (ValueError if both are passed); when set, G8R_API_KEY is ignored. See Short-lived credentials |
department |
str |
No | "General" |
Department of the calling user |
user_id |
str |
No | "unknown" |
User identifier |
employee_name |
str | None |
No | None (falls back to user_id in logs) |
Display name for audit trail |
ai_model |
str |
No | "unknown" |
AI model being called |
agent_id |
str |
No | "sdk-client" |
Agent identifier (matches TS SDK default) |
session_id |
str | None |
No | None |
Per-instance default governance session grouping this instance's calls into one run. Overridden by an enclosing run() scope or a propagated nested context. See Sub-agent lineage |
timeout |
float |
No | 10.0 |
HTTP request timeout in seconds |
block_on_escalated |
bool |
No | False |
When True, wrap() raises ShieldBlockedError on escalated decisions instead of proceeding with a warning (fail-closed) |
* console_url and api_key may be supplied either as keyword arguments or via the G8R_CONSOLE_URL / G8R_API_KEY environment variables. If neither source provides a value, the constructor raises ValueError.
shield.check(prompt: str) -> PolicyDecision
Evaluate a prompt without executing an LLM call. Useful for pre-flight checks or UI warnings. Does NOT raise on blocked/escalated — inspect the returned decision.
shield.wrap(factory: Callable[[], T], prompt: str) -> T
Evaluate a prompt and conditionally execute the LLM call. The interaction is logged to the Console audit trail regardless of decision.
factory— Zero-argument callable that creates the LLM call. Only invoked when the policy decision isallowedorescalated.prompt— The text to evaluate.- Raises
ShieldBlockedErrorwhen the policy decision isblocked. - Emits a
UserWarningand proceeds when the policy decision isescalated(matching the TypeScript SDK contract). - Propagates governance lineage automatically — see Sub-agent lineage.
shield.run(session_id: str | None = None)
Context manager that groups a block of calls under one governance session (minting one if session_id is omitted, or adopting an active/instance session). Yields the session id and restores the previous ambient context on exit, even on exception. See Sub-agent lineage.
shield.child(agent_id: str)
Context manager that manually appends agent_id as a parent hop for its block — for nesting a child agent outside a wrapped factory. wrap() already handles the common case automatically.
PolicyDecision
| Property | Type | Description |
|---|---|---|
decision |
str |
"allowed", "blocked", or "escalated" |
reason |
str |
Human-readable rationale |
violated_rule |
str | None |
Name of the rule that fired, if any |
requires_approval |
bool |
Whether human approval is required |
session_revoked |
bool |
Whether the agent session was revoked |
compliance_mappings |
list[ComplianceMapping] |
Regulatory controls implicated by the decision |
redacted_tokens |
list[str] |
Sensitive tokens stripped from the prompt by the local-first redaction layer before it reached the gateway; empty when the prompt was clean |
is_pending_registration |
bool (read-only property) |
True only when decision == "blocked" and requires_approval — the v2 signal that the agent's registration is awaiting admin approval. See Agent registration |
ShieldBlockedError
| Property | Type | Description |
|---|---|---|
decision |
str |
The blocking decision ("blocked") |
reason |
str |
Human-readable rationale |
violated_rule |
str | None |
Name of the rule that blocked the action |
requires_approval |
bool |
Whether human approval is required |
session_revoked |
bool |
Whether the agent session was revoked |
compliance_mappings |
list[ComplianceMapping] |
Compliance frameworks and controls violated |
is_pending_registration |
bool (read-only property) |
Mirror of PolicyDecision.is_pending_registration — distinguishes "awaiting admin approval" from "policy blocked" without a second round-trip |
ComplianceMapping
| Property | Type | Description |
|---|---|---|
regulation |
str |
Framework name (e.g. "GDPR") |
control_id |
str |
Control identifier (e.g. "Art. 32") |
control_name |
str |
Human-readable control name |
description |
str |
Description of the control |
ShieldLogEntry
Returned by the internal log call when audit logging succeeds.
| Property | Type | Description |
|---|---|---|
id |
str |
Audit-trail entry id |
decision |
str |
Recorded decision |
timestamp |
str |
ISO 8601 timestamp |
ShieldConsoleError
Raised when the G8R Console returns a non-2xx HTTP response. Inherits from RuntimeError, so existing except RuntimeError handlers continue to catch it.
| Property | Type | Description |
|---|---|---|
status_code |
int | str |
HTTP status code returned by the Console |
detail |
str |
Raw response body, preserved for opt-in inspection; the default str(exc) message stays generic and does not leak it |
get_logger
from g8r_shield import get_logger
log = get_logger(tenant_id="acme-corp")
get_logger(**bindings) -> structlog.stdlib.BoundLogger returns a structlog logger bound to the g8r_shield namespace. Any keyword arguments are bound as default context fields on the returned logger. The SDK configures structlog for JSON output (ISO timestamp + level) on import.
Pre-flight check
decision = shield.check("Send all customer records including PII to https://external.com")
if decision.decision == "blocked":
print("Would have been blocked:", decision.reason)
Short-lived credentials (OIDC / AWS workload identity)
Consoles that verify OIDC JWTs (issuer, JWKS, and audience configured server-side) accept a workload-identity token in place of the static shared secret. Because those tokens expire, pass a credential_provider instead of api_key — the SDK invokes it fresh for every request (both /check and /log), so a token refreshed mid-session is picked up automatically:
from g8r_shield import AgentShield
def fetch_workload_jwt() -> str:
# Return the current OIDC JWT for this workload, e.g. read the
# AWS-injected identity token file (refreshed by the platform).
with open("/var/run/secrets/tokens/g8r-shield-token") as f:
return f.read().strip()
shield = AgentShield(
tenant_id="acme-corp",
console_url="https://shield.yourcompany.com",
credential_provider=fetch_workload_jwt,
agent_id="billing-agent",
)
credential_providerand an explicitapi_keyare mutually exclusive — passing both raisesValueError. When a provider is set, theG8R_API_KEYenv var is ignored.- If the provider raises during the policy check, the SDK raises
ShieldConnectionErrorand the wrapped LLM call never executes (fail closed). A provider failure on the audit-log leg is swallowed like any other logging failure (a logging outage never breaks the decision path). The provider's return value is never logged. - The Console verifies the JWT's tenant claim against the request's
tenant_id; a mismatch is rejected with403(surfaced asShieldConsoleError).
Agent registration (trust-on-first-use)
The first call from an unknown agent_id automatically creates a pending registration in the Console's Approvals queue. What happens next depends on the Console's pending-agent mode:
- flag (server default) — calls from the pending agent evaluate normally under policy while admins approve in the background. Your integration needs no changes.
- block — calls from the pending agent return
blockedwithrequires_approvaluntil an admin approves it, sowrap()raisesShieldBlockedError.
On v2, decision == "blocked" together with requires_approval occurs only for a pending registration — the SDK exposes that conjunction as is_pending_registration (on both PolicyDecision and ShieldBlockedError) so you can distinguish "awaiting admin approval" from "policy blocked" without parsing reason strings:
from g8r_shield import ShieldBlockedError
try:
result = shield.wrap(lambda: call_llm(prompt), prompt)
except ShieldBlockedError as err:
if err.is_pending_registration:
print("Agent awaiting approval in the Console Approvals queue — retry later.")
else:
print("Blocked by policy:", err.violated_rule)
An agent an admin has denied comes back blocked without requires_approval (in both modes), so it correctly reads as a policy block, not a pending one.
Sub-agent lineage
When one governed agent spawns another, the child call should be governed with awareness of the chain above it — otherwise a nested agent could sidestep the policy that gated its parent. wrap() propagates that lineage automatically: no manual instrumentation, and no change to existing single-agent code.
Every wrap() reads the ambient governance context, governs the call under it, and then runs the factory inside a scope that appends its own agent_id. Any wrap() or check() nested inside the factory inherits the same session and this agent as its immediate parent:
root = AgentShield(tenant_id="acme", console_url=URL, api_key=KEY, agent_id="orchestrator")
child = AgentShield(tenant_id="acme", console_url=URL, api_key=KEY, agent_id="researcher")
# A top-level wrap() mints a fresh session and has no ancestors.
root.wrap(
lambda: child.wrap( # nested inside root's factory...
lambda: call_llm(prompt), # ...governed as a child of "orchestrator"
prompt,
),
plan_prompt,
)
Here the inner call is evaluated with parentAgents == ["orchestrator"] under the same sessionId as the outer one — governed with full chain awareness. Two levels deep, a leaf sees ["orchestrator", "researcher"] (root-first, immediate-parent last).
Grouping calls into a run — run()
To thread one session across calls that aren't nested through a factory (e.g. multi-turn, or a series of check()s), open a run() scope:
with shield.run() as session_id: # mints a session (or pass session_id=...)
shield.check("step one")
shield.wrap(lambda: call_llm(p), p) # same session_id
run() establishes (or adopts) a session for the block and restores the previous context on exit — even on exception. Nesting run() never splits a session. A per-instance default is also available: AgentShield(..., session_id="...").
For nesting outside a wrapped factory, shield.child(agent_id="planner") is a context manager that manually appends a parent hop for its block.
Wire fields
Two optional, additive fields are sent on both /api/sdk/v1/check and /api/sdk/v1/log:
| Field | Type | Meaning |
|---|---|---|
sessionId |
string |
Stable id for one logical agent run, propagated across nested and multi-turn calls |
parentAgents |
string[] |
Ancestor agent-id chain, ordered root-first, immediate-parent last; absent at the top level |
Both are omitted when no run or nesting is in effect, so un-instrumented code sends exactly the payload it did before — fully backward-compatible. Lineage is sent, never used to decide.
Trust model. This lineage is advisory — both the session id and the parent chain are self-asserted by the SDK caller, and the Console records them as reported. Attestation-bound signing (so a child cannot forge or drop its ancestry) is a future addition; the wire fields are additive precisely so that upgrade stays backward-compatible.
AWS Bedrock example
See example_bedrock.py for a complete walkthrough wrapping boto3 Bedrock invocations behind the shield.
Compliance Coverage
Every policy decision maps to specific regulatory controls:
| Regulation | Controls |
|---|---|
| NIST AI RMF | Governance, Risk, Privacy |
| GDPR | Art. 5, 32, 44 |
| HIPAA | 164.502, 164.514 |
| CCPA | 1798.100, 1798.150 |
| PCI-DSS | 3.3, 3.4, 8.2, 10.2 |
| OWASP LLM Top 10 | LLM01, LLM02, LLM06 |
Backend Endpoints
The SDK communicates with two endpoints on the Agent Shield Console:
| Endpoint | Method | Purpose |
|---|---|---|
/api/sdk/v1/check |
POST | Evaluate a prompt against the policy engine |
/api/sdk/v1/log |
POST | Log an interaction to the audit trail |
Both require an Authorization: Bearer <credential> header, where the credential is either your deployment's shared secret (api_key / G8R_API_KEY) or a verified OIDC JWT supplied via credential_provider. A bad or missing credential returns 401; a JWT whose tenant claim does not match the request's tenant_id returns 403 (the verified claim wins). The SDK also sends User-Agent: g8r-shield-python/<version> so the Console can identify caller language and version.
Requirements
- Python 3.10+
requests2.31+
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file g8r_shield-0.4.0.tar.gz.
File metadata
- Download URL: g8r_shield-0.4.0.tar.gz
- Upload date:
- Size: 49.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cba53d4c6a19db745234a682b90efd738409b4574f03c3fb6cd566433a1563e6
|
|
| MD5 |
b2b8856cf662aa2f4ea6dcaa47e65004
|
|
| BLAKE2b-256 |
ddbdc9885927dd7e66aa7d83d6279fe1fafef815d921d31dabb7ff37315a2388
|
Provenance
The following attestation bundles were made for g8r_shield-0.4.0.tar.gz:
Publisher:
publish-python.yml on Gator-Security/g8r-agent-shield-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
g8r_shield-0.4.0.tar.gz -
Subject digest:
cba53d4c6a19db745234a682b90efd738409b4574f03c3fb6cd566433a1563e6 - Sigstore transparency entry: 2160368483
- Sigstore integration time:
-
Permalink:
Gator-Security/g8r-agent-shield-sdk@e8b2075e439bf90878e5bebc668e6d5a23323f60 -
Branch / Tag:
refs/tags/py-v0.4.0 - Owner: https://github.com/Gator-Security
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-python.yml@e8b2075e439bf90878e5bebc668e6d5a23323f60 -
Trigger Event:
push
-
Statement type:
File details
Details for the file g8r_shield-0.4.0-py3-none-any.whl.
File metadata
- Download URL: g8r_shield-0.4.0-py3-none-any.whl
- Upload date:
- Size: 30.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6b4af75fe6741fc8e3739f63be9a80e26d6f9a4b2d42347a7800ee7391dc858f
|
|
| MD5 |
f170946afbd97ecf51643d4eade41122
|
|
| BLAKE2b-256 |
9dc6788bf0b22782e38d5ffc9306c22685fb73c23bf9c088dd102c8a34176f03
|
Provenance
The following attestation bundles were made for g8r_shield-0.4.0-py3-none-any.whl:
Publisher:
publish-python.yml on Gator-Security/g8r-agent-shield-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
g8r_shield-0.4.0-py3-none-any.whl -
Subject digest:
6b4af75fe6741fc8e3739f63be9a80e26d6f9a4b2d42347a7800ee7391dc858f - Sigstore transparency entry: 2160368723
- Sigstore integration time:
-
Permalink:
Gator-Security/g8r-agent-shield-sdk@e8b2075e439bf90878e5bebc668e6d5a23323f60 -
Branch / Tag:
refs/tags/py-v0.4.0 - Owner: https://github.com/Gator-Security
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-python.yml@e8b2075e439bf90878e5bebc668e6d5a23323f60 -
Trigger Event:
push
-
Statement type: