Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
dc32455
feat(governance): optional governance layer for GAIA agents
dislovelhl Apr 20, 2026
ffd9628
Merge branch 'amd:main' into feat/optional-governance-layer
dislovelhl Apr 20, 2026
ebc069d
Fix CI: add gaia.governance to setup.py, apply black/isort formatting
dislovelhl Apr 20, 2026
942e693
harden governance layer: thread safety, explicit guards, remove dupli…
dislovelhl Apr 20, 2026
0eb7475
fix(tests): guard against _TOOL_REGISTRY.clear() in test_tool_decorator
dislovelhl Apr 21, 2026
8b2a733
style: apply black formatting to governance test files
dislovelhl Apr 21, 2026
50c7fc0
style: apply black formatting to integration tests and mcp_bridge
dislovelhl Apr 21, 2026
37d5d5f
Merge branch 'amd:main' into feat/optional-governance-layer
dislovelhl Apr 28, 2026
f242e28
fix(governance): harden error handling and align docs with additive tags
dislovelhl Apr 29, 2026
2ed500d
ci(test_api): cap job runtime at 30 minutes
dislovelhl Apr 29, 2026
ca941a9
refactor(governance): polish pass — drop dead code, tighten lock, dee…
dislovelhl Apr 29, 2026
5cdfee5
test(governance): cover hardened error paths and fail-closed branches
dislovelhl Apr 29, 2026
1d78faa
Merge branch 'feat/optional-governance-layer' of https://github.com/d…
dislovelhl Apr 29, 2026
15bc40b
fix(governance): distinguish reviewer-crash from reviewer-no in audit…
dislovelhl Apr 29, 2026
603bfaa
ci(claude): gate workflow to canonical amd/gaia repository
dislovelhl Apr 29, 2026
fc88d0c
feat(governance): bridge REVIEW to existing console confirmation surface
dislovelhl Apr 29, 2026
52f97a8
ci: re-trigger after workflow guard landed
dislovelhl Apr 29, 2026
713378b
feat(governance): emit policy_alert SSE event when BLOCK denies a tool
dislovelhl Apr 29, 2026
14d7b81
Merge pull request #4 from dislovelhl/feat/governance-policy-alert-ba…
dislovelhl Apr 29, 2026
f986efb
Merge pull request #3 from dislovelhl/feat/governance-review-bridge
dislovelhl Apr 30, 2026
d6c5090
Merge amd/main into governance PR
dislovelhl Apr 30, 2026
eb0b015
fix(ui): propagate AttributeError from _canonical_agent_type
dislovelhl Apr 30, 2026
d36da2f
fix(governance): replace lambda with def in _prompt_review (E731)
dislovelhl Apr 30, 2026
9a44cd1
feat(chat-ui): surface governance policy block alerts
dislovelhl May 4, 2026
c8c0a0a
Merge amd/main into feat/optional-governance-layer
dislovelhl May 4, 2026
ad798d5
Potential fix for pull request finding
dislovelhl May 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/guides/agent-ui.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,17 @@ Once the agent finds files — or you drag them into a session — it can index

Create, rename, search, export (Markdown/JSON), and delete sessions. Sessions persist across the CLI (`gaia chat`) and the Agent UI.

### Policy Alerts and Receipts

When a governance-enabled agent blocks a tool call, the Agent UI shows a
non-actionable policy alert instead of an approval prompt. Policy blocks appear
as inline **Policy Shield** activity cards, critical notifications, and a toast
with a **View receipt** link when a receipt ID is available.

Policy alerts are durable session history. If you reload the UI or reconnect
after a blocked request with no assistant text, the block reason, rule IDs,
policy version, and receipt ID remain attached to the assistant message.

Comment on lines +186 to +196
---

## Keyboard Shortcuts
Expand Down
4 changes: 2 additions & 2 deletions src/gaia/apps/webui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion src/gaia/apps/webui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@amd-gaia/agent-ui",
"version": "0.17.5",
"version": "0.17.6",
"type": "module",
"productName": "GAIA Agent UI",
"description": "Privacy-first agentic AI interface with document Q&A - runs 100% locally on AMD Ryzen AI",
Expand Down Expand Up @@ -52,6 +52,7 @@
"preview": "vite preview",
"start": "electron .",
"package": "npm run build && electron-builder --config electron-builder.yml",
"make": "npm run package",
"package:win": "npm run build && electron-builder --win --config electron-builder.yml",
"package:mac": "npm run build && electron-builder --mac --config electron-builder.yml",
"package:linux": "npm run build && electron-builder --linux --config electron-builder.yml",
Expand Down
9 changes: 9 additions & 0 deletions src/gaia/apps/webui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import { MobileAccessModal } from './components/MobileAccessModal';
import { ConnectionBanner } from './components/ConnectionBanner';
import { UpdateIndicator } from './components/UpdateIndicator';
import { PermissionPrompt } from './components/PermissionPrompt';
import { NotificationCenter } from './components/NotificationCenter';
import { useChatStore } from './stores/chatStore';
import { useNotificationStore } from './stores/notificationStore';
import * as api from './services/api';
import { log, logBanner } from './utils/logger';
import { getSessionHash, findSessionByHash } from './utils/format';
Expand Down Expand Up @@ -71,6 +73,8 @@ function App() {
setBackendConnected,
setAgents,
} = useChatStore();
const showNotificationPanel = useNotificationStore((s) => s.showPanel);
const setShowNotificationPanel = useNotificationStore((s) => s.setShowPanel);

