Skip to content

Commit 503a251

Browse files
JiaDeclaude
andcommitted
feat: AWS Bedrock Guardrails L5 — topic denial + PII enforcement per Runtime
- server.py: GUARDRAIL_ID env var check on every INPUT + OUTPUT via apply_guardrail() Exec Runtime has no GUARDRAIL_ID → unrestricted. Standard Runtime blocked by policy. Logs guardrail_block events to DynamoDB for full audit trail. - main.py: GET /security/guardrails (list Bedrock Guardrails), update_runtime_config now writes GUARDRAIL_ID/VERSION as env vars on Runtime update. GET /audit/guardrail-events returns guardrail_block DynamoDB records. GET /security/runtimes now exposes guardrailId + guardrailVersion per runtime. - SecurityCenter.tsx: Guardrail dropdown in Runtime Configure modal (auto-populates from list_guardrails API), Guardrail badge on RuntimeCard, L5 row in Defense Layers. - AuditLog.tsx: Guardrail Events tab with per-event actor/policy/direction display, enforcement summary counters, guardrail_block added to event type filter. - useApi.ts: useGuardrails(), useGuardrailEvents() hooks, guardrailId on RuntimeConfig. - README.md: L5 Guardrail added to security layers table + Flagship Features. - Demo: CryptoVault-Standard-Policy guardrail (b44c26tk2kds v1) created in us-east-1, bound to Standard Runtime via GUARDRAIL_ID env var. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 2a37d80 commit 503a251

6 files changed

Lines changed: 327 additions & 4 deletions

File tree

enterprise/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@ Every agent invocation runs in an isolated Firecracker microVM — the same hype
4242
| L2 — Application | Skills manifest `allowedRoles`/`blockedRoles` | ⚠️ Code bug risk |
4343
| **L3 — IAM** | **Runtime role has no permission on target resource** | **Impossible** |
4444
| **L4 — Compute** | **Firecracker microVM per invocation, isolated at hypervisor level** | **Impossible** |
45+
| **L5 — Guardrail** | **Bedrock Guardrail checks every input + output: topic denial, PII filtering, compliance policies** | **Impossible — AWS-managed, semantic AI layer** |
4546

46-
Each runtime tier has its own Docker image, its own IAM role, and its own Firecracker boundary. An intern's agent IAM role literally cannot read the exec S3 bucket — even if the LLM tries.
47+
Each runtime tier has its own Docker image, its own IAM role, its own Firecracker boundary, and an optional Bedrock Guardrail. An intern's agent IAM role literally cannot read the exec S3 bucket — even if the LLM tries. And even if it could, the Guardrail blocks the output before it reaches the user.
4748

4849
Additional controls: no public ports (SSM only) · IAM roles throughout, no hardcoded credentials · gateway token in SSM SecureString, never on disk · VPC isolation between runtimes.
4950

@@ -77,6 +78,7 @@ Additional controls: no public ports (SSM only) · IAM roles throughout, no hard
7778
| **Three-Layer SOUL** | Global (IT) → Position (dept admin) → Personal (employee). 3 stakeholders, 3 layers, one merged identity. Same LLM — Finance Analyst vs SDE have completely different personalities and permissions |
7879
| **Self-Service IM Pairing** | Employee scans QR code from Portal → connects Telegram / Feishu / Discord in 30 seconds. No IT ticket, no admin approval |
7980
| **Multi-Runtime Architecture** | Standard tier (Nova 2 Lite, scoped IAM) vs Executive tier (Claude Sonnet 4.6, full access). Different Docker images, different models, different IAM roles — infrastructure-level isolation |
81+
| **Bedrock Guardrails (L5)** | Assign any Bedrock Guardrail to a Runtime from Security Center UI. Topic denial, PII filtering, and compliance policies wrap every user input and agent output — no OpenClaw source code changes needed. Standard employees get blocked; exec tier is unrestricted. Full block audit trail in Audit Center. |
8082
| **Org Directory KB** | Company directory (every employee, R&R, contact, agent capabilities) seeded from org data and injected into every agent — agents know who to contact and can draft messages for you |
8183
| **Position → Runtime Routing** | 3-tier routing chain: employee override → position rule → default. Assign positions to runtimes from Security Center UI, propagates to all members automatically |
8284
| **Per-Employee Model Config** | Override model, context window, compaction settings, and response language at position OR employee level from Agent Factory → Configuration tab |
@@ -198,6 +200,7 @@ Runtime: Executive (C-Suite / Senior Leadership)
198200
| L2 — Application | Skills manifest `allowedRoles`/`blockedRoles` | ⚠️ Code bug risk |
199201
| **L3 — IAM** | **Runtime role has no permission on target resource** | **✅ Impossible** |
200202
| L4 — Network | VPC isolation between Runtimes | ✅ Infrastructure-level |
203+
| **L5 — Guardrail** | **Bedrock Guardrail per Runtime: topic denial, PII, compliance. Wraps ALL inputs + outputs.** | **✅ Impossible — AWS-managed semantic AI filter** |
201204

