PageSourceSearch

https://neuravant.ai/static/js/pages/agent-compliance.js?v=1

js neuravant.ai collected 2026-10-05 13:06:28 UTC 13,215 bytes, 255 lines download raw bytes

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        { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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          &nbsp;·&nbsp; 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.