Your guardrails fire. Your agents run. Your models respond. But none of it is witnessed. SWT3 turns every Bedrock decision into cryptographic evidence that auditors can verify independently.
v0.7.8 PREVIEW – Bedrock Guardrails and Strands agent adapters ship with v0.7.8. Available on PyPI and npm at release.
Who this is for: Cloud architects deploying AI on Bedrock, compliance engineers building evidence pipelines for SOC 2 / FedRAMP / EU AI Act, platform teams running Strands agents in production, and security teams responsible for AI governance in regulated environments.
The evidence gap in every Bedrock deployment. CloudTrail logs that InvokeModel was called. It does not log what the model decided, whether guardrails intervened, or how the agent used its tools. When an auditor asks "how do you know this model behaved correctly on March 15th?", CloudTrail gives you an API call record. SWT3 gives you a cryptographic witness anchor with the decision fingerprint.
Every enterprise Bedrock deployment has the same compliance architecture: IAM policies control who can call the models, CloudTrail logs that they did, and Guardrails filter what goes in and out. This is necessary. It is not sufficient.
Here is what a CloudTrail entry for a Bedrock Guardrails evaluation actually contains:
{
"eventName": "ApplyGuardrail",
"userIdentity": {"arn": "arn:aws:iam::123456:role/ai-service"},
"requestParameters": {
"guardrailIdentifier": "abc123",
"source": "INPUT"
}
// No content. No decision. No assessment details.
}
The guardrail ran. Something was evaluated. But the audit record does not capture what was evaluated, whether the guardrail intervened, or which policy triggered the decision. It is metadata about the API call, not evidence of the decision itself.
This matters because regulators do not ask "did you have guardrails?" They ask: "Show me the evidence that your guardrails made the right decision on this specific input at this specific time, and prove this evidence has not been altered."
SWT3 closes this gap. Every guardrail evaluation, every agent invocation, every model inference gets a witness anchor: a cryptographic fingerprint of the decision that can be verified independently, years later, without access to the underlying data.
A production Bedrock deployment has three distinct decision surfaces. Each produces evidence that auditors need. Each has a dedicated SWT3 adapter:
| Layer | What Happens | Adapter | Procedure | What Gets Witnessed |
|---|---|---|---|---|
| Guardrails | Content filtering, topic blocking, PII detection | wrap_bedrock_guardrails |
AI-GRD.1 | Each guardrail evaluation: what was checked, whether it intervened, which policy triggered |
| Agents | Multi-step reasoning, tool selection, task execution | wrap_strands |
AI-INF.1 | Each agent invocation: prompt in, result out, model used, total latency |
| Inference | Direct model calls via InvokeModel/Converse | wrap_bedrock |
AI-INF.1 | Each model response: prompt hash, response hash, token usage, latency |
These layers are independent. You can witness guardrails without witnessing inference, or witness agents without touching guardrails. Deploy one adapter where you have the biggest evidence gap. Expand when ready. Zero all-or-nothing commitments.
Bedrock Guardrails evaluate content against policies you define: topic restrictions, content filters, word blocklists, PII detection, contextual grounding. When a guardrail intervenes, it blocks or modifies the content. When it does not, the content passes through.
Both outcomes are evidence. A guardrail that passes content is attesting "this content met all policy requirements at this time." A guardrail that intervenes is attesting "this content violated policy X and was blocked." Both decisions need to be witnessed because both are compliance-relevant.
import boto3
from swt3_ai import Witness
from swt3_ai.adapters.bedrock_guardrails import wrap_bedrock_guardrails
witness = Witness(
endpoint="https://your-instance.example.com/api/v1/witness",
api_key="axm_your_key",
tenant_id="YOUR_TENANT",
)
bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")
witnessed = wrap_bedrock_guardrails(bedrock, witness)
# Every guardrail evaluation is now witnessed
result = witnessed.apply_guardrail(
guardrailIdentifier="my-content-policy",
guardrailVersion="1",
source="INPUT",
content=[{"text": {"text": "Evaluate this user message"}}],
)
# result is untouched -- your code works exactly as before
if result["action"] == "GUARDRAIL_INTERVENED":
print("Content blocked by policy")
else:
print("Content passed all checks")
witness.flush()
import { Witness } from '@tenova/swt3-ai';
import { wrapBedrockGuardrails } from '@tenova/swt3-ai';
import { BedrockRuntimeClient, ApplyGuardrailCommand } from '@aws-sdk/client-bedrock-runtime';
const witness = new Witness({
endpoint: 'https://your-instance.example.com/api/v1/witness',
apiKey: 'axm_your_key',
tenantId: 'YOUR_TENANT',
});
const bedrock = new BedrockRuntimeClient({ region: 'us-east-1' });
const witnessed = wrapBedrockGuardrails(bedrock, witness);
const result = await witnessed.applyGuardrail({
guardrailIdentifier: 'my-content-policy',
guardrailVersion: '1',
source: 'INPUT',
content: [{ text: { text: 'Evaluate this user message' } }],
});
await witness.flush();
model_id: guardrail:my-content-policy:1 -- identifies which guardrail and version made the decision.
prompt_hash: SHA-256 of the content that was evaluated. You can prove what was checked without storing the content itself.
response_hash: SHA-256 of the guardrail's action + assessments. The full decision is fingerprinted.
has_refusal: true if the guardrail intervened, false if content passed. Instant audit filter.
access_target: INPUT or OUTPUT -- whether this was pre-inference or post-inference filtering.
This is the evidence auditors cannot get from CloudTrail. The guardrail's actual decision, fingerprinted and anchored, proving that content policy was enforced on this specific input at this specific time.
Strands agents are autonomous: they receive a prompt, reason about it, select and execute tools, and return a result. The internal loop -- model calls, tool selections, intermediate reasoning -- is managed by the framework. From the outside, you see the input and the output.
That outer boundary is exactly where compliance evidence lives. An auditor does not need to trace every internal reasoning step (that is model interpretability, a different problem). They need to verify: this agent was invoked with this input, it produced this output, using this model, in this timeframe. SWT3 witnesses that boundary.
from strands import Agent
from swt3_ai import Witness
from swt3_ai.adapters.strands import wrap_strands
witness = Witness(
endpoint="https://your-instance.example.com/api/v1/witness",
api_key="axm_your_key",
tenant_id="YOUR_TENANT",
agent_id="claims-processor",
)
agent = Agent(model="us.anthropic.claude-sonnet-4-20250514")
witnessed = wrap_strands(agent, witness)
# Identical usage -- agent is still callable
result = witnessed("Review this insurance claim and flag anomalies")
witness.flush()
import { Agent } from '@strands-agents/sdk';
import { Witness, wrapStrands } from '@tenova/swt3-ai';
const witness = new Witness({
endpoint: 'https://your-instance.example.com/api/v1/witness',
apiKey: 'axm_your_key',
tenantId: 'YOUR_TENANT',
agentId: 'claims-processor',
});
const agent = new Agent({ model: 'us.anthropic.claude-sonnet-4-20250514' });
const witnessed = wrapStrands(agent, witness);
const result = await witnessed.invoke('Review this insurance claim and flag anomalies');
await witness.flush();
Agent witnessing complements tool witnessing. wrap_strands witnesses the outer agent invocation. If you also need evidence of individual tool calls within the agent loop, use wrap_tool on the tools themselves. Two independent attestation layers, each with its own anchor.
Each Strands agent invocation produces a single anchor covering the full agent execution: prompt in, result out, model used, total wall-clock latency.
Query the ledger for provider="aws-strands" and compare model_hash values across a 30-day window. If the hash changes, the underlying model was swapped (Bedrock model version rotation or configuration change). Cross-reference with the organization's model change management records. Any hash change without a corresponding change ticket is a finding.
For direct model calls (no agent framework), the existing Bedrock adapter witnesses every InvokeModel or Converse call:
import boto3
from swt3_ai import Witness
witness = Witness(
endpoint="https://your-instance.example.com/api/v1/witness",
api_key="axm_your_key",
tenant_id="YOUR_TENANT",
)
bedrock = boto3.client("bedrock-runtime")
witnessed = witness.wrap(bedrock) # auto-detects Bedrock
response = witnessed.converse(
modelId="us.anthropic.claude-sonnet-4-20250514",
messages=[{"role": "user", "content": [{"text": "Summarize this report"}]}],
)
witness.flush()
Every call gets an anchor: model hash, prompt hash, response hash, latency, token usage. The response is untouched. Your application code does not change.
A real production pipeline often uses all three: guardrails filter input, a model generates a response, guardrails filter output. With SWT3, every decision surface is witnessed independently:
import boto3
from strands import Agent
from swt3_ai import Witness
from swt3_ai.adapters.bedrock_guardrails import wrap_bedrock_guardrails
witness = Witness(
endpoint="https://your-instance.example.com/api/v1/witness",
api_key="axm_your_key",
tenant_id="YOUR_TENANT",
agent_id="underwriting-agent",
jurisdiction="US",
)
bedrock = boto3.client("bedrock-runtime")
# Layer 1: Witness guardrail evaluations
guarded = wrap_bedrock_guardrails(bedrock, witness)
# Layer 2: Witness agent invocations (agent uses Bedrock internally)
from swt3_ai.adapters.strands import wrap_strands
agent = Agent(model="us.anthropic.claude-sonnet-4-20250514")
witnessed_agent = wrap_strands(agent, witness)
# Pre-inference guardrail check
input_check = guarded.apply_guardrail(
guardrailIdentifier="input-policy",
guardrailVersion="1",
source="INPUT",
content=[{"text": {"text": user_message}}],
)
if input_check["action"] == "NONE":
# Input passed -- run the agent
result = witnessed_agent(user_message)
# Post-inference guardrail check
output_check = guarded.apply_guardrail(
guardrailIdentifier="output-policy",
guardrailVersion="1",
source="OUTPUT",
content=[{"text": {"text": str(result)}}],
)
# 3 anchors minted: input guardrail + agent invocation + output guardrail
witness.flush()
Three independent witness anchors from one user interaction. Each anchor is separately verifiable. An auditor can confirm that input filtering, model reasoning, and output filtering all happened -- and in what order -- without accessing the content itself. This is the evidence chain that CloudTrail cannot provide.
For a given user interaction, the ledger should contain three anchors with timestamps in sequence: INPUT guardrail, agent/inference, OUTPUT guardrail.
Query anchors for a single agent_id within a 5-second window. Verify: (1) an INPUT guardrail anchor exists with guardrail_passed=true before the inference anchor, (2) the inference anchor's model_id matches the approved model list, (3) an OUTPUT guardrail anchor exists after the inference anchor. If any layer is missing, the pipeline has an unwitnessed decision surface. If INPUT guardrail shows has_refusal=true but an inference anchor still exists, the guardrail was bypassed -- this is a critical finding.
Organizations using Bedrock cross-account guardrails enforce policy centrally. The SWT3 anchor captures the guardrail ID and version regardless of which account owns the guardrail.
Verify that model_id on guardrail anchors follows the format guardrail:{id}:{version}. Compare the guardrail identifier against the organization's approved guardrail registry. If a production workload references a guardrail ID not in the registry, it may be using a development or test guardrail in production -- a policy control failure.
Every anchor minted on Bedrock maps to specific regulatory requirements. The evidence is not theoretical -- it is the exact artifact that satisfies the obligation.
| Regulation | Requirement | CloudTrail | SWT3 |
|---|---|---|---|
| EU AI Act Art. 12 | Automatic logging of AI system operation | API metadata only | Per-decision fingerprint |
| EU AI Act Art. 13 | Transparency: deployers must understand AI output | No output evidence | Response hash + model ID |
| EU AI Act Art. 14 | Human oversight measures | No guardrail decision log | Guardrail intervention evidence |
| NIST AI RMF MAP 1.5 | Document AI system interfaces and dependencies | Partial (IAM roles) | Agent + model + guardrail chain |
| NIST AI RMF MN 2.2 | Mechanisms to track AI system behavior | API-level only | Decision-level tracking |
| SOC 2 CC7.2 | Monitor system components for anomalies | Infrastructure monitoring | AI decision monitoring |
| FedRAMP AU-2 | Audit events for security-relevant activities | API call audit | Decision-level audit |
| SR 11-7 / SR 26-2 | Model risk management: ongoing monitoring | No model behavior data | Drift detection via hash comparison |
The pattern is always the same. CloudTrail tells you the API was called. SWT3 tells you what the AI decided. Regulators care about the decision, not the API call. Both records are necessary; only one provides decision-level accountability.
Request a ledger export filtered by provider IN ("aws-bedrock-guardrails", "aws-strands", "bedrock") for the assessment period. For each regulatory requirement in scope: (1) confirm at least one anchor type maps to the requirement per the table above, (2) verify anchor density is sufficient (no gaps longer than the organization's stated monitoring interval), (3) spot-check 5 anchors by re-hashing the stated model_id and confirming model_hash matches. If the organization claims continuous monitoring but the ledger shows anchor gaps exceeding 24 hours, the monitoring claim is unsupported.
# Python
pip install swt3-ai
# TypeScript
npm install @tenova/swt3-ai
| If You Have | Start With | Lines of Code |
|---|---|---|
| Bedrock Guardrails in production | wrap_bedrock_guardrails(client, witness) |
3 |
| Strands agents in production | wrap_strands(agent, witness) |
3 |
| Direct InvokeModel/Converse calls | witness.wrap(bedrock_client) |
1 |
import boto3
from swt3_ai import Witness
from swt3_ai.adapters.bedrock_guardrails import wrap_bedrock_guardrails
witness = Witness(
endpoint="https://your-instance.example.com/api/v1/witness",
api_key="axm_your_key",
tenant_id="YOUR_TENANT",
)
bedrock = boto3.client("bedrock-runtime")
witnessed = wrap_bedrock_guardrails(bedrock, witness)
# Your existing code. Unchanged. Every evaluation witnessed.
result = witnessed.apply_guardrail(
guardrailIdentifier="my-policy",
guardrailVersion="1",
source="INPUT",
content=[{"text": {"text": "user input here"}}],
)
witness.flush()
Three lines added. Zero code changes to your application logic. Every guardrail decision now has a cryptographic witness anchor that survives independently of AWS infrastructure.