// Load agent list on mount, then poll every 30s.
// Fingerprinting avoids re-renders when the list is unchanged.
Expand Down Expand Up @@ -542,6 +546,11 @@ function App() {
<AnimatedPresence show={showFileBrowser}>
<FileBrowser />
</AnimatedPresence>
<AnimatedPresence show={showNotificationPanel}>
<div className="notification-center-popover">
<NotificationCenter onClose={() => setShowNotificationPanel(false)} />
</div>
</AnimatedPresence>

{/* Mobile Access Modal */}
{!isMobile && (
Expand Down
81 changes: 80 additions & 1 deletion src/gaia/apps/webui/src/components/AgentActivity.css
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@
/* When activity is currently streaming or has errors, force full
opacity — the user genuinely needs to see those states. */
.agent-activity.active .agent-summary-bar,
.agent-activity.has-errors .agent-summary-bar {
.agent-activity.has-errors .agent-summary-bar,
.agent-activity.has-policy-alerts .agent-summary-bar {
opacity: 1;
}
/* Once expanded, the bar is the title for the panel below it — keep
Expand Down Expand Up @@ -82,6 +83,7 @@

.agent-icon-done { color: var(--text-muted); }
.agent-icon-error { color: var(--amd-red); }
.agent-activity.has-policy-alerts .agent-icon-done { color: var(--accent-yellow); }

/* Spinner */
.agent-spinner-wrap {
Expand Down Expand Up @@ -491,6 +493,83 @@
background: rgba(239, 68, 68, 0.06);
}

/* ── Flow: Policy Alert ─────────────────────────────────────────── */
.flow-policy-alert {
padding: 8px 10px;
border-radius: var(--radius-xs);
background: rgba(245, 158, 11, 0.06);
border: 1px solid rgba(245, 158, 11, 0.22);
font-family: var(--font-mono);
animation: errorSlideIn 300ms var(--ease);
}

[data-theme="dark"] .flow-policy-alert {
background: rgba(245, 158, 11, 0.08);
border-color: rgba(245, 158, 11, 0.28);
}

.flow-policy-alert-header {
display: flex;
align-items: center;
gap: 6px;
color: var(--accent-yellow);
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.7px;
}

.flow-policy-alert-title {
color: var(--text-primary);
}

.flow-policy-alert-decision {
margin-left: auto;
padding: 1px 6px;
border-radius: var(--radius-xs);
background: rgba(245, 158, 11, 0.14);
color: var(--accent-yellow);
}

.flow-policy-alert-body {
display: flex;
flex-direction: column;
gap: 5px;
margin-top: 7px;
font-size: 11px;
color: var(--text-secondary);
}

.flow-policy-alert-line {
display: flex;
gap: 8px;
align-items: baseline;
}

.flow-policy-alert-line span {
min-width: 48px;
color: var(--text-muted);
text-transform: uppercase;
font-size: 9px;
letter-spacing: 0.5px;
}

.flow-policy-alert-line code {
color: var(--text-primary);
word-break: break-word;
}

.flow-policy-alert-line em {
color: var(--accent-yellow);
font-style: normal;
}

.flow-policy-alert-reason {
color: var(--text-primary);
line-height: 1.5;
word-break: break-word;
}

/* ── Shared detail styles (reused from tool detail) ──────────── */
.detail-section-label {
display: block;
Expand Down
53 changes: 50 additions & 3 deletions src/gaia/apps/webui/src/components/AgentActivity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
Copy,
Check,
File,
ShieldAlert,
type LucideIcon,
} from 'lucide-react';
import type { AgentStep, CommandOutput, RetrievalChunk } from '../types';
Expand Down Expand Up @@ -155,6 +156,8 @@ export function AgentActivity({ steps, isActive, variant = 'inline' }: AgentActi
}, [steps]);

const toolSteps = displaySteps.filter((s) => s.type === 'tool');
const policySteps = displaySteps.filter((s) => s.type === 'policy_alert');
const hasPolicyAlerts = policySteps.length > 0;

// ── Filter state (for MCP tool visualization) ───────────────────
const [toolNameFilter, setToolNameFilter] = useState('');
Expand All @@ -176,6 +179,10 @@ export function AgentActivity({ steps, isActive, variant = 'inline' }: AgentActi
const errorSteps = displaySteps.filter((s) => s.type === 'error');
const hasErrors = errorSteps.length > 0;

useEffect(() => {
if (hasPolicyAlerts) setExpanded(true);
}, [hasPolicyAlerts]);

// Auto-expand native tools for visibility. MCP tools start collapsed
// by default (users expand on demand) to reduce noise in busy sessions.
useEffect(() => {
Expand Down Expand Up @@ -213,11 +220,13 @@ export function AgentActivity({ steps, isActive, variant = 'inline' }: AgentActi
// Build summary — always use stable step count so the bar doesn't
// visually change when transitioning from thinking to answer streaming.
const stepCount = displaySteps.length;
const summaryText = `${stepCount} step${stepCount !== 1 ? 's' : ''}`
+ (toolSteps.length > 0 ? ` \u00b7 ${toolSteps.length} tool${toolSteps.length !== 1 ? 's' : ''}` : '');
const summaryText = hasPolicyAlerts
? `Blocked by policy \u00b7 ${policySteps.length} alert${policySteps.length !== 1 ? 's' : ''}`
: `${stepCount} step${stepCount !== 1 ? 's' : ''}`
+ (toolSteps.length > 0 ? ` \u00b7 ${toolSteps.length} tool${toolSteps.length !== 1 ? 's' : ''}` : '');

return (
<div className={`agent-activity ${variant} ${isActive ? 'active' : 'done'} ${hasErrors ? 'has-errors' : ''}`}>
<div className={`agent-activity ${variant} ${isActive ? 'active' : 'done'} ${hasErrors ? 'has-errors' : ''} ${hasPolicyAlerts ? 'has-policy-alerts' : ''}`}>
{/* Summary bar */}
<button
className="agent-summary-bar"
Expand Down Expand Up @@ -290,6 +299,9 @@ export function AgentActivity({ steps, isActive, variant = 'inline' }: AgentActi
if (step.type === 'error') {
return <FlowError key={step.id} step={step} />;
}
if (step.type === 'policy_alert') {
return <FlowPolicyAlert key={step.id} step={step} />;
}
return null;
})}
</div>
Expand Down Expand Up @@ -553,6 +565,41 @@ function FlowError({ step }: { step: AgentStep }) {
);
}

function FlowPolicyAlert({ step }: { step: AgentStep }) {
return (
<div className="flow-policy-alert">
<div className="flow-policy-alert-header">
<ShieldAlert size={14} />
<span className="flow-policy-alert-title">Policy Shield</span>
<span className="flow-policy-alert-decision">{step.decision || 'BLOCK'}</span>
</div>
<div className="flow-policy-alert-body">
<div className="flow-policy-alert-line">
<span>Tool</span>
<code>{step.tool || 'unknown tool'}</code>
</div>
<div className="flow-policy-alert-reason">{step.reason || step.detail}</div>
{step.ruleIds && step.ruleIds.length > 0 && (
<div className="flow-policy-alert-line">
<span>Rules</span>
<code>{step.ruleIds.join(', ')}</code>
</div>
)}
{step.policyVersion && (
<div className="flow-policy-alert-line">
<span>Policy</span>
<code>{step.policyVersion}</code>
</div>
)}
<div className="flow-policy-alert-line">
<span>Receipt</span>
{step.receiptId ? <code>{step.receiptId}</code> : <em>Receipt unavailable</em>}
</div>
</div>
</div>
);
}

// ── Retrieval Chunks View ──────────────────────────────────────────────

function ChunksView({ chunks }: { chunks: RetrievalChunk[] }) {
Expand Down
28 changes: 28 additions & 0 deletions src/gaia/apps/webui/src/components/ChatView.css
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,34 @@
color: white;
}

/* ── Policy alert toast ─────────────────────────────────────────── */
.policy-alert-toast {
display: flex;
align-items: center;
gap: 12px;
border-color: rgba(245, 158, 11, 0.35);
pointer-events: all;
animation: toastIn 200ms var(--ease), toastOut 200ms var(--ease) 5s forwards;
}

.policy-alert-toast-link {
background: none;
border: 0;
color: var(--accent-yellow);
cursor: pointer;
font: inherit;
font-weight: 700;
padding: 0;
text-decoration: underline;
text-decoration-style: dotted;
}

.policy-alert-toast-missing {
color: var(--accent-yellow);
font-family: var(--font-mono);
font-size: 11px;
}

/* ── Responsive ──────────────────────────────────────────────────── */

@media (max-width: 768px) {
Expand Down
Loading
Loading