diff --git a/src/channels/web/static/app.js b/src/channels/web/static/app.js
index 8d19b497..bdf8b454 100644
--- a/src/channels/web/static/app.js
+++ b/src/channels/web/static/app.js
@@ -38,6 +38,7 @@ function authenticate() {
connectSSE();
connectLogSSE();
startGatewayStatusPolling();
+ checkTeeStatus();
loadThreads();
loadMemoryTree();
loadJobs();
@@ -2098,6 +2099,100 @@ document.getElementById('gateway-status-trigger').addEventListener('mouseleave',
document.getElementById('gateway-popover').classList.remove('visible');
});
+// --- TEE attestation ---
+
+let teeInfo = null;
+let teeReportCache = null;
+let teeReportLoading = false;
+
+function teeApiBase() {
+ var parts = window.location.hostname.split('.');
+ if (parts.length < 2) return null;
+ var domain = parts.slice(1).join('.');
+ return window.location.protocol + '//api.' + domain;
+}
+
+function teeInstanceName() {
+ return window.location.hostname.split('.')[0];
+}
+
+function checkTeeStatus() {
+ var base = teeApiBase();
+ if (!base) return;
+ var name = teeInstanceName();
+ fetch(base + '/instances/' + encodeURIComponent(name) + '/attestation').then(function(res) {
+ if (!res.ok) throw new Error(res.status);
+ return res.json();
+ }).then(function(data) {
+ teeInfo = data;
+ document.getElementById('tee-shield').style.display = 'flex';
+ }).catch(function() {});
+}
+
+function fetchTeeReport() {
+ if (teeReportCache) {
+ renderTeePopover(teeReportCache);
+ return;
+ }
+ if (teeReportLoading) return;
+ teeReportLoading = true;
+ var base = teeApiBase();
+ if (!base) return;
+ var popover = document.getElementById('tee-popover');
+ popover.innerHTML = '
Loading attestation report...
';
+ fetch(base + '/attestation/report').then(function(res) {
+ if (!res.ok) throw new Error(res.status);
+ return res.json();
+ }).then(function(data) {
+ teeReportCache = data;
+ renderTeePopover(data);
+ }).catch(function() {
+ popover.innerHTML = 'Could not load attestation report
';
+ }).finally(function() {
+ teeReportLoading = false;
+ });
+}
+
+function renderTeePopover(report) {
+ var popover = document.getElementById('tee-popover');
+ var digest = (teeInfo && teeInfo.image_digest) || 'N/A';
+ var fingerprint = report.tls_certificate_fingerprint || 'N/A';
+ var reportData = report.report_data || '';
+ var vmConfig = report.vm_config || 'N/A';
+ var truncated = reportData.length > 32 ? reportData.slice(0, 32) + '...' : reportData;
+ popover.innerHTML = ''
+ + '
'
+ + 'TEE Attestation
'
+ + 'Image Digest
'
+ + '
' + escapeHtml(digest) + '
'
+ + 'TLS Certificate Fingerprint
'
+ + '
' + escapeHtml(fingerprint) + '
'
+ + 'Report Data
'
+ + '
' + escapeHtml(truncated) + '
'
+ + 'VM Config
'
+ + '
' + escapeHtml(vmConfig) + '
'
+ + ''
+ + '
';
+}
+
+function copyTeeReport() {
+ if (!teeReportCache) return;
+ var combined = Object.assign({}, teeReportCache, teeInfo || {});
+ navigator.clipboard.writeText(JSON.stringify(combined, null, 2)).then(function() {
+ showToast('Attestation report copied', 'success');
+ }).catch(function() {
+ showToast('Failed to copy report', 'error');
+ });
+}
+
+document.getElementById('tee-shield').addEventListener('mouseenter', function() {
+ fetchTeeReport();
+ document.getElementById('tee-popover').classList.add('visible');
+});
+document.getElementById('tee-shield').addEventListener('mouseleave', function() {
+ document.getElementById('tee-popover').classList.remove('visible');
+});
+
// --- Extension install ---
function installExtension() {
diff --git a/src/channels/web/static/index.html b/src/channels/web/static/index.html
index 125dd586..d7f65025 100644
--- a/src/channels/web/static/index.html
+++ b/src/channels/web/static/index.html
@@ -40,6 +40,13 @@
+
Connected
diff --git a/src/channels/web/static/style.css b/src/channels/web/static/style.css
index d28edc2b..df3be9b8 100644
--- a/src/channels/web/static/style.css
+++ b/src/channels/web/static/style.css
@@ -189,6 +189,126 @@ body {
background: var(--danger);
}
+/* TEE Shield */
+.tee-shield {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 12px;
+ color: var(--success);
+ padding: 4px 10px;
+ border-radius: 12px;
+ background: rgba(63, 185, 80, 0.1);
+ border: 1px solid rgba(63, 185, 80, 0.25);
+ cursor: pointer;
+ position: relative;
+ margin-right: 8px;
+ transition: background 0.15s;
+}
+
+.tee-shield:hover {
+ background: rgba(63, 185, 80, 0.18);
+}
+
+.tee-shield svg {
+ flex-shrink: 0;
+}
+
+#tee-shield-label {
+ font-weight: 500;
+ white-space: nowrap;
+}
+
+.tee-popover {
+ display: none;
+ position: absolute;
+ top: 100%;
+ right: 0;
+ margin-top: 8px;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ padding: 16px;
+ min-width: 340px;
+ max-width: 420px;
+ z-index: 100;
+ box-shadow: var(--shadow);
+}
+
+.tee-popover.visible {
+ display: block;
+}
+
+.tee-popover-title {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--text);
+ margin-bottom: 12px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.tee-popover-title svg {
+ color: var(--success);
+}
+
+.tee-field {
+ margin-bottom: 10px;
+}
+
+.tee-field:last-child {
+ margin-bottom: 0;
+}
+
+.tee-field-label {
+ font-size: 11px;
+ font-weight: 500;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ margin-bottom: 3px;
+}
+
+.tee-field-value {
+ font-size: 12px;
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
+ color: var(--text);
+ word-break: break-all;
+ background: var(--bg);
+ padding: 4px 8px;
+ border-radius: 4px;
+ border: 1px solid var(--border);
+}
+
+.tee-popover-actions {
+ margin-top: 12px;
+ display: flex;
+ gap: 8px;
+}
+
+.tee-btn-copy {
+ padding: 4px 10px;
+ background: none;
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ color: var(--text-secondary);
+ cursor: pointer;
+ font-size: 11px;
+ transition: color 0.15s, border-color 0.15s;
+}
+
+.tee-btn-copy:hover {
+ color: var(--text);
+ border-color: var(--text-secondary);
+}
+
+.tee-popover-loading {
+ font-size: 12px;
+ color: var(--text-secondary);
+ padding: 8px 0;
+}
+
/* Tab Panels */
.tab-panel {
display: none;