202205
#### 3. Digital Twin — AI Availability Beyond Office Hours
203206

enterprise/admin-console/server/main.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4835,6 +4835,8 @@ def get_security_runtimes(authorization: str = Header(default="")):
48354835
"region": env.get("AWS_REGION", "us-east-1"),
48364836
"idleTimeoutSec": lc.get("idleRuntimeSessionTimeout", 900),
48374837
"maxLifetimeSec": lc.get("maxLifetime", 28800),
4838+
"guardrailId": env.get("GUARDRAIL_ID", ""),
4839+
"guardrailVersion": env.get("GUARDRAIL_VERSION", ""),
48384840
"createdAt": detail.get("createdAt", "").isoformat() if hasattr(detail.get("createdAt", ""), "isoformat") else str(detail.get("createdAt", "")),
48394841
"version": detail.get("agentRuntimeVersion", "1"),
48404842
})
@@ -4910,6 +4912,16 @@ def update_runtime_config(runtime_id: str, body: dict, authorization: str = Head
49104912
except Exception:
49114913
pass
49124914

4915+
# Guardrail binding: store as env vars; "" means remove guardrail
4916+
if "guardrailId" in body:
4917+
gid = body["guardrailId"].strip()
4918+
if gid:
4919+
new_env["GUARDRAIL_ID"] = gid
4920+
new_env["GUARDRAIL_VERSION"] = body.get("guardrailVersion", "DRAFT").strip() or "DRAFT"
4921+
else:
4922+
new_env.pop("GUARDRAIL_ID", None)
4923+
new_env.pop("GUARDRAIL_VERSION", None)
4924+
49134925
role_arn = body.get("roleArn") or detail["roleArn"]
49144926
idle = body.get("idleTimeoutSec") or detail.get("lifecycleConfiguration", {}).get("idleRuntimeSessionTimeout", 900)
49154927
max_life = body.get("maxLifetimeSec") or detail.get("lifecycleConfiguration", {}).get("maxLifetime", 28800)
@@ -4986,6 +4998,52 @@ def create_runtime(body: CreateRuntimeRequest, authorization: str = Header(defau
49864998
raise HTTPException(500, str(e))
49874999

49885000

5001+
# ── Guardrails ───────────────────────────────────────────────────────────────
5002+
5003+
@app.get("/api/v1/security/guardrails")
5004+
def list_guardrails(authorization: str = Header(default="")):
5005+
"""List all Bedrock Guardrails available in this account/region."""
5006+
_require_role(authorization, roles=["admin"])
5007+
try:
5008+
import boto3 as _b3gr
5009+
bedrock = _b3gr.client("bedrock", region_name=_GATEWAY_REGION)
5010+
resp = bedrock.list_guardrails(maxResults=100)
5011+
guardrails = []
5012+
for g in resp.get("guardrails", []):
5013+
guardrails.append({
5014+
"id": g["id"],
5015+
"name": g["name"],
5016+
"status": g.get("status", "READY"),
5017+
"version": g.get("version", "DRAFT"),
5018+
"updatedAt": g.get("updatedAt", "").isoformat() if hasattr(g.get("updatedAt", ""), "isoformat") else str(g.get("updatedAt", "")),
5019+
})
5020+
return {"guardrails": guardrails}
5021+
except Exception as e:
5022+
return {"guardrails": [], "error": str(e)}
5023+
5024+
5025+
@app.get("/api/v1/audit/guardrail-events")
5026+
def get_guardrail_events(authorization: str = Header(default=""), limit: int = 50):
5027+
"""Fetch guardrail_block audit events from DynamoDB."""
5028+
_require_role(authorization, roles=["admin", "manager"])
5029+
try:
5030+
table = boto3.resource("dynamodb", region_name=DYNAMODB_REGION).Table(DYNAMODB_TABLE)
5031+
resp = table.query(
5032+
IndexName="GSI1",
5033+
KeyConditionExpression=Key("GSI1PK").eq("TYPE#audit"),
5034+
ScanIndexForward=False,
5035+
Limit=limit * 5, # over-fetch since we filter by eventType
5036+
)
5037+
events = [item for item in resp.get("Items", []) if item.get("eventType") == "guardrail_block"]
5038+
events = events[:limit]
5039+
for e in events:
5040+
e.pop("PK", None); e.pop("SK", None)
5041+
e.pop("GSI1PK", None); e.pop("GSI1SK", None)
5042+
return {"events": events}
5043+
except Exception as e:
5044+
return {"events": [], "error": str(e)}
5045+
5046+
49895047
# ── Separate resource endpoints for dropdowns ──────────────────────────────
49905048

49915049
@app.get("/api/v1/security/ecr-images")

enterprise/admin-console/src/hooks/useApi.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,9 +641,20 @@ export interface SecurityRuntime {
641641
id: string; name: string; status: string;
642642
containerUri: string; roleArn: string; model: string;
643643
idleTimeoutSec: number; maxLifetimeSec: number;
644+
guardrailId?: string; guardrailVersion?: string;
644645
createdAt: string; version: string;
645646
}
646647

648+
export interface Guardrail {
649+
id: string; name: string; status: string; version: string; updatedAt: string;
650+
}
651+
652+
export interface GuardrailEvent {
653+
id: string; timestamp: string; actorName: string; actorId: string;
654+
guardrailId: string; guardrailVersion: string; guardrailSource: string;
655+
guardrailPolicy: string; detail: string; status: string;
656+
}
657+
647658
export function useSecurityRuntimes() {
648659
return useQuery<{ runtimes: SecurityRuntime[]; error?: string }>({
649660
queryKey: ['security-runtimes'],
@@ -756,11 +767,29 @@ export function useUpdateRuntimeConfig() {
756767
runtimeId: string; containerUri?: string; roleArn?: string;
757768
networkMode?: string; securityGroupIds?: string[]; subnetIds?: string[];
758769
modelId?: string; idleTimeoutSec?: number; maxLifetimeSec?: number;
770+
guardrailId?: string; guardrailVersion?: string;
759771
}) => api.put(`/security/runtimes/${data.runtimeId}/config`, data),
760772
onSuccess: () => qc.invalidateQueries({ queryKey: ['security-runtimes'] }),
761773
});
762774
}
763775

776+
export function useGuardrails() {
777+
return useQuery<{ guardrails: Guardrail[]; error?: string }>({
778+
queryKey: ['guardrails'],
779+
queryFn: () => api.get('/security/guardrails'),
780+
staleTime: 60_000,
781+
});
782+
}
783+
784+
export function useGuardrailEvents(limit = 50) {
785+
return useQuery<{ events: GuardrailEvent[]; error?: string }>({
786+
queryKey: ['guardrail-events', limit],
787+
queryFn: () => api.get(`/audit/guardrail-events?limit=${limit}`),
788+
staleTime: 15_000,
789+
refetchInterval: 30_000,
790+
});
791+
}
792+
764793
export function useCreateRuntime() {
765794
const qc = useQueryClient();
766795
return useMutation({

enterprise/admin-console/src/pages/AuditLog.tsx

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { useState, useMemo } from 'react';
22
import { useNavigate } from 'react-router-dom';
3-
import { Shield, Download, Search, AlertTriangle, CheckCircle, XCircle, Info, Clock, Brain, Scan, Loader, Sparkles } from 'lucide-react';
3+
import { Shield, Download, Search, AlertTriangle, CheckCircle, XCircle, Info, Clock, Brain, Scan, Loader, Sparkles, ShieldAlert, Filter } from 'lucide-react';
44
import { Card, Badge, Button, PageHeader, Table, StatCard, Tabs } from '../components/ui';
5-
import { useAuditEntries, useAuditInsights, useRunAuditScan } from '../hooks/useApi';
5+
import { useAuditEntries, useAuditInsights, useRunAuditScan, useGuardrailEvents } from '../hooks/useApi';
66
import type { AuditEntry } from '../types';
77

88
const eventTypeOptions = [
99
{ label: 'All Events', value: 'all' },
10+
{ label: 'Guardrail Block', value: 'guardrail_block' },
1011
{ label: 'Agent Invocation', value: 'agent_invocation' },
1112
{ label: 'Tool Execution', value: 'tool_execution' },
1213
{ label: 'Permission Denied', value: 'permission_denied' },
@@ -46,9 +47,11 @@ export default function AuditLog() {
4647

4748
const { data: AUDIT_ENTRIES = [] } = useAuditEntries({ limit: 50, eventType: eventType !== 'all' ? eventType : undefined });
4849
const { data: insightsData, refetch: refetchInsights } = useAuditInsights();
50+
const { data: guardrailData } = useGuardrailEvents(50);
4951
const runScan = useRunAuditScan();
5052
const insights = insightsData?.insights || [];
5153
const insightsSummary = insightsData?.summary;
54+
const guardrailEvents = guardrailData?.events || [];
5255

5356
// Compute stats
5457
const stats = useMemo(() => {
@@ -92,11 +95,12 @@ export default function AuditLog() {
9295
/>
9396

9497
{/* Summary Stats */}
95-
<div className="grid grid-cols-2 gap-4 sm:grid-cols-5 mb-6">
98+
<div className="grid grid-cols-2 gap-4 sm:grid-cols-6 mb-6">
9699
<StatCard title="Total Events" value={stats.total} icon={<Shield size={22} />} color="primary" />
97100
<StatCard title="Agent Invocations" value={stats.invocations} icon={<CheckCircle size={22} />} color="success" />
98101
<StatCard title="Tool Executions" value={stats.toolExecs} icon={<Clock size={22} />} color="info" />
99102
<StatCard title="Permission Denied" value={stats.blocked} icon={<XCircle size={22} />} color="danger" />
103+
<StatCard title="Guardrail Blocks" value={guardrailEvents.length} icon={<ShieldAlert size={22} />} color="warning" />
100104
<StatCard title="Config Changes" value={stats.configChanges} icon={<AlertTriangle size={22} />} color="warning" />
101105
</div>
102106

@@ -107,6 +111,7 @@ export default function AuditLog() {
107111
{ id: 'timeline', label: 'Event Timeline', count: filtered.length },
108112
{ id: 'breakdown', label: 'Breakdown' },
109113
{ id: 'security', label: 'Security Alerts', count: stats.blocked || undefined },
114+
{ id: 'guardrail', label: 'Guardrail Events', count: guardrailEvents.length || undefined },
110115
]}
111116
activeTab={activeTab}
112117
onChange={setActiveTab}
@@ -327,6 +332,60 @@ export default function AuditLog() {
327332
</div>
328333
)}
329334

335+
{activeTab === 'guardrail' && (
336+
<div>
337+
<div className="rounded-xl bg-warning/5 border border-warning/20 px-4 py-3 text-xs text-warning mb-4 flex items-center gap-2">
338+
<ShieldAlert size={16} />
339+
<span>Bedrock Guardrail intercepts — every blocked user input and filtered agent output. Standard Runtime only. Exec Runtime has no guardrail restrictions.</span>
340+
</div>
341+
342+
{guardrailEvents.length === 0 ? (
343+
<div className="text-center py-12 text-text-muted">
344+
<ShieldAlert size={32} className="mx-auto mb-3 opacity-30" />
345+
<p className="text-sm">No guardrail blocks yet</p>
346+
<p className="text-xs mt-1">Blocks appear here when Standard Runtime agents trigger topic denial or PII rules.</p>
347+
</div>
348+
) : (
349+
<div className="space-y-2">
350+
{guardrailEvents.map(e => (
351+
<div key={e.id} className="flex items-start gap-4 rounded-xl bg-amber-500/5 border border-amber-500/20 px-4 py-3">
352+
<ShieldAlert size={18} className="text-amber-400 mt-0.5 shrink-0" />
353+
<div className="flex-1 min-w-0">
354+
<div className="flex items-center gap-2 flex-wrap mb-1">
355+
<span className="text-sm font-semibold text-amber-300">{e.actorName}</span>
356+
<Badge color="warning">{e.guardrailSource === 'INPUT' ? 'Input blocked' : 'Output filtered'}</Badge>
357+
{e.guardrailPolicy && <Badge color="default">{e.guardrailPolicy.replace(/-/g, ' ')}</Badge>}
358+
<span className="text-xs font-mono text-text-muted">{e.guardrailId} v{e.guardrailVersion}</span>
359+
</div>
360+
<p className="text-sm text-text-secondary truncate">{e.detail}</p>
361+
<p className="text-xs text-text-muted mt-1">{new Date(e.timestamp).toLocaleString()}</p>
362+
</div>
363+
<Badge color="danger">Blocked</Badge>
364+
</div>
365+
))}
366+
</div>
367+
)}
368+
369+
<div className="mt-4 rounded-xl bg-dark-bg p-4">
370+
<h4 className="text-sm font-medium text-text-primary mb-2">Guardrail Enforcement Summary</h4>
371+
<div className="grid grid-cols-3 gap-4 text-center">
372+
<div>
373+
<p className="text-2xl font-semibold text-amber-400">{guardrailEvents.length}</p>
374+
<p className="text-xs text-text-muted">Total Blocks</p>
375+
</div>
376+
<div>
377+
<p className="text-2xl font-semibold text-text-primary">{guardrailEvents.filter(e => e.guardrailSource === 'INPUT').length}</p>
378+
<p className="text-xs text-text-muted">Input Blocked</p>
379+
</div>
380+
<div>
381+
<p className="text-2xl font-semibold text-text-primary">{guardrailEvents.filter(e => e.guardrailSource === 'OUTPUT').length}</p>
382+
<p className="text-xs text-text-muted">Output Filtered</p>
383+
</div>
384+
</div>
385+
</div>
386+
</div>
387+
)}
388+
330389
{activeTab === 'security' && (
331390
<div>
332391
<p className="text-sm text-text-secondary mb-4">Permission denials and security-relevant events. These indicate policy enforcement working correctly, or potential unauthorized access attempts.</p>

0 commit comments

Comments
 (0)