Spaces:
Running
Running
Display scaling, View menu, window placement right of icons
Browse files- app.js +7 -3
- display.js +26 -0
- index.html +1 -0
- server.py +1 -1
- settings.js +366 -364
app.js
CHANGED
|
@@ -69,9 +69,12 @@ function openApp(id) {
|
|
| 69 |
if (OPEN[id]) { OPEN[id].classList.remove('min'); focusWin(OPEN[id]); return OPEN[id]; }
|
| 70 |
const n = Object.keys(OPEN).length, D = $('#desk'), DW = D.clientWidth - 110, DH = D.clientHeight - 116;
|
| 71 |
const w = el('div', {class:'win', role:'dialog', 'aria-label':app.name(), 'data-app':id});
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
| 73 |
w.style.height = Math.min(app.h || 460, DH - 24) + 'px';
|
| 74 |
-
w.style.left =
|
| 75 |
w.style.top = (30 + Math.max(8, Math.min(12 + n * 30, DH - 200))) + 'px';
|
| 76 |
const tb = el('div', {class:'tb', ondblclick:e => { if (!e.target.closest('button')) w.classList.toggle('max'); }},
|
| 77 |
el('span', {class:'lights'}, el('button', {class:'c', title:t('close'), 'aria-label':t('close'), onclick:() => closeWin(id)}),
|
|
@@ -366,6 +369,7 @@ function menus() {
|
|
| 366 |
['File', [[t('newTerm'), () => openApp('term')], [t('closeAll'), () => Object.keys(OPEN).forEach(closeWin)]]],
|
| 367 |
['Kernel', [[t('mHalt'), () => $('#kKill').click()], [t('mResume'), () => $('#kResume').click()], [t('mVerify'), () => openApp('audit')]]],
|
| 368 |
['Apps', APPS.map(a => [a.name(), () => openApp(a.id)])],
|
|
|
|
| 369 |
['Help', [[t('mHelp'), () => openApp('term')], ['About', () => openApp('about')]]]];
|
| 370 |
for (const [name, items] of M) { const mi = el('div', {class:'mitem'});
|
| 371 |
mi.append(el('button', {onclick:e => { e.stopPropagation(); const o = mi.classList.contains('open'); document.querySelectorAll('.mitem.open').forEach(x => x.classList.remove('open')); if (!o) mi.classList.add('open'); }}, name),
|
|
@@ -410,7 +414,7 @@ function boot() {
|
|
| 410 |
document.addEventListener('click', e => { $('#ctx').classList.remove('show'); document.querySelectorAll('.mitem.open').forEach(m => m.classList.remove('open')); if (!e.target.closest('#start')) $('#start').classList.remove('show'); });
|
| 411 |
const tick = () => { $('#clock').textContent = new Date().toTimeString().slice(0, 5); }; tick(); setInterval(tick, 10000);
|
| 412 |
$('#banner').textContent = t('replayBanner'); $('#banner').classList.add('show'); setTimeout(() => $('#banner').classList.remove('show'), 6000);
|
| 413 |
-
refreshTray(); renderDock(); { const tw = openApp('term'), gw = openApp('guard'); if (!isPhone()) { const D = $('#desk'), L =
|
| 414 |
const sp = el('div', {id:'splash', role:'dialog', onclick:() => sp.remove()}, el('div', {class:'sp'}, el('div', {class:'spbar'}), ...copyNodes(), el('div', {class:'small muted', style:'margin-top:14px'}, t('booting'))));
|
| 415 |
if (!new URLSearchParams(location.search).has('nosplash')) { document.body.append(sp); setTimeout(() => sp.remove(), 4500); }
|
| 416 |
window.VK = {openApp, verifyChain};
|
|
|
|
| 69 |
if (OPEN[id]) { OPEN[id].classList.remove('min'); focusWin(OPEN[id]); return OPEN[id]; }
|
| 70 |
const n = Object.keys(OPEN).length, D = $('#desk'), DW = D.clientWidth - 110, DH = D.clientHeight - 116;
|
| 71 |
const w = el('div', {class:'win', role:'dialog', 'aria-label':app.name(), 'data-app':id});
|
| 72 |
+
const IC = $('#icons'), IR = (!isPhone() && IC) ? IC.offsetLeft + IC.offsetWidth + 12 : 8;
|
| 73 |
+
const AR = (!isPhone() && $('#arch') && $('#arch').offsetWidth) ? $('#arch').offsetWidth + 24 : 0;
|
| 74 |
+
const FW = Math.max(320, D.clientWidth - IR - AR - 12);
|
| 75 |
+
w.style.width = Math.min(app.w || 560, FW) + 'px';
|
| 76 |
w.style.height = Math.min(app.h || 460, DH - 24) + 'px';
|
| 77 |
+
w.style.left = (isPhone() ? 8 : Math.min(IR + n * 30, IR + Math.max(0, FW - Math.min(app.w || 560, FW)))) + 'px';
|
| 78 |
w.style.top = (30 + Math.max(8, Math.min(12 + n * 30, DH - 200))) + 'px';
|
| 79 |
const tb = el('div', {class:'tb', ondblclick:e => { if (!e.target.closest('button')) w.classList.toggle('max'); }},
|
| 80 |
el('span', {class:'lights'}, el('button', {class:'c', title:t('close'), 'aria-label':t('close'), onclick:() => closeWin(id)}),
|
|
|
|
| 369 |
['File', [[t('newTerm'), () => openApp('term')], [t('closeAll'), () => Object.keys(OPEN).forEach(closeWin)]]],
|
| 370 |
['Kernel', [[t('mHalt'), () => $('#kKill').click()], [t('mResume'), () => $('#kResume').click()], [t('mVerify'), () => openApp('audit')]]],
|
| 371 |
['Apps', APPS.map(a => [a.name(), () => openApp(a.id)])],
|
| 372 |
+
['View', window.VK_DISPLAY.modes.map(m => [(m === 'auto' ? 'Display scale: Auto' : 'Display scale: ' + Math.round(parseFloat(m) * 100) + '%') + (window.VK_DISPLAY.get() === m ? ' ✓' : ''), () => { window.VK_DISPLAY.set(m); toast('Display scale: ' + (m === 'auto' ? 'Auto' : Math.round(parseFloat(m) * 100) + '%')); location.reload(); }])],
|
| 373 |
['Help', [[t('mHelp'), () => openApp('term')], ['About', () => openApp('about')]]]];
|
| 374 |
for (const [name, items] of M) { const mi = el('div', {class:'mitem'});
|
| 375 |
mi.append(el('button', {onclick:e => { e.stopPropagation(); const o = mi.classList.contains('open'); document.querySelectorAll('.mitem.open').forEach(x => x.classList.remove('open')); if (!o) mi.classList.add('open'); }}, name),
|
|
|
|
| 414 |
document.addEventListener('click', e => { $('#ctx').classList.remove('show'); document.querySelectorAll('.mitem.open').forEach(m => m.classList.remove('open')); if (!e.target.closest('#start')) $('#start').classList.remove('show'); });
|
| 415 |
const tick = () => { $('#clock').textContent = new Date().toTimeString().slice(0, 5); }; tick(); setInterval(tick, 10000);
|
| 416 |
$('#banner').textContent = t('replayBanner'); $('#banner').classList.add('show'); setTimeout(() => $('#banner').classList.remove('show'), 6000);
|
| 417 |
+
refreshTray(); renderDock(); { const tw = openApp('term'), gw = openApp('guard'); if (!isPhone()) { const D = $('#desk'), L = $('#icons').offsetLeft + $('#icons').offsetWidth + 12, AW = ($('#arch') && $('#arch').offsetWidth) ? $('#arch').offsetWidth + 24 : 0, W = Math.max(480, Math.min(D.clientWidth - L - AW - 16, Math.round(D.clientWidth * 0.72))), H = Math.max(420, D.clientHeight - 110); gw.style.left = L + 'px'; gw.style.top = '38px'; gw.style.width = W + 'px'; gw.style.height = H + 'px'; tw.style.left = (L + 40) + 'px'; tw.style.top = '70px'; tw.style.width = Math.min(620, W - 60) + 'px'; tw.style.height = Math.min(420, H - 60) + 'px'; } focusWin(gw); }
|
| 418 |
const sp = el('div', {id:'splash', role:'dialog', onclick:() => sp.remove()}, el('div', {class:'sp'}, el('div', {class:'spbar'}), ...copyNodes(), el('div', {class:'small muted', style:'margin-top:14px'}, t('booting'))));
|
| 419 |
if (!new URLSearchParams(location.search).has('nosplash')) { document.body.append(sp); setTimeout(() => sp.remove(), 4500); }
|
| 420 |
window.VK = {openApp, verifyChain};
|
display.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Display scaling: lay the desktop out at a virtual 1920x1080 and scale it to the viewport (0.7 to 1.0).
|
| 2 |
+
Modes: auto (default), 1, 0.9, 0.8, 0.7. Phones (< 768 px) keep the mobile layout without scaling. */
|
| 3 |
+
(() => {
|
| 4 |
+
'use strict';
|
| 5 |
+
const KEY = 'vk_scale';
|
| 6 |
+
const get = () => { try { return localStorage.getItem(KEY) || 'auto'; } catch (e) { return 'auto'; } };
|
| 7 |
+
function apply() {
|
| 8 |
+
const de = document.documentElement, b = document.body;
|
| 9 |
+
if (!b) return;
|
| 10 |
+
const vw = window.innerWidth, vh = window.innerHeight, m = get();
|
| 11 |
+
let s = 1;
|
| 12 |
+
if (vw >= 768) s = m === 'auto' ? Math.max(0.7, Math.min(1, Math.min(vw / 1920, vh / 1080))) : Math.max(0.7, Math.min(1, parseFloat(m) || 1));
|
| 13 |
+
if (s >= 0.999) { de.style.zoom = ''; b.style.width = ''; b.style.height = ''; de.classList.remove('vk-scaled'); }
|
| 14 |
+
else { de.style.zoom = String(s); b.style.width = (vw / s) + 'px'; b.style.height = (vh / s) + 'px'; de.classList.add('vk-scaled'); }
|
| 15 |
+
de.style.setProperty('--vk-scale', String(s));
|
| 16 |
+
window.VK_SCALE = s;
|
| 17 |
+
}
|
| 18 |
+
window.VK_DISPLAY = { get, set(v) { try { localStorage.setItem(KEY, v); } catch (e) {} apply(); window.dispatchEvent(new Event('vk-display')); }, apply, modes: ['auto', '1', '0.9', '0.8', '0.7'] };
|
| 19 |
+
window.addEventListener('resize', apply);
|
| 20 |
+
document.addEventListener('DOMContentLoaded', apply);
|
| 21 |
+
apply();
|
| 22 |
+
const css = document.createElement('style');
|
| 23 |
+
/* keep small labels readable when scaled down (effective size about 11 px or more) */
|
| 24 |
+
css.textContent = `html.vk-scaled .icon span{font-size:calc(11.5px / var(--vk-scale))}html.vk-scaled #menubar,html.vk-scaled .win .tb b{font-size:calc(11.5px / var(--vk-scale))}html.vk-scaled .win .body{font-size:calc(11.5px / var(--vk-scale))}`;
|
| 25 |
+
document.head.append(css);
|
| 26 |
+
})();
|
index.html
CHANGED
|
@@ -80,6 +80,7 @@
|
|
| 80 |
<script src="i18n.js"></script>
|
| 81 |
<script src="demo_data.js"></script>
|
| 82 |
<script src="common.js"></script>
|
|
|
|
| 83 |
<script src="backdoor.js"></script>
|
| 84 |
<script src="terminal.js"></script>
|
| 85 |
<script src="live_apps.js"></script>
|
|
|
|
| 80 |
<script src="i18n.js"></script>
|
| 81 |
<script src="demo_data.js"></script>
|
| 82 |
<script src="common.js"></script>
|
| 83 |
+
<script src="display.js"></script>
|
| 84 |
<script src="backdoor.js"></script>
|
| 85 |
<script src="terminal.js"></script>
|
| 86 |
<script src="live_apps.js"></script>
|
server.py
CHANGED
|
@@ -11,7 +11,7 @@ RATE = int(os.environ.get("RATE_PER_10MIN", "20"))
|
|
| 11 |
TIMEOUT = float(os.environ.get("BACKEND_TIMEOUT", "300"))
|
| 12 |
GET_OK = {"health", "status", "audit", "store/catalog", "browser/img", "browser/res"}
|
| 13 |
POST_OK = {"chat", "scenario", "kill", "resume", "tamper", "reset", "store/submit", "store/publish", "store/app", "store/install", "gate/decide", "vmask", "game/hint", "game/cheat", "browser/open", "browser/agent", "browser/snap", "browser/policy", "settings/get", "settings/revoke", "settings/grant", "settings/egress", "settings/policy", "settings/locked", "rsi/state", "rsi/propose", "rsi/apply", "rsi/rollback", "rsi/reset", "ztc/status", "ztc/judge", "ztc/run", "backdoor/run", "backdoor/scan", "backdoor/wake"}
|
| 14 |
-
STATIC = {"index.html", "app.js", "ag.js", "store.js", "common.js", "terminal.js", "mines.js", "browser.js", "settings.js", "live_apps.js", "game.js", "rsi.js", "ztc.js", "backdoor.js", "backdoor.css", "backdoor_metrics.json", "downloads/vdrpack.py", "demo/poisoned-page", "samples/weather-plus.vdr", "samples/weather-plus-tampered.vdr", "i18n.js", "demo_data.js", "styles.css", "ag_replay.json"}
|
| 15 |
HITS = {}
|
| 16 |
|
| 17 |
|
|
|
|
| 11 |
TIMEOUT = float(os.environ.get("BACKEND_TIMEOUT", "300"))
|
| 12 |
GET_OK = {"health", "status", "audit", "store/catalog", "browser/img", "browser/res"}
|
| 13 |
POST_OK = {"chat", "scenario", "kill", "resume", "tamper", "reset", "store/submit", "store/publish", "store/app", "store/install", "gate/decide", "vmask", "game/hint", "game/cheat", "browser/open", "browser/agent", "browser/snap", "browser/policy", "settings/get", "settings/revoke", "settings/grant", "settings/egress", "settings/policy", "settings/locked", "rsi/state", "rsi/propose", "rsi/apply", "rsi/rollback", "rsi/reset", "ztc/status", "ztc/judge", "ztc/run", "backdoor/run", "backdoor/scan", "backdoor/wake"}
|
| 14 |
+
STATIC = {"index.html", "app.js", "ag.js", "store.js", "common.js", "display.js", "terminal.js", "mines.js", "browser.js", "settings.js", "live_apps.js", "game.js", "rsi.js", "ztc.js", "backdoor.js", "backdoor.css", "backdoor_metrics.json", "downloads/vdrpack.py", "demo/poisoned-page", "samples/weather-plus.vdr", "samples/weather-plus-tampered.vdr", "i18n.js", "demo_data.js", "styles.css", "ag_replay.json"}
|
| 15 |
HITS = {}
|
| 16 |
|
| 17 |
|
settings.js
CHANGED
|
@@ -1,364 +1,366 @@
|
|
| 1 |
-
/* ViKernel Desktop: System Settings app. Non-critical settings change here;
|
| 2 |
-
critical ones are sent to the real ViKernel gate, which denies them. */
|
| 3 |
-
(() => {
|
| 4 |
-
'use strict';
|
| 5 |
-
const LS = {
|
| 6 |
-
get(k, d) { try { const v = localStorage.getItem(k); return v === null ? d : v; } catch (e) { return d; } },
|
| 7 |
-
set(k, v) { try { localStorage.setItem(k, v); } catch (e) {} },
|
| 8 |
-
};
|
| 9 |
-
const LANG = LS.get('vk_lang', 'en') === 'ko' ? 'ko' : 'en';
|
| 10 |
-
const STR = {
|
| 11 |
-
en: {
|
| 12 |
-
title: 'System Settings', about: 'About', model: 'Model & kernel', sec: 'Security & privacy', net: 'Network egress',
|
| 13 |
-
pol: 'Policies', kill: 'Kill switch', audit: 'Audit', look: 'Appearance', apps: 'Apps', upd: 'Updates',
|
| 14 |
-
loading: 'Loading...', err: 'Could not reach the backend. It may be starting; try again.', rate: 'Too many requests. Wait a moment.',
|
| 15 |
-
retry: 'Retry', locked: 'Locked by ViKernel', needKey: 'requires a signed operator key', close: 'Close',
|
| 16 |
-
asking: 'Asking the kernel...', verdict: 'Verdict', rule: 'rule', auditN: 'audit #',
|
| 17 |
-
},
|
| 18 |
-
ko: {
|
| 19 |
-
title: '시스템 설정', about: '정보', model: '모델과 커널', sec: '보안과 개인정보', net: '외부 접속',
|
| 20 |
-
pol: '정책', kill: '킬 스위치', audit: '감사 기록', look: '화면', apps: '앱', upd: '업데이트',
|
| 21 |
-
loading: '불러오는 중...', err: '백엔드에 닿지 못했습니다. 시작 중일 수 있습니다. 다시 시도하십시오.', rate: '요청이 너무 많습니다. 잠시 기다리십시오.',
|
| 22 |
-
retry: '다시 시도', locked: 'ViKernel 이 잠금', needKey: '서명된 운영자 키가 필요합니다', close: '닫기',
|
| 23 |
-
asking: '커널에 묻는 중...', verdict: '판정', rule: '규칙', auditN: '감사 #',
|
| 24 |
-
},
|
| 25 |
-
};
|
| 26 |
-
const T = k => (STR[LANG][k] || STR.en[k] || k);
|
| 27 |
-
|
| 28 |
-
/* Appearance: apply on load */
|
| 29 |
-
const ACCENTS = ['#4338CA', '#0EA5E9', '#10B981', '#F43F5E', '#F59E0B', '#7C3AED'];
|
| 30 |
-
const WALLS = {
|
| 31 |
-
aurora: 'linear-gradient(135deg,#c7d2fe 0%,#e0e7ff 40%,#cffafe 100%)',
|
| 32 |
-
dusk: 'linear-gradient(160deg,#312e81 0%,#7c3aed 55%,#f43f5e 100%)',
|
| 33 |
-
slate: 'linear-gradient(180deg,#0f172a 0%,#334155 100%)',
|
| 34 |
-
};
|
| 35 |
-
function applyTheme() {
|
| 36 |
-
const t = LS.get('vk_theme', 'system');
|
| 37 |
-
const dark = t === 'dark' || (t === 'system' && window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches);
|
| 38 |
-
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
|
| 39 |
-
}
|
| 40 |
-
function applyLook() {
|
| 41 |
-
applyTheme();
|
| 42 |
-
const a = LS.get('vk_accent', '');
|
| 43 |
-
if (a && ACCENTS.includes(a)) document.documentElement.style.setProperty('--indigo', a);
|
| 44 |
-
const w = LS.get('vk_wall', '');
|
| 45 |
-
const d = document.getElementById('desk');
|
| 46 |
-
if (d && WALLS[w]) d.style.background = WALLS[w];
|
| 47 |
-
}
|
| 48 |
-
applyLook();
|
| 49 |
-
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', applyLook);
|
| 50 |
-
try { matchMedia('(prefers-color-scheme: dark)').addEventListener('change', applyTheme); } catch (e) {}
|
| 51 |
-
|
| 52 |
-
const css = `
|
| 53 |
-
.vs2-root{display:flex;height:100%;min-height:0;font-size:13px;color:var(--vs2-fg);background:var(--vs2-bg);
|
| 54 |
-
--vs2-bg:#fff;--vs2-fg:#0f172a;--vs2-mut:#64748b;--vs2-line:#e2e8f0;--vs2-side:#f8fafc;--vs2-card:#fff;--vs2-soft:#f1f5f9}
|
| 55 |
-
[data-theme=dark] .vs2-root{--vs2-bg:#0f172a;--vs2-fg:#e2e8f0;--vs2-mut:#94a3b8;--vs2-line:#334155;--vs2-side:#111827;--vs2-card:#1e293b;--vs2-soft:#1e293b}
|
| 56 |
-
.vs2-side{width:210px;flex:none;background:var(--vs2-side);border-right:1px solid var(--vs2-line);padding:10px 8px;overflow:auto;outline:none}
|
| 57 |
-
.vs2-nav{display:flex;align-items:center;gap:8px;width:100%;padding:8px 10px;border:0;border-radius:8px;background:none;color:inherit;font:inherit;text-align:left;cursor:pointer}
|
| 58 |
-
.vs2-nav:hover{background:var(--vs2-soft)}.vs2-nav.on{background:var(--indigo);color:#fff}
|
| 59 |
-
.vs2-nav:focus-visible{outline:2px solid var(--indigo);outline-offset:1px}
|
| 60 |
-
.vs2-main{flex:1;min-width:0;overflow:auto;padding:18px 22px}
|
| 61 |
-
.vs2-main h2{margin:0 0 4px;font-size:20px}.vs2-sub{color:var(--vs2-mut);margin:0 0 14px}
|
| 62 |
-
.vs2-card{background:var(--vs2-card);border:1px solid var(--vs2-line);border-radius:12px;padding:12px 14px;margin-bottom:12px}
|
| 63 |
-
.vs2-row{display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid var(--vs2-line)}.vs2-row:last-child{border-bottom:0}
|
| 64 |
-
.vs2-row .vs2-k{flex:1;min-width:0}.vs2-row small{display:block;color:var(--vs2-mut)}
|
| 65 |
-
.vs2-mono{font-family:ui-monospace,Consolas,monospace;font-size:12px;word-break:break-all}
|
| 66 |
-
.vs2-btn{border:1px solid var(--vs2-line);background:var(--vs2-card);color:inherit;border-radius:8px;padding:6px 12px;font:inherit;cursor:pointer}
|
| 67 |
-
.vs2-btn:hover{border-color:var(--indigo)}.vs2-btn.pri{background:var(--indigo);border-color:var(--indigo);color:#fff}
|
| 68 |
-
.vs2-btn.dng{background:#e11d48;border-color:#e11d48;color:#fff}.vs2-btn:disabled{opacity:.5;cursor:default}
|
| 69 |
-
.vs2-sw{position:relative;width:40px;height:22px;flex:none;border-radius:11px;border:0;background:#cbd5e1;cursor:pointer;transition:background .15s}
|
| 70 |
-
.vs2-sw::after{content:"";position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:#fff;transition:left .15s}
|
| 71 |
-
.vs2-sw[aria-checked=true]{background:var(--indigo)}.vs2-sw[aria-checked=true]::after{left:21px}.vs2-sw:disabled{opacity:.5}
|
| 72 |
-
.vs2-lockrow{cursor:pointer}.vs2-lockrow:hover{background:var(--vs2-soft)}
|
| 73 |
-
.vs2-lk{display:inline-flex;align-items:center;gap:4px;color:#b45309;font-weight:600;font-size:12px}
|
| 74 |
-
.vs2-pill{display:inline-block;padding:2px 10px;border-radius:999px;font-weight:700;font-size:12px}
|
| 75 |
-
.vs2-ok{background:#d1fae5;color:#065f46}.vs2-bad{background:#ffe4e6;color:#9f1239}.vs2-unk{background:var(--vs2-soft);color:var(--vs2-mut)}
|
| 76 |
-
.vs2-radio{display:block;width:100%;text-align:left;border:2px solid var(--vs2-line);border-radius:12px;padding:12px;margin-bottom:10px;background:var(--vs2-card);color:inherit;font:inherit;cursor:pointer}
|
| 77 |
-
.vs2-radio.on{border-color:var(--indigo)}
|
| 78 |
-
.vs2-in{border:1px solid var(--vs2-line);border-radius:8px;padding:6px 10px;font:inherit;background:var(--vs2-bg);color:inherit;min-width:0}
|
| 79 |
-
.vs2-swatch{width:26px;height:26px;border-radius:50%;border:2px solid transparent;cursor:pointer}.vs2-swatch.on{border-color:var(--vs2-fg)}
|
| 80 |
-
.vs2-wall{width:70px;height:44px;border-radius:8px;border:2px solid transparent;cursor:pointer}.vs2-wall.on{border-color:var(--indigo)}
|
| 81 |
-
.vs2-note{color:var(--vs2-mut);font-size:12px;margin-top:6px}.vs2-res{margin-top:8px;font-size:12px}
|
| 82 |
-
.vs2-table{width:100%;border-collapse:collapse}.vs2-table td,.vs2-table th{text-align:left;padding:6px;border-bottom:1px solid var(--vs2-line);vertical-align:top}
|
| 83 |
-
.vs2-empty{text-align:center;padding:30px 10px;color:var(--vs2-mut)}
|
| 84 |
-
.vs2-modal{position:absolute;inset:0;background:rgba(15,23,42,.45);display:flex;align-items:center;justify-content:center;z-index:5}
|
| 85 |
-
.vs2-dlg{background:var(--vs2-card);color:var(--vs2-fg);border-radius:14px;padding:20px;width:min(420px,92%);box-shadow:0 20px 50px rgba(0,0,0,.3);text-align:center}
|
| 86 |
-
.vs2-dlg svg{color:#b45309}
|
| 87 |
-
.vs2-back{display:none}
|
| 88 |
-
@media (max-width:640px){.vs2-root{display:block;overflow:auto}.vs2-side{width:auto;border-right:0}
|
| 89 |
-
.vs2-root.vs2-detail .vs2-side{display:none}.vs2-root:not(.vs2-detail) .vs2-main{display:none}
|
| 90 |
-
.vs2-back{display:inline-block;margin-bottom:10px}.vs2-main{padding:14px 16px}}
|
| 91 |
-
`;
|
| 92 |
-
const LOCK = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>';
|
| 93 |
-
const GEAR = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>';
|
| 94 |
-
|
| 95 |
-
const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
| 96 |
-
const api = (p, b) => window.VK_API(p, b === undefined ? undefined : Object.assign({session: window.VK_SID}, b));
|
| 97 |
-
const errMsg = e => (e && e.message === 'rate') ? T('rate') : T('err');
|
| 98 |
-
const kv = k => k ? `${T('verdict')}: <b>${esc(k.verdict)}</b> · ${T('rule')} ${esc(k.rule)}${k.audit_seq != null ? ' · ' + T('auditN') + esc(k.audit_seq) : ''}` : '';
|
| 99 |
-
|
| 100 |
-
window.VK_EXTRA_APPS = window.VK_EXTRA_APPS || [];
|
| 101 |
-
window.VK_EXTRA_APPS.push(h => ({
|
| 102 |
-
id: 'settings', name: () => T('title'), color: '#64748B', icon: GEAR, w: 980, h: 680,
|
| 103 |
-
render(body, win) {
|
| 104 |
-
if (!document.getElementById('vs2-style')) {
|
| 105 |
-
const st = document.createElement('style'); st.id = 'vs2-style'; st.textContent = css; document.head.appendChild(st);
|
| 106 |
-
}
|
| 107 |
-
const SECS = ['about', 'model', 'sec', 'net', 'pol', 'kill', 'audit', 'look', 'apps', 'upd'];
|
| 108 |
-
let cur = LS.get('vk_settings_sec', 'about'); if (!SECS.includes(cur)) cur = 'about';
|
| 109 |
-
let data = null;
|
| 110 |
-
body.innerHTML = '';
|
| 111 |
-
body.style.position = 'relative';
|
| 112 |
-
const root = document.createElement('div'); root.className = 'vs2-root';
|
| 113 |
-
const side = document.createElement('nav'); side.className = 'vs2-side'; side.setAttribute('role', 'tablist'); side.setAttribute('aria-label', T('title'));
|
| 114 |
-
const main = document.createElement('section'); main.className = 'vs2-main'; main.setAttribute('role', 'tabpanel');
|
| 115 |
-
root.append(side, main); body.appendChild(root);
|
| 116 |
-
const toast = m => { try { h.toast(m); } catch (e) {} };
|
| 117 |
-
|
| 118 |
-
SECS.forEach(id => {
|
| 119 |
-
const b = document.createElement('button'); b.className = 'vs2-nav'; b.dataset.id = id; b.setAttribute('role', 'tab');
|
| 120 |
-
b.textContent = T(id); b.onclick = () => go(id, true); side.appendChild(b);
|
| 121 |
-
});
|
| 122 |
-
side.addEventListener('keydown', e => {
|
| 123 |
-
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return;
|
| 124 |
-
e.preventDefault();
|
| 125 |
-
const i = SECS.indexOf(cur), n = (i + (e.key === 'ArrowDown' ? 1 : SECS.length - 1)) % SECS.length;
|
| 126 |
-
go(SECS[n], false); side.querySelector(`[data-id="${SECS[n]}"]`).focus();
|
| 127 |
-
});
|
| 128 |
-
|
| 129 |
-
function go(id, detail) {
|
| 130 |
-
cur = id; LS.set('vk_settings_sec', id);
|
| 131 |
-
side.querySelectorAll('.vs2-nav').forEach(b => { const on = b.dataset.id === id; b.classList.toggle('on', on); b.setAttribute('aria-selected', on); b.tabIndex = on ? 0 : -1; });
|
| 132 |
-
if (detail) root.classList.add('vs2-detail');
|
| 133 |
-
draw();
|
| 134 |
-
}
|
| 135 |
-
async function load() {
|
| 136 |
-
main.innerHTML = `<div class="vs2-empty">${T('loading')}</div>`;
|
| 137 |
-
try { data = await api('settings/get', {}); draw(); }
|
| 138 |
-
catch (e) {
|
| 139 |
-
data = null;
|
| 140 |
-
main.innerHTML = `<div class="vs2-empty">${esc(errMsg(e))}<br><br><button class="vs2-btn pri">${T('retry')}</button></div>`;
|
| 141 |
-
main.querySelector('button').onclick = load;
|
| 142 |
-
}
|
| 143 |
-
}
|
| 144 |
-
function head(title, sub) {
|
| 145 |
-
return `<button class="vs2-btn vs2-back">← ${esc(T('title'))}</button><h2>${esc(title)}</h2>${sub ? `<p class="vs2-sub">${esc(sub)}</p>` : ''}`;
|
| 146 |
-
}
|
| 147 |
-
function lockRow(action, label, sub) {
|
| 148 |
-
return `<div class="vs2-row vs2-lockrow" tabindex="0" role="button" data-lock="${esc(action)}"><div class="vs2-k">${esc(label)}${sub ? `<small>${esc(sub)}</small>` : ''}</div><span class="vs2-lk">${LOCK} Locked</span></div>`;
|
| 149 |
-
}
|
| 150 |
-
function wireLocks() {
|
| 151 |
-
main.querySelectorAll('[data-lock]').forEach(r => {
|
| 152 |
-
r.onclick = () => lockedModal(r.dataset.lock);
|
| 153 |
-
r.onkeydown = e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); lockedModal(r.dataset.lock); } };
|
| 154 |
-
});
|
| 155 |
-
const bk = main.querySelector('.vs2-back'); if (bk) bk.onclick = () => root.classList.remove('vs2-detail');
|
| 156 |
-
}
|
| 157 |
-
function lockedModal(action, pre) {
|
| 158 |
-
const m = document.createElement('div'); m.className = 'vs2-modal';
|
| 159 |
-
m.innerHTML = `<div class="vs2-dlg" role="dialog" aria-modal="true"><div>${LOCK.replace(/14/g, '36')}</div>
|
| 160 |
-
<h3 style="margin:8px 0 4px">${T('locked')}</h3><p style="margin:0 0 10px">${esc(action.replace(/_/g, ' '))}: ${T('needKey')}</p>
|
| 161 |
-
<div class="vs2-res vs2-mono">${pre ? kv(pre.kernel) : T('asking')}</div><br><button class="vs2-btn pri">${T('close')}</button></div>`;
|
| 162 |
-
body.appendChild(m);
|
| 163 |
-
const close = () => m.remove();
|
| 164 |
-
m.querySelector('button').onclick = close; m.onclick = e => { if (e.target === m) close(); };
|
| 165 |
-
m.addEventListener('keydown', e => { if (e.key === 'Escape') close(); });
|
| 166 |
-
m.querySelector('button').focus();
|
| 167 |
-
if (pre) return;
|
| 168 |
-
api('settings/locked', {action}).then(r => {
|
| 169 |
-
m.querySelector('.vs2-res').innerHTML = r && r.kernel ? kv(r.kernel) + (r.kernel.reason ? `<br>${esc(r.kernel.reason)}` : '') : esc(T('err'));
|
| 170 |
-
}).catch(e => { m.querySelector('.vs2-res').textContent = errMsg(e); });
|
| 171 |
-
}
|
| 172 |
-
|
| 173 |
-
const view = {
|
| 174 |
-
about() {
|
| 175 |
-
const k = data.kernel || {};
|
| 176 |
-
const ok = k.verified === true, halted = String(k.kernel_state || '').toUpperCase().includes('HALT');
|
| 177 |
-
return head(T('about'), 'ViKernel Desktop, a public web demo.') + `<div class="vs2-card">
|
| 178 |
-
<div class="vs2-row"><div class="vs2-k">Version</div><b>${esc(data.version)}</b></div>
|
| 179 |
-
<div class="vs2-row"><div class="vs2-k">Boot status</div><span class="vs2-pill ${halted ? 'vs2-bad' : ok ? 'vs2-ok' : 'vs2-unk'}">${esc(k.kernel_state || 'unknown')}${ok ? ' · verified' : ''}</span></div>
|
| 180 |
-
<div class="vs2-row"><div class="vs2-k">Audit chain head</div><span class="vs2-mono">${esc(String(k.audit_head || '').slice(0, 16))}${k.audit_head ? '...' : 'none'} (${esc(k.audit_n || 0)} entries)</span></div>
|
| 181 |
-
<div class="vs2-row"><div class="vs2-k">Model</div><span>${esc(data.model || 'unknown')}</span></div>
|
| 182 |
-
<div class="vs2-row"><div class="vs2-k">Gate kernel mode<small>Mode B: CPU reference implementation of the ViKernel gate</small></div><b>Mode B</b></div></div>
|
| 183 |
-
<p class="vs2-note">Mode A (kernel inside the weights) is available on the Glimmer option in Agent Guard.</p>`;
|
| 184 |
-
},
|
| 185 |
-
model() {
|
| 186 |
-
const pref = LS.get('vk_model_pref', 'spark');
|
| 187 |
-
const opt = (id, t, s, active) => `<button class="vs2-radio ${pref === id ? 'on' : ''}" data-pref="${id}" role="radio" aria-checked="${pref === id}"><b>${esc(t)}</b>${active ? ' <span class="vs2-pill vs2-ok">active</span>' : ''}<br><small class="vs2-note">${esc(s)}</small></button>`;
|
| 188 |
-
return head(T('model'), 'Which model drives the agent, and where the kernel runs.') +
|
| 189 |
-
opt('spark', 'Meta Muse Spark 1.3 (API) + ViKernel gate', 'Every tool call is decided by the ViKernel gate (Mode B).', true) +
|
| 190 |
-
opt('glimmer_b', 'Meta Muse Glimmer 30B + ViKernel in-weights, Mode B', 'Local open weights, gate on the CPU reference.') +
|
| 191 |
-
opt('glimmer_a', 'Meta Muse Glimmer 30B + ViKernel in-weights, Mode A', 'The kernel runs inside the model weights.') +
|
| 192 |
-
(pref !== 'spark' ? `<div class="vs2-card">Open Agent Guard to use it. <button class="vs2-btn pri" data-open="guard">Open Agent Guard</button></div>` : '');
|
| 193 |
-
},
|
| 194 |
-
sec() {
|
| 195 |
-
const a = data.app;
|
| 196 |
-
if (!a) return head(T('sec'), 'Per-app permissions.') + `<div class="vs2-card vs2-empty">No app is installed in this session.<br><br><button class="vs2-btn pri" data-open="appstore">Open the App Store</button></div>`;
|
| 197 |
-
const rows = a.declared.map(t => { const on = a.current.includes(t);
|
| 198 |
-
return `<div class="vs2-row"><div class="vs2-k"><span class="vs2-mono">${esc(t)}</span><small>${on ? 'Allowed' : 'Revoked: the kernel denies it (rule manifest)'}</small></div><button class="vs2-sw" role="switch" aria-label="${esc(t)}" aria-checked="${on}" data-tool="${esc(t)}"></button></div>`; }).join('');
|
| 199 |
-
return head(T('sec'), 'Like a phone OS: turn off any permission the app declared.') + `<div class="vs2-card"><b>${esc(a.name)}</b> <small class="vs2-note">${esc(a.id)}</small>
|
| 200 |
-
${rows || '<div class="vs2-empty">This app declared no tools.</div>'}
|
| 201 |
-
${a.paths.length ? `<div class="vs2-note">File paths: <span class="vs2-mono">${a.paths.map(esc).join(', ')}</span></div>` : ''}
|
| 202 |
-
<div class="vs2-res" id="vs2-permres"></div></div>
|
| 203 |
-
<p class="vs2-note">Try it: Ask the agent in Agent Guard to use this tool; it will be denied (rule manifest).</p>`;
|
| 204 |
-
},
|
| 205 |
-
net() {
|
| 206 |
-
const l = data.settings.egress_allow;
|
| 207 |
-
return head(T('net'), 'The agent may only reach these domains. Everything else is denied (rule egress).') + `<div class="vs2-card">
|
| 208 |
-
${l.length ? l.map(d => `<div class="vs2-row"><div class="vs2-k vs2-mono">${esc(d)}</div><button class="vs2-btn" data-rm="${esc(d)}">Remove</button></div>`).join('') : '<div class="vs2-empty">The allowlist is empty: all egress is denied.</div>'}
|
| 209 |
-
<div class="vs2-row"><input class="vs2-in" id="vs2-dom" placeholder="docs.python.org" aria-label="Domain" style="flex:1"><button class="vs2-btn pri" id="vs2-add">Add</button></div>
|
| 210 |
-
<div class="vs2-res" id="vs2-netres"></div></div>
|
| 211 |
-
<div class="vs2-card">${lockRow('allow_all_egress', 'Allow all egress', 'Wildcards, IP addresses, localhost and metadata hosts count as this.')}</div>`;
|
| 212 |
-
},
|
| 213 |
-
pol() {
|
| 214 |
-
const p = data.settings.policy;
|
| 215 |
-
const tog = (k, l) => `<div class="vs2-row"><div class="vs2-k">${esc(l)}</div><button class="vs2-sw" role="switch" aria-label="${esc(l)}" aria-checked="${!!p[k]}" data-pol="${k}"></button></div>`;
|
| 216 |
-
return head(T('pol'), 'The rules the kernel enforces on every action.') +
|
| 217 |
-
`<div class="vs2-card"><table class="vs2-table"><tr><th>Rule</th><th>Name</th><th>What it does</th></tr>${data.policies.map(r => `<tr><td><b>${esc(r.id)}</b></td><td>${esc(r.name)}</td><td>${esc(r.desc)}</td></tr>`).join('')}</table></div>
|
| 218 |
-
<div class="vs2-card"><b>Display and agent (not critical)</b>
|
| 219 |
-
${tog('verbose_audit_view', 'Verbose audit view')}${tog('show_kernel_ms', 'Show kernel time (ms)')}
|
| 220 |
-
<div class="vs2-row"><div class="vs2-k">Agent step limit<small>1 to 12</small></div><input class="vs2-in" type="number" min="1" max="12" id="vs2-steps" value="${esc(p.agent_step_limit)}" style="width:70px"></div>
|
| 221 |
-
<div class="vs2-res" id="vs2-polres"></div></div>
|
| 222 |
-
<div class="vs2-card"><b>Critical rules</b>${data.policies.filter(r => r.critical).map(r => lockRow('lower_policy', 'Turn off rule ' + r.id + ' (' + r.name + ')')).join('')}</div>`;
|
| 223 |
-
},
|
| 224 |
-
kill() {
|
| 225 |
-
const k = data.kernel || {}, halted = String(k.kernel_state || '').toUpperCase().includes('HALT');
|
| 226 |
-
return head(T('kill'), 'The kill switch sits above every app. When halted, every action is denied.') + `<div class="vs2-card">
|
| 227 |
-
<div class="vs2-row"><div class="vs2-k">Kernel state</div><span class="vs2-pill ${halted ? 'vs2-bad' : 'vs2-ok'}" id="vs2-kstate">${esc(k.kernel_state || 'unknown')}</span></div>
|
| 228 |
-
<div class="vs2-row"><div class="vs2-k">Test the kill switch<small>Halts the kernel for this demo. Resume needs the operator key.</small></div><button class="vs2-btn dng" id="vs2-kill">Test kill switch</button></div>
|
| 229 |
-
<div class="vs2-res" id="vs2-killres"></div></div>
|
| 230 |
-
<div class="vs2-card"><b>Operator key</b><p class="vs2-note">The demo key is published in the README for testing. A real deployment keeps it offline.</p>
|
| 231 |
-
<div class="vs2-row"><input class="vs2-in" type="password" id="vs2-key" autocomplete="off" placeholder="operator key" aria-label="Operator key" style="flex:1"><button class="vs2-btn pri" id="vs2-resume">Resume</button></div>
|
| 232 |
-
<div class="vs2-res" id="vs2-resres"></div></div>
|
| 233 |
-
<div class="vs2-card">${lockRow('disable_kill_switch', 'Disable kill switch')}</div>`;
|
| 234 |
-
},
|
| 235 |
-
audit() {
|
| 236 |
-
return head(T('audit'), 'Every kernel decision is sealed in a hash chain.') + `<div class="vs2-card">
|
| 237 |
-
<div class="vs2-row"><div class="vs2-k">Retention</div><span>kept for the session; exported on demand</span></div>
|
| 238 |
-
<div class="vs2-row"><div class="vs2-k">Verify the chain</div><button class="vs2-btn" id="vs2-verify">Verify</button></div>
|
| 239 |
-
<div class="vs2-row"><div class="vs2-k">Export</div><button class="vs2-btn" id="vs2-export">Export JSON</button></div>
|
| 240 |
-
<div class="vs2-res" id="vs2-audres"></div></div>
|
| 241 |
-
<div class="vs2-card">${lockRow('disable_audit', 'Turn off audit')}</div>`;
|
| 242 |
-
},
|
| 243 |
-
look() {
|
| 244 |
-
const t = LS.get('vk_theme', 'system'), a = LS.get('vk_accent', ACCENTS[0]), w = LS.get('vk_wall', '');
|
| 245 |
-
return head(T('look'), 'Saved in this browser only.') + `<div class="vs2-card">
|
| 246 |
-
<div class="vs2-row"><div class="vs2-k">Theme</div>${['light', 'dark', 'system'].map(x => `<button class="vs2-btn ${t === x ? 'pri' : ''}" data-theme-set="${x}">${x[0].toUpperCase() + x.slice(1)}</button>`).join('')}</div>
|
| 247 |
-
<div class="vs2-row"><div class="vs2-k">Accent color</div>${ACCENTS.map(c => `<button class="vs2-swatch ${a === c ? 'on' : ''}" style="background:${c}" data-accent="${c}" aria-label="Accent ${c}"></button>`).join('')}</div>
|
| 248 |
-
<div class="vs2-row"><div class="vs2-k">Wallpaper</div><button class="vs2-wall ${!w ? 'on' : ''}" style="background:var(--vs2-soft)" data-wall="" aria-label="Default"></button>${Object.keys(WALLS).map(k => `<button class="vs2-wall ${w === k ? 'on' : ''}" style="background:${WALLS[k]}" data-wall="${k}" aria-label="${k}"></button>`).join('')}</div>
|
| 249 |
-
<div class="vs2-row"><div class="vs2-k">
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
main.
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
const
|
| 271 |
-
|
| 272 |
-
if (cur === '
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
if (r && r.
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
q('#vs2-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
if (r && r.
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
const
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
const
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
const
|
| 338 |
-
const
|
| 339 |
-
const
|
| 340 |
-
document.
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
main.querySelectorAll('[data-
|
| 348 |
-
main.querySelectorAll('[data-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
}
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ViKernel Desktop: System Settings app. Non-critical settings change here;
|
| 2 |
+
critical ones are sent to the real ViKernel gate, which denies them. */
|
| 3 |
+
(() => {
|
| 4 |
+
'use strict';
|
| 5 |
+
const LS = {
|
| 6 |
+
get(k, d) { try { const v = localStorage.getItem(k); return v === null ? d : v; } catch (e) { return d; } },
|
| 7 |
+
set(k, v) { try { localStorage.setItem(k, v); } catch (e) {} },
|
| 8 |
+
};
|
| 9 |
+
const LANG = LS.get('vk_lang', 'en') === 'ko' ? 'ko' : 'en';
|
| 10 |
+
const STR = {
|
| 11 |
+
en: {
|
| 12 |
+
title: 'System Settings', about: 'About', model: 'Model & kernel', sec: 'Security & privacy', net: 'Network egress',
|
| 13 |
+
pol: 'Policies', kill: 'Kill switch', audit: 'Audit', look: 'Appearance', apps: 'Apps', upd: 'Updates',
|
| 14 |
+
loading: 'Loading...', err: 'Could not reach the backend. It may be starting; try again.', rate: 'Too many requests. Wait a moment.',
|
| 15 |
+
retry: 'Retry', locked: 'Locked by ViKernel', needKey: 'requires a signed operator key', close: 'Close',
|
| 16 |
+
asking: 'Asking the kernel...', verdict: 'Verdict', rule: 'rule', auditN: 'audit #',
|
| 17 |
+
},
|
| 18 |
+
ko: {
|
| 19 |
+
title: '시스템 설정', about: '정보', model: '모델과 커널', sec: '보안과 개인정보', net: '외부 접속',
|
| 20 |
+
pol: '정책', kill: '킬 스위치', audit: '감사 기록', look: '화면', apps: '앱', upd: '업데이트',
|
| 21 |
+
loading: '불러오는 중...', err: '백엔드에 닿지 못했습니다. 시작 중일 수 있습니다. 다시 시도하십시오.', rate: '요청이 너무 많습니다. 잠시 기다리십시오.',
|
| 22 |
+
retry: '다시 시도', locked: 'ViKernel 이 잠금', needKey: '서명된 운영자 키가 필요합니다', close: '닫기',
|
| 23 |
+
asking: '커널에 묻는 중...', verdict: '판정', rule: '규칙', auditN: '감사 #',
|
| 24 |
+
},
|
| 25 |
+
};
|
| 26 |
+
const T = k => (STR[LANG][k] || STR.en[k] || k);
|
| 27 |
+
|
| 28 |
+
/* Appearance: apply on load */
|
| 29 |
+
const ACCENTS = ['#4338CA', '#0EA5E9', '#10B981', '#F43F5E', '#F59E0B', '#7C3AED'];
|
| 30 |
+
const WALLS = {
|
| 31 |
+
aurora: 'linear-gradient(135deg,#c7d2fe 0%,#e0e7ff 40%,#cffafe 100%)',
|
| 32 |
+
dusk: 'linear-gradient(160deg,#312e81 0%,#7c3aed 55%,#f43f5e 100%)',
|
| 33 |
+
slate: 'linear-gradient(180deg,#0f172a 0%,#334155 100%)',
|
| 34 |
+
};
|
| 35 |
+
function applyTheme() {
|
| 36 |
+
const t = LS.get('vk_theme', 'system');
|
| 37 |
+
const dark = t === 'dark' || (t === 'system' && window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches);
|
| 38 |
+
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
|
| 39 |
+
}
|
| 40 |
+
function applyLook() {
|
| 41 |
+
applyTheme();
|
| 42 |
+
const a = LS.get('vk_accent', '');
|
| 43 |
+
if (a && ACCENTS.includes(a)) document.documentElement.style.setProperty('--indigo', a);
|
| 44 |
+
const w = LS.get('vk_wall', '');
|
| 45 |
+
const d = document.getElementById('desk');
|
| 46 |
+
if (d && WALLS[w]) d.style.background = WALLS[w];
|
| 47 |
+
}
|
| 48 |
+
applyLook();
|
| 49 |
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', applyLook);
|
| 50 |
+
try { matchMedia('(prefers-color-scheme: dark)').addEventListener('change', applyTheme); } catch (e) {}
|
| 51 |
+
|
| 52 |
+
const css = `
|
| 53 |
+
.vs2-root{display:flex;height:100%;min-height:0;font-size:13px;color:var(--vs2-fg);background:var(--vs2-bg);
|
| 54 |
+
--vs2-bg:#fff;--vs2-fg:#0f172a;--vs2-mut:#64748b;--vs2-line:#e2e8f0;--vs2-side:#f8fafc;--vs2-card:#fff;--vs2-soft:#f1f5f9}
|
| 55 |
+
[data-theme=dark] .vs2-root{--vs2-bg:#0f172a;--vs2-fg:#e2e8f0;--vs2-mut:#94a3b8;--vs2-line:#334155;--vs2-side:#111827;--vs2-card:#1e293b;--vs2-soft:#1e293b}
|
| 56 |
+
.vs2-side{width:210px;flex:none;background:var(--vs2-side);border-right:1px solid var(--vs2-line);padding:10px 8px;overflow:auto;outline:none}
|
| 57 |
+
.vs2-nav{display:flex;align-items:center;gap:8px;width:100%;padding:8px 10px;border:0;border-radius:8px;background:none;color:inherit;font:inherit;text-align:left;cursor:pointer}
|
| 58 |
+
.vs2-nav:hover{background:var(--vs2-soft)}.vs2-nav.on{background:var(--indigo);color:#fff}
|
| 59 |
+
.vs2-nav:focus-visible{outline:2px solid var(--indigo);outline-offset:1px}
|
| 60 |
+
.vs2-main{flex:1;min-width:0;overflow:auto;padding:18px 22px}
|
| 61 |
+
.vs2-main h2{margin:0 0 4px;font-size:20px}.vs2-sub{color:var(--vs2-mut);margin:0 0 14px}
|
| 62 |
+
.vs2-card{background:var(--vs2-card);border:1px solid var(--vs2-line);border-radius:12px;padding:12px 14px;margin-bottom:12px}
|
| 63 |
+
.vs2-row{display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid var(--vs2-line)}.vs2-row:last-child{border-bottom:0}
|
| 64 |
+
.vs2-row .vs2-k{flex:1;min-width:0}.vs2-row small{display:block;color:var(--vs2-mut)}
|
| 65 |
+
.vs2-mono{font-family:ui-monospace,Consolas,monospace;font-size:12px;word-break:break-all}
|
| 66 |
+
.vs2-btn{border:1px solid var(--vs2-line);background:var(--vs2-card);color:inherit;border-radius:8px;padding:6px 12px;font:inherit;cursor:pointer}
|
| 67 |
+
.vs2-btn:hover{border-color:var(--indigo)}.vs2-btn.pri{background:var(--indigo);border-color:var(--indigo);color:#fff}
|
| 68 |
+
.vs2-btn.dng{background:#e11d48;border-color:#e11d48;color:#fff}.vs2-btn:disabled{opacity:.5;cursor:default}
|
| 69 |
+
.vs2-sw{position:relative;width:40px;height:22px;flex:none;border-radius:11px;border:0;background:#cbd5e1;cursor:pointer;transition:background .15s}
|
| 70 |
+
.vs2-sw::after{content:"";position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:#fff;transition:left .15s}
|
| 71 |
+
.vs2-sw[aria-checked=true]{background:var(--indigo)}.vs2-sw[aria-checked=true]::after{left:21px}.vs2-sw:disabled{opacity:.5}
|
| 72 |
+
.vs2-lockrow{cursor:pointer}.vs2-lockrow:hover{background:var(--vs2-soft)}
|
| 73 |
+
.vs2-lk{display:inline-flex;align-items:center;gap:4px;color:#b45309;font-weight:600;font-size:12px}
|
| 74 |
+
.vs2-pill{display:inline-block;padding:2px 10px;border-radius:999px;font-weight:700;font-size:12px}
|
| 75 |
+
.vs2-ok{background:#d1fae5;color:#065f46}.vs2-bad{background:#ffe4e6;color:#9f1239}.vs2-unk{background:var(--vs2-soft);color:var(--vs2-mut)}
|
| 76 |
+
.vs2-radio{display:block;width:100%;text-align:left;border:2px solid var(--vs2-line);border-radius:12px;padding:12px;margin-bottom:10px;background:var(--vs2-card);color:inherit;font:inherit;cursor:pointer}
|
| 77 |
+
.vs2-radio.on{border-color:var(--indigo)}
|
| 78 |
+
.vs2-in{border:1px solid var(--vs2-line);border-radius:8px;padding:6px 10px;font:inherit;background:var(--vs2-bg);color:inherit;min-width:0}
|
| 79 |
+
.vs2-swatch{width:26px;height:26px;border-radius:50%;border:2px solid transparent;cursor:pointer}.vs2-swatch.on{border-color:var(--vs2-fg)}
|
| 80 |
+
.vs2-wall{width:70px;height:44px;border-radius:8px;border:2px solid transparent;cursor:pointer}.vs2-wall.on{border-color:var(--indigo)}
|
| 81 |
+
.vs2-note{color:var(--vs2-mut);font-size:12px;margin-top:6px}.vs2-res{margin-top:8px;font-size:12px}
|
| 82 |
+
.vs2-table{width:100%;border-collapse:collapse}.vs2-table td,.vs2-table th{text-align:left;padding:6px;border-bottom:1px solid var(--vs2-line);vertical-align:top}
|
| 83 |
+
.vs2-empty{text-align:center;padding:30px 10px;color:var(--vs2-mut)}
|
| 84 |
+
.vs2-modal{position:absolute;inset:0;background:rgba(15,23,42,.45);display:flex;align-items:center;justify-content:center;z-index:5}
|
| 85 |
+
.vs2-dlg{background:var(--vs2-card);color:var(--vs2-fg);border-radius:14px;padding:20px;width:min(420px,92%);box-shadow:0 20px 50px rgba(0,0,0,.3);text-align:center}
|
| 86 |
+
.vs2-dlg svg{color:#b45309}
|
| 87 |
+
.vs2-back{display:none}
|
| 88 |
+
@media (max-width:640px){.vs2-root{display:block;overflow:auto}.vs2-side{width:auto;border-right:0}
|
| 89 |
+
.vs2-root.vs2-detail .vs2-side{display:none}.vs2-root:not(.vs2-detail) .vs2-main{display:none}
|
| 90 |
+
.vs2-back{display:inline-block;margin-bottom:10px}.vs2-main{padding:14px 16px}}
|
| 91 |
+
`;
|
| 92 |
+
const LOCK = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>';
|
| 93 |
+
const GEAR = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>';
|
| 94 |
+
|
| 95 |
+
const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
| 96 |
+
const api = (p, b) => window.VK_API(p, b === undefined ? undefined : Object.assign({session: window.VK_SID}, b));
|
| 97 |
+
const errMsg = e => (e && e.message === 'rate') ? T('rate') : T('err');
|
| 98 |
+
const kv = k => k ? `${T('verdict')}: <b>${esc(k.verdict)}</b> · ${T('rule')} ${esc(k.rule)}${k.audit_seq != null ? ' · ' + T('auditN') + esc(k.audit_seq) : ''}` : '';
|
| 99 |
+
|
| 100 |
+
window.VK_EXTRA_APPS = window.VK_EXTRA_APPS || [];
|
| 101 |
+
window.VK_EXTRA_APPS.push(h => ({
|
| 102 |
+
id: 'settings', name: () => T('title'), color: '#64748B', icon: GEAR, w: 980, h: 680,
|
| 103 |
+
render(body, win) {
|
| 104 |
+
if (!document.getElementById('vs2-style')) {
|
| 105 |
+
const st = document.createElement('style'); st.id = 'vs2-style'; st.textContent = css; document.head.appendChild(st);
|
| 106 |
+
}
|
| 107 |
+
const SECS = ['about', 'model', 'sec', 'net', 'pol', 'kill', 'audit', 'look', 'apps', 'upd'];
|
| 108 |
+
let cur = LS.get('vk_settings_sec', 'about'); if (!SECS.includes(cur)) cur = 'about';
|
| 109 |
+
let data = null;
|
| 110 |
+
body.innerHTML = '';
|
| 111 |
+
body.style.position = 'relative';
|
| 112 |
+
const root = document.createElement('div'); root.className = 'vs2-root';
|
| 113 |
+
const side = document.createElement('nav'); side.className = 'vs2-side'; side.setAttribute('role', 'tablist'); side.setAttribute('aria-label', T('title'));
|
| 114 |
+
const main = document.createElement('section'); main.className = 'vs2-main'; main.setAttribute('role', 'tabpanel');
|
| 115 |
+
root.append(side, main); body.appendChild(root);
|
| 116 |
+
const toast = m => { try { h.toast(m); } catch (e) {} };
|
| 117 |
+
|
| 118 |
+
SECS.forEach(id => {
|
| 119 |
+
const b = document.createElement('button'); b.className = 'vs2-nav'; b.dataset.id = id; b.setAttribute('role', 'tab');
|
| 120 |
+
b.textContent = T(id); b.onclick = () => go(id, true); side.appendChild(b);
|
| 121 |
+
});
|
| 122 |
+
side.addEventListener('keydown', e => {
|
| 123 |
+
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return;
|
| 124 |
+
e.preventDefault();
|
| 125 |
+
const i = SECS.indexOf(cur), n = (i + (e.key === 'ArrowDown' ? 1 : SECS.length - 1)) % SECS.length;
|
| 126 |
+
go(SECS[n], false); side.querySelector(`[data-id="${SECS[n]}"]`).focus();
|
| 127 |
+
});
|
| 128 |
+
|
| 129 |
+
function go(id, detail) {
|
| 130 |
+
cur = id; LS.set('vk_settings_sec', id);
|
| 131 |
+
side.querySelectorAll('.vs2-nav').forEach(b => { const on = b.dataset.id === id; b.classList.toggle('on', on); b.setAttribute('aria-selected', on); b.tabIndex = on ? 0 : -1; });
|
| 132 |
+
if (detail) root.classList.add('vs2-detail');
|
| 133 |
+
draw();
|
| 134 |
+
}
|
| 135 |
+
async function load() {
|
| 136 |
+
main.innerHTML = `<div class="vs2-empty">${T('loading')}</div>`;
|
| 137 |
+
try { data = await api('settings/get', {}); draw(); }
|
| 138 |
+
catch (e) {
|
| 139 |
+
data = null;
|
| 140 |
+
main.innerHTML = `<div class="vs2-empty">${esc(errMsg(e))}<br><br><button class="vs2-btn pri">${T('retry')}</button></div>`;
|
| 141 |
+
main.querySelector('button').onclick = load;
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
function head(title, sub) {
|
| 145 |
+
return `<button class="vs2-btn vs2-back">← ${esc(T('title'))}</button><h2>${esc(title)}</h2>${sub ? `<p class="vs2-sub">${esc(sub)}</p>` : ''}`;
|
| 146 |
+
}
|
| 147 |
+
function lockRow(action, label, sub) {
|
| 148 |
+
return `<div class="vs2-row vs2-lockrow" tabindex="0" role="button" data-lock="${esc(action)}"><div class="vs2-k">${esc(label)}${sub ? `<small>${esc(sub)}</small>` : ''}</div><span class="vs2-lk">${LOCK} Locked</span></div>`;
|
| 149 |
+
}
|
| 150 |
+
function wireLocks() {
|
| 151 |
+
main.querySelectorAll('[data-lock]').forEach(r => {
|
| 152 |
+
r.onclick = () => lockedModal(r.dataset.lock);
|
| 153 |
+
r.onkeydown = e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); lockedModal(r.dataset.lock); } };
|
| 154 |
+
});
|
| 155 |
+
const bk = main.querySelector('.vs2-back'); if (bk) bk.onclick = () => root.classList.remove('vs2-detail');
|
| 156 |
+
}
|
| 157 |
+
function lockedModal(action, pre) {
|
| 158 |
+
const m = document.createElement('div'); m.className = 'vs2-modal';
|
| 159 |
+
m.innerHTML = `<div class="vs2-dlg" role="dialog" aria-modal="true"><div>${LOCK.replace(/14/g, '36')}</div>
|
| 160 |
+
<h3 style="margin:8px 0 4px">${T('locked')}</h3><p style="margin:0 0 10px">${esc(action.replace(/_/g, ' '))}: ${T('needKey')}</p>
|
| 161 |
+
<div class="vs2-res vs2-mono">${pre ? kv(pre.kernel) : T('asking')}</div><br><button class="vs2-btn pri">${T('close')}</button></div>`;
|
| 162 |
+
body.appendChild(m);
|
| 163 |
+
const close = () => m.remove();
|
| 164 |
+
m.querySelector('button').onclick = close; m.onclick = e => { if (e.target === m) close(); };
|
| 165 |
+
m.addEventListener('keydown', e => { if (e.key === 'Escape') close(); });
|
| 166 |
+
m.querySelector('button').focus();
|
| 167 |
+
if (pre) return;
|
| 168 |
+
api('settings/locked', {action}).then(r => {
|
| 169 |
+
m.querySelector('.vs2-res').innerHTML = r && r.kernel ? kv(r.kernel) + (r.kernel.reason ? `<br>${esc(r.kernel.reason)}` : '') : esc(T('err'));
|
| 170 |
+
}).catch(e => { m.querySelector('.vs2-res').textContent = errMsg(e); });
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
const view = {
|
| 174 |
+
about() {
|
| 175 |
+
const k = data.kernel || {};
|
| 176 |
+
const ok = k.verified === true, halted = String(k.kernel_state || '').toUpperCase().includes('HALT');
|
| 177 |
+
return head(T('about'), 'ViKernel Desktop, a public web demo.') + `<div class="vs2-card">
|
| 178 |
+
<div class="vs2-row"><div class="vs2-k">Version</div><b>${esc(data.version)}</b></div>
|
| 179 |
+
<div class="vs2-row"><div class="vs2-k">Boot status</div><span class="vs2-pill ${halted ? 'vs2-bad' : ok ? 'vs2-ok' : 'vs2-unk'}">${esc(k.kernel_state || 'unknown')}${ok ? ' · verified' : ''}</span></div>
|
| 180 |
+
<div class="vs2-row"><div class="vs2-k">Audit chain head</div><span class="vs2-mono">${esc(String(k.audit_head || '').slice(0, 16))}${k.audit_head ? '...' : 'none'} (${esc(k.audit_n || 0)} entries)</span></div>
|
| 181 |
+
<div class="vs2-row"><div class="vs2-k">Model</div><span>${esc(data.model || 'unknown')}</span></div>
|
| 182 |
+
<div class="vs2-row"><div class="vs2-k">Gate kernel mode<small>Mode B: CPU reference implementation of the ViKernel gate</small></div><b>Mode B</b></div></div>
|
| 183 |
+
<p class="vs2-note">Mode A (kernel inside the weights) is available on the Glimmer option in Agent Guard.</p>`;
|
| 184 |
+
},
|
| 185 |
+
model() {
|
| 186 |
+
const pref = LS.get('vk_model_pref', 'spark');
|
| 187 |
+
const opt = (id, t, s, active) => `<button class="vs2-radio ${pref === id ? 'on' : ''}" data-pref="${id}" role="radio" aria-checked="${pref === id}"><b>${esc(t)}</b>${active ? ' <span class="vs2-pill vs2-ok">active</span>' : ''}<br><small class="vs2-note">${esc(s)}</small></button>`;
|
| 188 |
+
return head(T('model'), 'Which model drives the agent, and where the kernel runs.') +
|
| 189 |
+
opt('spark', 'Meta Muse Spark 1.3 (API) + ViKernel gate', 'Every tool call is decided by the ViKernel gate (Mode B).', true) +
|
| 190 |
+
opt('glimmer_b', 'Meta Muse Glimmer 30B + ViKernel in-weights, Mode B', 'Local open weights, gate on the CPU reference.') +
|
| 191 |
+
opt('glimmer_a', 'Meta Muse Glimmer 30B + ViKernel in-weights, Mode A', 'The kernel runs inside the model weights.') +
|
| 192 |
+
(pref !== 'spark' ? `<div class="vs2-card">Open Agent Guard to use it. <button class="vs2-btn pri" data-open="guard">Open Agent Guard</button></div>` : '');
|
| 193 |
+
},
|
| 194 |
+
sec() {
|
| 195 |
+
const a = data.app;
|
| 196 |
+
if (!a) return head(T('sec'), 'Per-app permissions.') + `<div class="vs2-card vs2-empty">No app is installed in this session.<br><br><button class="vs2-btn pri" data-open="appstore">Open the App Store</button></div>`;
|
| 197 |
+
const rows = a.declared.map(t => { const on = a.current.includes(t);
|
| 198 |
+
return `<div class="vs2-row"><div class="vs2-k"><span class="vs2-mono">${esc(t)}</span><small>${on ? 'Allowed' : 'Revoked: the kernel denies it (rule manifest)'}</small></div><button class="vs2-sw" role="switch" aria-label="${esc(t)}" aria-checked="${on}" data-tool="${esc(t)}"></button></div>`; }).join('');
|
| 199 |
+
return head(T('sec'), 'Like a phone OS: turn off any permission the app declared.') + `<div class="vs2-card"><b>${esc(a.name)}</b> <small class="vs2-note">${esc(a.id)}</small>
|
| 200 |
+
${rows || '<div class="vs2-empty">This app declared no tools.</div>'}
|
| 201 |
+
${a.paths.length ? `<div class="vs2-note">File paths: <span class="vs2-mono">${a.paths.map(esc).join(', ')}</span></div>` : ''}
|
| 202 |
+
<div class="vs2-res" id="vs2-permres"></div></div>
|
| 203 |
+
<p class="vs2-note">Try it: Ask the agent in Agent Guard to use this tool; it will be denied (rule manifest).</p>`;
|
| 204 |
+
},
|
| 205 |
+
net() {
|
| 206 |
+
const l = data.settings.egress_allow;
|
| 207 |
+
return head(T('net'), 'The agent may only reach these domains. Everything else is denied (rule egress).') + `<div class="vs2-card">
|
| 208 |
+
${l.length ? l.map(d => `<div class="vs2-row"><div class="vs2-k vs2-mono">${esc(d)}</div><button class="vs2-btn" data-rm="${esc(d)}">Remove</button></div>`).join('') : '<div class="vs2-empty">The allowlist is empty: all egress is denied.</div>'}
|
| 209 |
+
<div class="vs2-row"><input class="vs2-in" id="vs2-dom" placeholder="docs.python.org" aria-label="Domain" style="flex:1"><button class="vs2-btn pri" id="vs2-add">Add</button></div>
|
| 210 |
+
<div class="vs2-res" id="vs2-netres"></div></div>
|
| 211 |
+
<div class="vs2-card">${lockRow('allow_all_egress', 'Allow all egress', 'Wildcards, IP addresses, localhost and metadata hosts count as this.')}</div>`;
|
| 212 |
+
},
|
| 213 |
+
pol() {
|
| 214 |
+
const p = data.settings.policy;
|
| 215 |
+
const tog = (k, l) => `<div class="vs2-row"><div class="vs2-k">${esc(l)}</div><button class="vs2-sw" role="switch" aria-label="${esc(l)}" aria-checked="${!!p[k]}" data-pol="${k}"></button></div>`;
|
| 216 |
+
return head(T('pol'), 'The rules the kernel enforces on every action.') +
|
| 217 |
+
`<div class="vs2-card"><table class="vs2-table"><tr><th>Rule</th><th>Name</th><th>What it does</th></tr>${data.policies.map(r => `<tr><td><b>${esc(r.id)}</b></td><td>${esc(r.name)}</td><td>${esc(r.desc)}</td></tr>`).join('')}</table></div>
|
| 218 |
+
<div class="vs2-card"><b>Display and agent (not critical)</b>
|
| 219 |
+
${tog('verbose_audit_view', 'Verbose audit view')}${tog('show_kernel_ms', 'Show kernel time (ms)')}
|
| 220 |
+
<div class="vs2-row"><div class="vs2-k">Agent step limit<small>1 to 12</small></div><input class="vs2-in" type="number" min="1" max="12" id="vs2-steps" value="${esc(p.agent_step_limit)}" style="width:70px"></div>
|
| 221 |
+
<div class="vs2-res" id="vs2-polres"></div></div>
|
| 222 |
+
<div class="vs2-card"><b>Critical rules</b>${data.policies.filter(r => r.critical).map(r => lockRow('lower_policy', 'Turn off rule ' + r.id + ' (' + r.name + ')')).join('')}</div>`;
|
| 223 |
+
},
|
| 224 |
+
kill() {
|
| 225 |
+
const k = data.kernel || {}, halted = String(k.kernel_state || '').toUpperCase().includes('HALT');
|
| 226 |
+
return head(T('kill'), 'The kill switch sits above every app. When halted, every action is denied.') + `<div class="vs2-card">
|
| 227 |
+
<div class="vs2-row"><div class="vs2-k">Kernel state</div><span class="vs2-pill ${halted ? 'vs2-bad' : 'vs2-ok'}" id="vs2-kstate">${esc(k.kernel_state || 'unknown')}</span></div>
|
| 228 |
+
<div class="vs2-row"><div class="vs2-k">Test the kill switch<small>Halts the kernel for this demo. Resume needs the operator key.</small></div><button class="vs2-btn dng" id="vs2-kill">Test kill switch</button></div>
|
| 229 |
+
<div class="vs2-res" id="vs2-killres"></div></div>
|
| 230 |
+
<div class="vs2-card"><b>Operator key</b><p class="vs2-note">The demo key is published in the README for testing. A real deployment keeps it offline.</p>
|
| 231 |
+
<div class="vs2-row"><input class="vs2-in" type="password" id="vs2-key" autocomplete="off" placeholder="operator key" aria-label="Operator key" style="flex:1"><button class="vs2-btn pri" id="vs2-resume">Resume</button></div>
|
| 232 |
+
<div class="vs2-res" id="vs2-resres"></div></div>
|
| 233 |
+
<div class="vs2-card">${lockRow('disable_kill_switch', 'Disable kill switch')}</div>`;
|
| 234 |
+
},
|
| 235 |
+
audit() {
|
| 236 |
+
return head(T('audit'), 'Every kernel decision is sealed in a hash chain.') + `<div class="vs2-card">
|
| 237 |
+
<div class="vs2-row"><div class="vs2-k">Retention</div><span>kept for the session; exported on demand</span></div>
|
| 238 |
+
<div class="vs2-row"><div class="vs2-k">Verify the chain</div><button class="vs2-btn" id="vs2-verify">Verify</button></div>
|
| 239 |
+
<div class="vs2-row"><div class="vs2-k">Export</div><button class="vs2-btn" id="vs2-export">Export JSON</button></div>
|
| 240 |
+
<div class="vs2-res" id="vs2-audres"></div></div>
|
| 241 |
+
<div class="vs2-card">${lockRow('disable_audit', 'Turn off audit')}</div>`;
|
| 242 |
+
},
|
| 243 |
+
look() {
|
| 244 |
+
const t = LS.get('vk_theme', 'system'), a = LS.get('vk_accent', ACCENTS[0]), w = LS.get('vk_wall', '');
|
| 245 |
+
return head(T('look'), 'Saved in this browser only.') + `<div class="vs2-card">
|
| 246 |
+
<div class="vs2-row"><div class="vs2-k">Theme</div>${['light', 'dark', 'system'].map(x => `<button class="vs2-btn ${t === x ? 'pri' : ''}" data-theme-set="${x}">${x[0].toUpperCase() + x.slice(1)}</button>`).join('')}</div>
|
| 247 |
+
<div class="vs2-row"><div class="vs2-k">Accent color</div>${ACCENTS.map(c => `<button class="vs2-swatch ${a === c ? 'on' : ''}" style="background:${c}" data-accent="${c}" aria-label="Accent ${c}"></button>`).join('')}</div>
|
| 248 |
+
<div class="vs2-row"><div class="vs2-k">Wallpaper</div><button class="vs2-wall ${!w ? 'on' : ''}" style="background:var(--vs2-soft)" data-wall="" aria-label="Default"></button>${Object.keys(WALLS).map(k => `<button class="vs2-wall ${w === k ? 'on' : ''}" style="background:${WALLS[k]}" data-wall="${k}" aria-label="${k}"></button>`).join('')}</div>
|
| 249 |
+
<div class="vs2-row"><div class="vs2-k">Display scale</div>${window.VK_DISPLAY ? window.VK_DISPLAY.modes.map(m => `<button class="vs2-btn ${window.VK_DISPLAY.get() === m ? 'pri' : ''}" data-scale="${m}">${m === 'auto' ? 'Auto' : Math.round(parseFloat(m) * 100) + '%'}</button>`).join('') : ''}</div>
|
| 250 |
+
<div class="vs2-row"><div class="vs2-k">Language / 언어</div><button class="vs2-btn ${LANG === 'en' ? 'pri' : ''}" data-lang="en">EN</button><button class="vs2-btn ${LANG === 'ko' ? 'pri' : ''}" data-lang="ko">KO</button></div></div>`;
|
| 251 |
+
},
|
| 252 |
+
apps() {
|
| 253 |
+
const a = data.app;
|
| 254 |
+
return head(T('apps'), 'Agent apps installed in this session.') + `<div class="vs2-card">${a ? `<div class="vs2-row"><div class="vs2-k"><b>${esc(a.name)}</b><small>${esc(a.id)} · ${a.current.length}/${a.declared.length} tools allowed</small></div><button class="vs2-btn dng" id="vs2-remove">Remove</button></div>` : '<div class="vs2-empty">No app installed.</div>'}
|
| 255 |
+
<div class="vs2-res" id="vs2-appres"></div></div><button class="vs2-btn pri" data-open="appstore">Open the App Store</button>`;
|
| 256 |
+
},
|
| 257 |
+
upd() {
|
| 258 |
+
const log = [['v0.4', 'App Store, Browser, Settings, Terminal, Minesweeper, product pass'], ['v0.3', 'App Store preview, VDR packages'],
|
| 259 |
+
['v0.2', 'Live Meta Muse agent with Agent Guard'], ['v0.1', 'Desktop shell']];
|
| 260 |
+
return head(T('upd'), 'You are on ' + (data.version || 'v0.4') + '.') + `<div class="vs2-card">${log.map(([v, s]) => `<div class="vs2-row"><div class="vs2-k"><b>${v}</b><small>${esc(s)}</small></div></div>`).join('')}</div>`;
|
| 261 |
+
},
|
| 262 |
+
};
|
| 263 |
+
|
| 264 |
+
function draw() {
|
| 265 |
+
if (!data) return;
|
| 266 |
+
main.innerHTML = view[cur]();
|
| 267 |
+
main.scrollTop = 0;
|
| 268 |
+
wireLocks();
|
| 269 |
+
main.querySelectorAll('[data-open]').forEach(b => b.onclick = () => { try { h.openApp(b.dataset.open); } catch (e) {} });
|
| 270 |
+
const q = s => main.querySelector(s);
|
| 271 |
+
const res = (id, html) => { const e = q(id); if (e) e.innerHTML = html; };
|
| 272 |
+
if (cur === 'model') main.querySelectorAll('[data-pref]').forEach(b => b.onclick = () => { LS.set('vk_model_pref', b.dataset.pref); draw(); });
|
| 273 |
+
if (cur === 'sec') main.querySelectorAll('[data-tool]').forEach(b => b.onclick = async () => {
|
| 274 |
+
const on = b.getAttribute('aria-checked') === 'true'; b.disabled = true;
|
| 275 |
+
try {
|
| 276 |
+
const r = await api(on ? 'settings/revoke' : 'settings/grant', {tool: b.dataset.tool});
|
| 277 |
+
if (r && r.app) { data.app = r.app; draw(); res('#vs2-permres', `${on ? 'Revoked' : 'Granted'}: <span class="vs2-mono">${esc(b.dataset.tool)}</span>`); }
|
| 278 |
+
else res('#vs2-permres', esc((r && r.error) || T('err')));
|
| 279 |
+
try { h.refreshTray && h.refreshTray(); } catch (e) {}
|
| 280 |
+
} catch (e) { b.disabled = false; res('#vs2-permres', esc(errMsg(e))); }
|
| 281 |
+
});
|
| 282 |
+
if (cur === 'net') {
|
| 283 |
+
const edit = async body => {
|
| 284 |
+
try {
|
| 285 |
+
const r = await api('settings/egress', body);
|
| 286 |
+
if (r && r.locked) { lockedModal(r.action, r); return; }
|
| 287 |
+
if (r && r.egress_allow) { data.settings.egress_allow = r.egress_allow; draw(); }
|
| 288 |
+
else res('#vs2-netres', esc((r && r.error) || T('err')));
|
| 289 |
+
} catch (e) { res('#vs2-netres', esc(e.message.startsWith('http 400') ? 'Invalid domain. Use a name like docs.python.org.' : errMsg(e))); }
|
| 290 |
+
};
|
| 291 |
+
q('#vs2-add').onclick = () => { const v = q('#vs2-dom').value.trim(); if (v) edit({add: v}); };
|
| 292 |
+
q('#vs2-dom').onkeydown = e => { if (e.key === 'Enter') q('#vs2-add').click(); };
|
| 293 |
+
main.querySelectorAll('[data-rm]').forEach(b => b.onclick = () => edit({remove: b.dataset.rm}));
|
| 294 |
+
}
|
| 295 |
+
if (cur === 'pol') {
|
| 296 |
+
const set = async (key, value) => {
|
| 297 |
+
try {
|
| 298 |
+
const r = await api('settings/policy', {key, value});
|
| 299 |
+
if (r && r.locked) { lockedModal(r.action, r); return; }
|
| 300 |
+
if (r && r.policy) { data.settings.policy = r.policy; draw(); res('#vs2-polres', 'Saved.'); }
|
| 301 |
+
} catch (e) { res('#vs2-polres', esc(e.message.startsWith('http 400') ? 'Value out of range.' : errMsg(e))); }
|
| 302 |
+
};
|
| 303 |
+
main.querySelectorAll('[data-pol]').forEach(b => b.onclick = () => set(b.dataset.pol, b.getAttribute('aria-checked') !== 'true'));
|
| 304 |
+
q('#vs2-steps').onchange = e => set('agent_step_limit', parseInt(e.target.value, 10));
|
| 305 |
+
}
|
| 306 |
+
if (cur === 'kill') {
|
| 307 |
+
q('#vs2-kill').onclick = async () => {
|
| 308 |
+
try {
|
| 309 |
+
const r = await api('kill', {});
|
| 310 |
+
const s = await window.VK_API('status').catch(() => null);
|
| 311 |
+
if (s) data.kernel = Object.assign(data.kernel || {}, s);
|
| 312 |
+
draw(); res('#vs2-killres', `<span class="vs2-pill vs2-bad">HALTED</span> ${esc((r && r.message) || '')}`);
|
| 313 |
+
try { h.refreshTray && h.refreshTray(); } catch (e) {}
|
| 314 |
+
} catch (e) { res('#vs2-killres', esc(errMsg(e))); }
|
| 315 |
+
};
|
| 316 |
+
q('#vs2-resume').onclick = async () => {
|
| 317 |
+
const inp = q('#vs2-key'), key = inp.value; inp.value = '';
|
| 318 |
+
if (!key) { res('#vs2-resres', 'Enter the operator key.'); return; }
|
| 319 |
+
try {
|
| 320 |
+
const r = await api('resume', {key});
|
| 321 |
+
const s = await window.VK_API('status').catch(() => null);
|
| 322 |
+
if (s) data.kernel = Object.assign(data.kernel || {}, s);
|
| 323 |
+
draw(); res('#vs2-resres', `<span class="vs2-pill ${r && r.ok ? 'vs2-ok' : 'vs2-bad'}">${r && r.ok ? 'RESUMED' : 'REFUSED'}</span> ${esc((r && r.message) || '')}`);
|
| 324 |
+
try { h.refreshTray && h.refreshTray(); } catch (e) {}
|
| 325 |
+
} catch (e) { res('#vs2-resres', esc(errMsg(e))); }
|
| 326 |
+
};
|
| 327 |
+
}
|
| 328 |
+
if (cur === 'audit') {
|
| 329 |
+
q('#vs2-verify').onclick = async () => {
|
| 330 |
+
res('#vs2-audres', T('loading'));
|
| 331 |
+
try { const r = await window.VK_API('audit'); const v = (r && r.verify) || {};
|
| 332 |
+
res('#vs2-audres', `<span class="vs2-pill ${v.ok ? 'vs2-ok' : 'vs2-bad'}">${v.ok ? 'CHAIN OK' : 'CHAIN BROKEN'}</span> ${esc(v.n || 0)} entries${v.errors && v.errors.length ? ' · ' + esc(v.errors.length) + ' errors' : ''}`);
|
| 333 |
+
} catch (e) { res('#vs2-audres', esc(errMsg(e))); }
|
| 334 |
+
};
|
| 335 |
+
q('#vs2-export').onclick = async () => {
|
| 336 |
+
try {
|
| 337 |
+
const r = await window.VK_API('audit');
|
| 338 |
+
const rows = (r.lines || []).map(l => { try { return JSON.parse(l); } catch (e) { return l; } });
|
| 339 |
+
const blob = new Blob([JSON.stringify({exported: new Date().toISOString(), verify: r.verify, entries: rows}, null, 2)], {type: 'application/json'});
|
| 340 |
+
const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'vikernel-audit.json';
|
| 341 |
+
document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 2000);
|
| 342 |
+
res('#vs2-audres', `Exported ${rows.length} entries.`);
|
| 343 |
+
} catch (e) { res('#vs2-audres', esc(errMsg(e))); }
|
| 344 |
+
};
|
| 345 |
+
}
|
| 346 |
+
if (cur === 'look') {
|
| 347 |
+
main.querySelectorAll('[data-theme-set]').forEach(b => b.onclick = () => { LS.set('vk_theme', b.dataset.themeSet); applyTheme(); draw(); });
|
| 348 |
+
main.querySelectorAll('[data-accent]').forEach(b => b.onclick = () => { LS.set('vk_accent', b.dataset.accent); document.documentElement.style.setProperty('--indigo', b.dataset.accent); draw(); });
|
| 349 |
+
main.querySelectorAll('[data-scale]').forEach(b => b.onclick = () => { window.VK_DISPLAY.set(b.dataset.scale); location.reload(); });
|
| 350 |
+
main.querySelectorAll('[data-wall]').forEach(b => b.onclick = () => {
|
| 351 |
+
LS.set('vk_wall', b.dataset.wall); const d = document.getElementById('desk');
|
| 352 |
+
if (d) d.style.background = WALLS[b.dataset.wall] || ''; draw();
|
| 353 |
+
});
|
| 354 |
+
main.querySelectorAll('[data-lang]').forEach(b => b.onclick = () => { if (b.dataset.lang === LANG) return; LS.set('vk_lang', b.dataset.lang); location.reload(); });
|
| 355 |
+
}
|
| 356 |
+
if (cur === 'apps' && q('#vs2-remove')) q('#vs2-remove').onclick = async () => {
|
| 357 |
+
try { const cur = (data && (data.app || data.installed)) || null; await api('store/install', {id: ''}); try { const bin = JSON.parse(localStorage.getItem('vk_trash') || '[]'); if (cur && cur.id) { bin.push({id: cur.id, name: cur.name || cur.id, removed_at: new Date().toISOString()}); localStorage.setItem('vk_trash', JSON.stringify(bin)); } } catch (e2) {} toast('App removed (moved to the Recycle Bin)'); await load(); }
|
| 358 |
+
catch (e) { res('#vs2-appres', esc(errMsg(e))); }
|
| 359 |
+
};
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
go(cur, false);
|
| 363 |
+
load();
|
| 364 |
+
},
|
| 365 |
+
}));
|
| 366 |
+
})();
|