1/** 2 * AI Act Readiness page â tri-jurisdiction (EU / UK / US) governance gap 3 * report for the customer's AI system. Declare the system's capabilities, 4 * get a control-by-control gap report (EU AI Act, UK principles + UK-GDPR, 5 * NIST AI RMF + US state law), a downloadable report and a sha256 6 * attestation. Certified baselines + compliance drift require Monitor+. 7 * 8 * Backend: GET /api/saas/agent-compliance/options 9 * POST /api/saas/agent-compliance (synchronous) 10 * POST /api/saas/agent-compliance/baseline (Monitor+) 11 * GET /api/saas/agent-compliance[/{report_id}] 12 */ 13function _acEsc(s) { 14 return String(s == null ? '' : s).replace(/[&<>"']/g, c => ( 15 { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); 16} 17 18const _AC_STATUS = { 19 satisfied: { icon: 'â ', colour: '#16c784', label: 'Satisfied' }, 20 partial: { icon: 'ð¡', colour: '#f7b500', label: 'Partial' }, 21 gap: { icon: 'ð´', colour: '#ff8c42', label: 'Gap' }, 22 violation: { icon: 'â', colour: '#ef4444', label: 'Violation' }, 23}; 24 25async function renderAgentCompliancePage(container) { 26 container.innerHTML = ` 27 <div class="page-header"> 28 <div> 29 <h1 class="page-title">AI Act Readiness</h1> 30 <p class="page-subtitle">Tri-jurisdiction readiness check of <em>your AI system</em> â 31 EU AI Act, UK principles + UK-GDPR, NIST AI RMF + US state law. 32 Declare what your system does; get a control-by-control gap report with an attestation.</p> 33 </div> 34 </div> 35 36 <div class="card" id="ac-form-card"> 37 <div class="flex gap-md" style="flex-wrap:wrap"> 38 <div class="form-group" style="min-width:220px;flex:1"> 39 <label class="form-label">System name</label> 40 <input id="ac-name" class="form-input" placeholder="e.g. support-copilot" autocomplete="off"> 41 </div> 42 <div class="form-group" style="min-width:160px"> 43 <label class="form-label">Your role</label> 44 <select id="ac-party" class="form-input"> 45 <option value="deployer" selected>Deployer (you use it)</option> 46 <option value="provider">Provider (you build it)</option> 47 <option value="both">Both</option> 48 </select> 49 </div> 50 <div class="form-group" style="min-width:160px"> 51 <label class="form-label">Risk tier</label> 52 <select id="ac-tier" class="form-input"> 53 <option value="high" selected>High-risk</option> 54 <option value="limited">Limited</option> 55 <option value="minimal">Minimal</option> 56 <option value="gpai">General-purpose AI</option> 57 </select> 58 </div> 59 <div class="form-group" style="min-width:180px"> 60 <label class="form-label">Jurisdictions</label> 61 <div id="ac-jur" style="display:flex;gap:12px;padding-top:8px"> 62 <label><input type="checkbox" value="eu" checked> EU</label> 63 <label><input type="checkbox" value="uk" checked> UK</label> 64 <label><input type="checkbox" value="us" checked> USA</label> 65 </div> 66 </div> 67 </div> 68 69 <div id="ac-caps" class="text-muted">Loading capability checklistâ¦</div> 70 71 <button id="ac-run" class="btn btn-primary" style="margin-top:16px">Run readiness check â</button> 72 <p class="text-muted" style="font-size:.78rem;margin:10px 0 0" id="ac-disclaimer"> 73 Decision-support gap assessment against machine-checkable controls â not legal advice.</p> 74 </div> 75 76 <div id="ac-results"></div> 77 78 <div class="card" id="ac-history-card"> 79 <h3 style="margin:0 0 12px">Recent reports</h3> 80 <div id="ac-history" class="text-muted">Loadingâ¦</div> 81 </div> 82 `; 83 84 let _options = null; 85 86 document.getElementById('ac-run').addEventListener('click', _acRun); 87 _acLoadOptions(); 88 _acLoadHistory(); 89 90 async function _acLoadOptions() { 91 const el = document.getElementById('ac-caps'); 92 try { 93 _options = await NAILAPI.get('/api/saas/agent-compliance/options'); 94 } catch (err) { 95 el.innerHTML = '<span class="text-muted">Could not load the checklist. Reload the page to retry.</span>'; 96 return; 97 } 98 const detected = new Set(_options.detected || []); 99 el.innerHTML = ` 100 <h4 style="margin:16px 0 4px">What does your system already have?</h4> 101 <p class="text-muted" style="font-size:.82rem;margin:0 0 10px"> 102 Tick what genuinely exists today â the report is only as honest as the answers. 103 ${detected.size ? 'Items marked <span style="color:#16c784">â</span> were detected from your Neuravant workspace.' : ''}</p> 104 <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(290px,1fr));gap:14px"> 105 ${(_options.capability_groups || []).map(g => { 106 const prohibited = g.group.toLowerCase().startsWith('prohibited'); 107 return ` 108 <div style="border:1px solid ${prohibited ? 'rgba(239,68,68,.4)' : 'rgba(127,127,127,.25)'};border-radius:8px;padding:10px 12px"> 109 <strong style="font-size:.85rem;${prohibited ? 'color:#ef4444' : ''}">${_acEsc(g.group)}</strong> 110 ${g.items.map(it => ` 111 <label style="display:flex;gap:8px;align-items:flex-start;margin-top:8px;font-size:.85rem;cursor:pointer"> 112 <input type="checkbox" class="ac-cap" value="${_acEsc(it.key)}" ${detected.has(it.key) ? 'checked' : ''}>
113 <span>${_acEsc(it.label)} 114 ${detected.has(it.key) ? ' <span title="Detected from your workspace" style="color:#16c784">â</span>' : ''}</span> 115 </label>`).join('')} 116 </div>`; 117 }).join('')} 118 </div>`; 119 } 120 121 async function _acRun() { 122 const name = document.getElementById('ac-name').value.trim() || 'my-ai-system'; 123 const jurisdictions = Array.from( 124 document.querySelectorAll('#ac-jur input:checked')).map(c => c.value); 125 if (!jurisdictions.length) { NAILUI.toast('Pick at least one jurisdiction', 'warning'); return; } 126 const capabilities = Array.from( 127 document.querySelectorAll('.ac-cap:checked')).map(c => c.value); 128 const body = { 129 name, 130 party: document.getElementById('ac-party').value, 131 risk_tier: document.getElementById('ac-tier').value, 132 jurisdictions, capabilities, 133 }; 134 const btn = document.getElementById('ac-run'); 135 btn.disabled = true; btn.textContent = 'Checkingâ¦'; 136 try { 137 const resp = await NAILAPI.post('/api/saas/agent-compliance', body); 138 if (!resp || !resp.success) { 139 throw new Error((resp && resp.message) || 'Could not run the readiness check'); 140 } 141 _acRenderResult(resp); 142 _acLoadHistory(); 143 } catch (err) { 144 NAILUI.toast(err.message || 'Readiness check failed', 'error'); 145 } finally { 146 btn.disabled = false; btn.textContent = 'Run readiness check â'; 147 } 148 } 149 150 function _acRenderResult(r) { 151 const res = r.result || {}; 152 const s = r.summary || {}; 153 const viol = s.violation || 0, gaps = (s.gap || 0) + (s.partial || 0); 154 const banner = r.compliant 155 ? { txt: 'â No gaps against the applicable controls', col: '#16c784' } 156 : viol 157 ? { txt: `â ${viol} prohibited-practice violation(s) â seek legal advice before deploying`, col: '#ef4444' } 158 : { txt: `ð¡ ${gaps} control(s) not yet satisfied`, col: '#f7b500' }; 159 160 const drift = r.drift && r.drift.has_drift ? ` 161 <div style="border:1px solid rgba(239,68,68,.5);border-radius:8px;padding:10px 14px;margin:12px 0"> 162 <strong style="color:#ef4444">Compliance drift vs your certified baseline</strong> 163 <ul style="margin:8px 0 0 18px;font-size:.85rem"> 164 ${r.drift.drifts.map(d => `<li><code>${_acEsc(d.control_id)}</code> ${_acEsc(d.kind)}: 165 ${_acEsc(d.from)} â ${_acEsc(d.to)}</li>`).join('')} 166 </ul> 167 </div>` : ''; 168 169 const rows = (res.findings || []).map(f => { 170 const st = _AC_STATUS[f.status] || { icon: 'â¹ï¸', colour: '#9fb0d6', label: f.status }; 171 const missing = (f.missing_capabilities || []).join(', '); 172 const trig = (f.triggered_prohibitions || []).join(', '); 173 return `<tr> 174 <td style="white-space:nowrap"><span style="color:${st.colour}">${st.icon} ${_acEsc(st.label)}</span></td> 175 <td style="white-space:nowrap"><code>${_acEsc(f.control_id)}</code></td> 176 <td>${_acEsc(f.reference)} â ${_acEsc(f.title)} 177 ${trig ? `<br><span style="color:#ef4444;font-size:.8rem">prohibited: ${_acEsc(trig)}</span>` : ''} 178 ${missing ? `<br><span class="text-muted" style="font-size:.8rem">missing: ${_acEsc(missing)}</span>` : ''}</td> 179 </tr>`; 180 }).join(''); 181 182 document.getElementById('ac-results').innerHTML = ` 183 <div class="card"> 184 <div class="flex items-center gap-sm" style="justify-content:space-between;flex-wrap:wrap"> 185 <h3 style="margin:0;color:${banner.col}">${banner.txt}</h3> 186 <div class="flex gap-sm"> 187 <button class="btn" id="ac-download">⬠Download report (.md)</button> 188 <button class="btn" id="ac-baseline" title="Monitor plan and above">ð Certify as baseline</button> 189 </div> 190 </div> 191 <p class="text-muted" style="font-size:.82rem;margin:8px 0 0"> 192 ${Object.entries(s).map(([k, v]) => `${_acEsc(k)}: <strong>${v}</strong>`).join(' · ')} 193 · attestation <code>sha256:${_acEsc((r.attestation || {}).content_hash || '').slice(0, 16)}â¦</code></p> 194 ${drift} 195 <div style="overflow-x:auto;margin-top:12px"><table class="table" style="width:100%"> 196 <thead><tr><th>
196Status</th><th>Control</th><th>Requirement</th></tr></thead> 197 <tbody>${rows}</tbody> 198 </table></div> 199 <p class="text-muted" style="font-size:.78rem;margin-top:10px">${_acEsc(r.disclaimer || '')}</p> 200 </div>`; 201 202 document.getElementById('ac-download').addEventListener('click', () => { 203 const blob = new Blob([r.markdown || ''], { type: 'text/markdown' }); 204 const a = document.createElement('a'); 205 a.href = URL.createObjectURL(blob); 206 a.download = `${(res.profile || 'report').replace(/[^\w.-]+/g, '_')}-compliance.md`; 207 a.click(); 208 URL.revokeObjectURL(a.href); 209 }); 210 document.getElementById('ac-baseline').addEventListener('click', async () => { 211 try { 212 const resp = await NAILAPI.post('/api/saas/agent-compliance/baseline', 213 { report_id: r.report_id }); 214 if (!resp || !resp.success) { 215 throw new Error((resp && resp.message) || 'Could not certify baseline'); 216 } 217 NAILUI.toast(`Baseline certified for ${resp.name} â future reports show drift`, 'success'); 218 } catch (err) { 219 NAILUI.toast(err.message || 'Baselines require the Monitor plan or above', 'warning'); 220 } 221 }); 222 } 223 224 async function _acLoadHistory() { 225 const el = document.getElementById('ac-history'); 226 if (!el) return; 227 try { 228 const r = await NAILAPI.get('/api/saas/agent-compliance'); 229 const reports = (r && r.reports) || []; 230 const baselined = new Set(((r && r.baselines) || []).map(b => b.report_id)); 231 if (!reports.length) { el.innerHTML = '<span class="text-muted">No reports yet.</span>'; return; } 232 el.innerHTML = ` 233 <div style="overflow-x:auto"><table class="table" style="width:100%"> 234 <thead><tr><th>System</th><th>Scope</th><th>Result</th><th>When</th></tr></thead> 235 <tbody>${reports.map(rep => { 236 const s = rep.summary || {}; 237 const badge = rep.compliant 238 ? '<span style="color:#16c784">â compliant</span>' 239 : (s.violation ? `<span style="color:#ef4444">â ${s.violation} violation(s)</span>` 240 : `<span style="color:#f7b500">ð¡ ${(s.gap || 0) + (s.partial || 0)} gap(s)</span>`); 241 return `<tr> 242 <td>${_acEsc(rep.name)}${baselined.has(rep.report_id) ? ' <span title="Certified baseline">ð</span>' : ''}</td> 243 <td style="text-transform:uppercase;font-size:.8rem">${_acEsc(rep.jurisdictions)} · ${_acEsc(rep.party)} · ${_acEsc(rep.risk_tier)}</td> 244 <td>${badge}</td> 245 <td class="text-muted" style="font-size:.82rem">${_acEsc((rep.created_at || '').replace('T', ' ').slice(0, 16))}</td> 246 </tr>`; 247 }).join('')}</tbody> 248 </table></div>`; 249 } catch (err) { 250 el.innerHTML = '<span class="text-muted">Could not load history.</span>'; 251 } 252 } 253} 254 255NAILRouter.registerPage('agent-compliance', renderAgentCompliancePage);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.