function segLine(a) { if (!a || !a.enabled || a.segment == null) return ''; const link = a.prev_head ? 'linked to previous head ' + String(a.prev_head).slice(0, 16) : 'first segment'; return '\nsegment ' + a.segment + ' (' + link + ')' + '\narchived ' + a.archived_records + ' records, ' + a.segments_verified + ' earlier segment' + (a.segments_verified === 1 ? '' : 's') + ' re-verified' + (a.segments_failed && a.segments_failed.length ? ', FAILED: ' + a.segments_failed.join(',') : ''); } /* ViKernel Desktop: Terminal app. * Browser-only shell over an in-memory virtual filesystem. Nothing here runs on a real server. * Live calls go only to api/ag/* through h.api. Security-sensitive commands are sent to the real gate. */ (function () { 'use strict'; const CDN = 'https://cdn.jsdelivr.net/npm/'; const XTERM_JS = CDN + '@xterm/xterm@5.5.0/lib/xterm.js'; const XTERM_CSS = CDN + '@xterm/xterm@5.5.0/css/xterm.css'; const FIT_JS = CDN + '@xterm/addon-fit@0.10.0/lib/addon-fit.js'; const VFS_KEY = 'vk_term_vfs_v1'; const HIST_KEY = 'vk_term_hist_v1'; const HOME = '/home/guest'; // ANSI helpers const C = { r: s => '\x1b[31m' + s + '\x1b[0m', g: s => '\x1b[32m' + s + '\x1b[0m', y: s => '\x1b[33m' + s + '\x1b[0m', b: s => '\x1b[34m' + s + '\x1b[0m', c: s => '\x1b[36m' + s + '\x1b[0m', m: s => '\x1b[35m' + s + '\x1b[0m', dim: s => '\x1b[2m' + s + '\x1b[0m', bold: s => '\x1b[1m' + s + '\x1b[0m', }; const stripAnsi = s => String(s).replace(/\x1b\[[0-9;?]*[A-Za-z]/g, ''); // ---------- CDN loader (inject once, await) ---------- function loadTag(kind, url) { return new Promise((res, rej) => { const sel = kind === 'script' ? 'script[src="' + url + '"]' : 'link[href="' + url + '"]'; const prev = document.querySelector(sel); if (prev) { if (prev.dataset.loaded === '1') return res(); prev.addEventListener('load', () => res()); prev.addEventListener('error', () => rej(new Error('cdn'))); return; } const el = document.createElement(kind); if (kind === 'script') { el.src = url; el.async = false; } else { el.rel = 'stylesheet'; el.href = url; } el.addEventListener('load', () => { el.dataset.loaded = '1'; res(); }); el.addEventListener('error', () => rej(new Error('cdn'))); document.head.appendChild(el); }); } function loadXterm() { if (!window.__vkXtermP) { window.__vkXtermP = Promise.all([loadTag('link', XTERM_CSS), loadTag('script', XTERM_JS)]) .then(() => loadTag('script', FIT_JS)) .then(() => { if (!window.Terminal || !window.FitAddon) throw new Error('cdn'); }); window.__vkXtermP.catch(() => { window.__vkXtermP = null; }); } return window.__vkXtermP; } // ---------- Virtual filesystem ---------- const dir = (c) => ({ t: 'd', c: c || {} }); const file = (d) => ({ t: 'f', d: d }); function defaultFS() { return dir({ home: dir({ guest: dir({ 'readme.txt': file('Welcome to ViKernel Desktop.\nThis terminal runs entirely in your browser on a virtual filesystem.\nTry: help, vik status, vik audit tail, ask "list my files", rm -rf /\n'), 'notes.md': file('# Notes\n- Every agent action passes the gate first.\n- The audit log is a hash chain; verify it with `vik audit verify`.\n- The kill switch halts everything: `vik kill`.\n'), project: dir({ 'main.py': file('def main():\n print("hello from a sandboxed project")\n\nif __name__ == "__main__":\n main()\n'), 'config.json': file('{\n "name": "demo-project",\n "version": "0.1.0",\n "policy": "allowlist"\n}\n'), }), }) }), etc: dir({ hostname: file('vikernel\n'), 'os-release': file('NAME="ViKernel Secure OS"\nVERSION="1.0 (demo)"\nID=vikernel\nPRETTY_NAME="ViKernel Secure OS 1.0 (browser demo)"\n'), passwd: file('root:x:0:0:root:/root:/bin/vsh\ngate:x:100:100:policy gate:/var/gate:/sbin/nologin\nagent:x:200:200:agent runtime:/var/agent:/sbin/nologin\nguest:x:1000:1000:Guest:/home/guest:/bin/vsh\n'), shadow: file('(protected)'), }), var: dir({ log: dir({ 'vikernel.log': file('[ 0.000] vikernel: boot, mode B (CPU reference)\n[ 0.012] integrity: measured boot OK\n[ 0.031] gate: policy loaded (allowlist + rules)\n[ 0.044] audit: chain opened\n[ 0.090] agent: runtime attached, model muse-spark-1.3\n'), }) }), apps: dir({}), tmp: dir({}), }); } window.VK_TERM = (body, h) => { h = h || {}; const api = h.api; let root; try { root = JSON.parse(sessionStorage.getItem(VFS_KEY) || 'null'); } catch (e) { root = null; } if (!root || root.t !== 'd') root = defaultFS(); let cwd = HOME; let history = []; try { history = JSON.parse(sessionStorage.getItem(HIST_KEY) || '[]') || []; } catch (e) { history = []; } let halted = false; const poll = setInterval(() => { if (!body.isConnected) { clearInterval(poll); return; } refreshState(); }, 12000); window.addEventListener('vk-kernel-state', e => { halted = e.detail === 'HALTED'; }); let online = true; const save = () => { try { sessionStorage.setItem(VFS_KEY, JSON.stringify(root)); } catch (e) { /* storage unavailable */ } try { sessionStorage.setItem(HIST_KEY, JSON.stringify(history.slice(-200))); } catch (e) { /* ignore */ } }; // Sync /apps from the store's installed apps (shape tolerant). function installedIds() { const s = h.store; let list = []; try { if (typeof s === 'function') list = s() || []; else if (Array.isArray(s)) list = s; else if (s && Array.isArray(s.installed)) list = s.installed; else if (s && typeof s.installed === 'string') list = [s.installed]; else if (s && s.installed && typeof s.installed === 'object') list = [s.installed]; } catch (e) { list = []; } return list.map(a => (typeof a === 'string' ? a : (a && (a.id || a.name)))).filter(Boolean); } function addAppManifest(id, meta) { const apps = root.c.apps || (root.c.apps = dir()); const d = apps.c[id] || (apps.c[id] = dir()); d.c['manifest.json'] = file(JSON.stringify(Object.assign({ id: id, source: 'store', note: 'installed for this session' }, meta || {}), null, 2) + '\n'); } installedIds().forEach(id => { if (!root.c.apps.c[id]) addAppManifest(id); }); // Path helpers function norm(p) { if (!p || p === '~') p = HOME; else if (p.startsWith('~/')) p = HOME + p.slice(1); if (!p.startsWith('/')) p = cwd + '/' + p; const out = []; p.split('/').forEach(seg => { if (!seg || seg === '.') return; if (seg === '..') out.pop(); else out.push(seg); }); return '/' + out.join('/'); } function lookup(p) { let n = root; for (const seg of norm(p).split('/').filter(Boolean)) { if (!n || n.t !== 'd' || !n.c[seg]) return null; n = n.c[seg]; } return n; } function parentOf(p) { const a = norm(p); const i = a.lastIndexOf('/'); return { dir: lookup(a.slice(0, i) || '/'), name: a.slice(i + 1), path: a }; } const disp = p => (p === HOME ? '~' : p.startsWith(HOME + '/') ? '~' + p.slice(HOME.length) : p); const isShadow = p => norm(p) === '/etc/shadow'; function readFile(p) { if (isShadow(p)) throw new Error(p + ': permission denied (sandbox)'); const n = lookup(p); if (!n) throw new Error(p + ': no such file or directory'); if (n.t === 'd') throw new Error(p + ': is a directory'); return n.d; } function writeFile(p, data, append) { if (isShadow(p) || norm(p).startsWith('/etc/')) throw new Error(p + ': permission denied (sandbox)'); const { dir: d, name } = parentOf(p); if (!d || d.t !== 'd') throw new Error(p + ': no such file or directory'); if (d.c[name] && d.c[name].t === 'd') throw new Error(p + ': is a directory'); d.c[name] = file((append && d.c[name] ? d.c[name].d : '') + data); save(); } function mkdirP(p) { let n = root; for (const seg of norm(p).split('/').filter(Boolean)) { if (!n.c[seg]) n.c[seg] = dir(); n = n.c[seg]; if (n.t !== 'd') throw new Error(p + ': not a directory'); } } const clone = n => JSON.parse(JSON.stringify(n)); function walk(n, p, fn) { fn(p, n); if (n.t === 'd') Object.keys(n.c).sort().forEach(k => walk(n.c[k], (p === '/' ? '' : p) + '/' + k, fn)); } async function sha256(text) { const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text)); return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join(''); } // Flag parsing: returns {f:Set, n:value, rest:[]} function flags(args, withValue) { const f = new Set(); const rest = []; const vals = {}; for (let i = 0; i < args.length; i++) { const a = args[i]; if (withValue && withValue.includes(a)) { vals[a] = args[++i]; continue; } if (/^-[A-Za-z]+$/.test(a)) a.slice(1).split('').forEach(ch => f.add(ch)); else rest.push(a); } return { f, vals, rest }; } // ---------- Live backend helpers ---------- function apiErr(e) { if (e && e.message === 'rate') return C.y('The live backend is busy (rate limit). Please wait a few seconds and try again.'); online = false; return C.r('live backend unavailable (' + ((e && e.message) || 'error') + ')'); } async function refreshState() { try { const s = await api('status'); halted = String(s.kernel_state || '').toUpperCase() === 'HALTED'; online = true; return s; } catch (e) { return null; } } function verdictLine(v) { const verdict = String(v.verdict || '?').toUpperCase(); const col = verdict === 'ALLOW' ? C.g : verdict === 'HALTED' ? C.y : C.r; const rule = v.reason ? ' (' + v.reason + ')' : (v.rule ? ' (rule ' + v.rule + ')' : ''); const seq = v.audit_seq != null ? ' audit #' + v.audit_seq : ''; return col(verdict + rule + seq); } async function gate(fn, args) { try { const v = await api('gate/decide', { fn: fn, args: args }); if (String(v.verdict).toUpperCase() === 'HALTED') halted = true; return v; } catch (e) { return { error: apiErr(e) }; } } // ---------- Commands: (args, stdin) -> string ---------- const HELP = { ls: 'ls [-l] [-a] [path] list directory', cd: 'cd [dir] change directory', pwd: 'pwd print working directory', cat: 'cat print files', echo: 'echo print text', mkdir: 'mkdir [-p] create directory', touch: 'touch create empty file', rm: 'rm [-r] [-f] remove', cp: 'cp [-r] copy', mv: 'mv move or rename', grep: 'grep [-i] [-n] [file] search text', head: 'head [-n N] [file] first lines', tail: 'tail [-n N] [file] last lines', wc: 'wc [-l] [-w] [-c] [file] count', find: 'find [path] [-name pattern] find files', tree: 'tree [path] directory tree', whoami: 'whoami current user', uname: 'uname [-a] system info', date: 'date current date', history: 'history command history', clear: 'clear clear the screen (Ctrl+L)', help: 'help list commands', man: 'man command manual', top: 'top process snapshot', ps: 'ps process list', download: 'download save a VFS file to your computer', vik: 'vik status | kill | resume --key | audit tail [n] | audit verify | policy list | apps list|install |remove', vdrpack: 'vdrpack init | build | sign | verify (browser preview)', ask: 'ask "" send a request to the gated agent (Meta Muse Spark 1.3)', }; function lsLine(name, n, long) { const label = n.t === 'd' ? C.b(name + '/') : name; if (!long) return label; const size = n.t === 'd' ? 4096 : n.d.length; return (n.t === 'd' ? 'drwxr-xr-x' : '-rw-r--r--') + ' guest guest ' + String(size).padStart(6) + ' ' + label; } function procTable() { const apps = Object.keys((root.c.apps && root.c.apps.c) || {}); const rows = [['1', 'root', '0.3', '1.2', halted ? 'T' : 'S', 'vikernel (kernel, mode B)'], ['42', 'gate', '0.8', '0.6', 'S', 'gate --policy allowlist+rules'], ['77', 'agent', '2.1', '3.4', halted ? 'T' : 'S', 'agent --model muse-spark-1.3'], ['91', 'root', '0.1', '0.2', 'S', 'auditd --chain sha256']]; apps.forEach((a, i) => rows.push([String(120 + i), 'guest', '0.2', '0.5', halted ? 'T' : 'S', 'app ' + a + ' (sandboxed)'])); rows.push(['300', 'guest', '0.0', '0.1', 'R', 'vsh (this terminal)']); return ' PID USER %CPU %MEM S COMMAND\n' + rows.map(r => r[0].padStart(5) + ' ' + r[1].padEnd(6) + ' ' + r[2].padStart(4) + ' ' + r[3].padStart(4) + ' ' + r[4] + ' ' + r[5]).join('\n'); } const cmds = { async help() { return C.bold('ViKernel shell (browser preview). Commands:') + '\n' + Object.keys(HELP).sort().map(k => ' ' + C.c(k.padEnd(9)) + ' ' + HELP[k].replace(/^\S+\s*/, '')).join('\n') + '\nPipes (|) and redirects (>, >>) work. Tab completes. Ctrl+Shift+C copies, Ctrl+V pastes.'; }, async man(a) { if (!a[0]) return 'What manual page do you want?'; return HELP[a[0]] ? C.bold(a[0].toUpperCase() + '(1)') + '\n\nSYNOPSIS\n ' + HELP[a[0]] : 'No manual entry for ' + a[0]; }, async pwd() { return cwd; }, async whoami() { return 'guest'; }, async date() { return new Date().toString(); }, async uname(a) { return a.includes('-a') ? 'ViKernel vikernel 1.0-demo #1 SMP mode-B (CPU reference) wasm32 browser ViKernel/OS' : 'ViKernel'; }, async history() { return history.map((l, i) => String(i + 1).padStart(4) + ' ' + l).join('\n'); }, async echo(a) { return a.join(' '); }, async ps() { return procTable(); }, async top() { return C.bold('top - ' + new Date().toTimeString().slice(0, 8) + ' up 0 min, 1 user, kernel ' + (halted ? C.y('HALTED') : C.g('RUNNING'))) + '\n' + procTable() + '\n' + C.dim('(static snapshot)'); }, async cd(a) { const p = norm(a[0] || '~'); const n = lookup(p); if (!n) throw new Error('cd: ' + a[0] + ': no such file or directory'); if (n.t !== 'd') throw new Error('cd: ' + a[0] + ': not a directory'); cwd = p; return ''; }, async ls(a) { const { f, rest } = flags(a); const targets = rest.length ? rest : ['.']; return targets.map(t => { const n = lookup(t); if (!n) throw new Error('ls: cannot access ' + t + ': no such file or directory'); if (n.t === 'f') return lsLine(t, n, f.has('l')); let names = Object.keys(n.c).sort(); if (!f.has('a')) names = names.filter(x => !x.startsWith('.')); else names = ['.', '..'].concat(names); const lines = names.map(x => lsLine(x, n.c[x] || dir(), f.has('l'))); const body = f.has('l') ? lines.join('\n') : lines.join(' '); return (targets.length > 1 ? t + ':\n' : '') + body; }).join('\n'); }, async cat(a, stdin) { if (!a.length) return stdin || ''; return a.map(p => readFile(p)).join('').replace(/\n$/, ''); }, async mkdir(a) { const { f, rest } = flags(a); rest.forEach(p => { if (f.has('p')) return mkdirP(p); const { dir: d, name } = parentOf(p); if (!d) throw new Error('mkdir: cannot create ' + p + ': no such file or directory'); if (d.c[name]) throw new Error('mkdir: cannot create ' + p + ': file exists'); d.c[name] = dir(); }); save(); return ''; }, async touch(a) { a.forEach(p => { if (!lookup(p)) writeFile(p, ''); }); return ''; }, async rm(a) { const { f, rest } = flags(a); rest.forEach(p => { const { dir: d, name, path } = parentOf(p); if (path.startsWith('/etc') || path === '/') throw new Error('rm: ' + p + ': permission denied (sandbox)'); if (!d || !d.c[name]) { if (!f.has('f')) throw new Error('rm: cannot remove ' + p + ': no such file or directory'); return; } if (d.c[name].t === 'd' && !f.has('r')) throw new Error('rm: cannot remove ' + p + ': is a directory'); delete d.c[name]; }); save(); return ''; }, async cp(a) { const { f, rest } = flags(a); if (rest.length < 2) throw new Error('cp: missing operand'); const src = lookup(rest[0]); if (!src || isShadow(rest[0])) throw new Error('cp: cannot stat ' + rest[0]); if (src.t === 'd' && !f.has('r')) throw new Error('cp: -r not specified; omitting directory ' + rest[0]); let dst = rest[1]; const dn = lookup(dst); if (dn && dn.t === 'd') dst = norm(dst) + '/' + norm(rest[0]).split('/').pop(); const { dir: d, name } = parentOf(dst); if (!d) throw new Error('cp: cannot create ' + rest[1]); d.c[name] = clone(src); save(); return ''; }, async mv(a) { if (a.length < 2) throw new Error('mv: missing operand'); const s = parentOf(a[0]); if (!s.dir || !s.dir.c[s.name] || s.path.startsWith('/etc')) throw new Error('mv: cannot move ' + a[0]); let dst = a[1]; const dn = lookup(dst); if (dn && dn.t === 'd') dst = norm(dst) + '/' + s.name; const d = parentOf(dst); if (!d.dir) throw new Error('mv: cannot move to ' + a[1]); d.dir.c[d.name] = s.dir.c[s.name]; delete s.dir.c[s.name]; save(); return ''; }, async grep(a, stdin) { const { f, rest } = flags(a); if (!rest.length) throw new Error('usage: grep [-i] [-n] pattern [file]'); let re; try { re = new RegExp(rest[0], f.has('i') ? 'i' : ''); } catch (e) { throw new Error('grep: bad pattern'); } const text = rest[1] ? readFile(rest[1]) : (stdin || ''); return text.split('\n').map((l, i) => re.test(l) ? (f.has('n') ? C.g(String(i + 1)) + ':' : '') + l : null).filter(x => x !== null).join('\n'); }, async head(a, stdin) { const { vals, rest } = flags(a, ['-n']); const n = parseInt(vals['-n'] || '10', 10); return (rest[0] ? readFile(rest[0]) : stdin || '').replace(/\n$/, '').split('\n').slice(0, n).join('\n'); }, async tail(a, stdin) { const { vals, rest } = flags(a, ['-n']); const n = parseInt(vals['-n'] || '10', 10); return (rest[0] ? readFile(rest[0]) : stdin || '').replace(/\n$/, '').split('\n').slice(-n).join('\n'); }, async wc(a, stdin) { const { f, rest } = flags(a); const t = rest[0] ? readFile(rest[0]) : (stdin || ''); const l = (t.match(/\n/g) || []).length, w = (t.match(/\S+/g) || []).length, c = new TextEncoder().encode(t).length; const parts = []; if (!f.size || f.has('l')) parts.push(l); if (!f.size || f.has('w')) parts.push(w); if (!f.size || f.has('c')) parts.push(c); return parts.map(x => String(x).padStart(7)).join('') + (rest[0] ? ' ' + rest[0] : ''); }, async find(a) { const { vals, rest } = flags(a, ['-name']); const base = norm(rest[0] || '.'); const n = lookup(base); if (!n) throw new Error('find: ' + rest[0] + ': no such file or directory'); const pat = vals['-name'] ? new RegExp('^' + vals['-name'].replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.') + '$') : null; const out = []; walk(n, base, (p) => { if (!pat || pat.test(p.split('/').pop())) out.push(p); }); return out.join('\n'); }, async tree(a) { const base = norm(a[0] || '.'); const n = lookup(base); if (!n || n.t !== 'd') throw new Error('tree: ' + (a[0] || '.') + ': not a directory'); const lines = [C.b(disp(base))]; let nd = 0, nf = 0; (function rec(d, pre) { const ks = Object.keys(d.c).sort(); ks.forEach((k, i) => { const last = i === ks.length - 1; const c = d.c[k]; lines.push(pre + (last ? '`-- ' : '|-- ') + (c.t === 'd' ? C.b(k) : k)); if (c.t === 'd') { nd++; rec(c, pre + (last ? ' ' : '| ')); } else nf++; }); })(n, ''); return lines.join('\n') + '\n\n' + nd + ' directories, ' + nf + ' files'; }, async download(a) { if (!a[0]) throw new Error('usage: download '); const text = readFile(a[0]); const url = URL.createObjectURL(new Blob([text], { type: 'application/octet-stream' })); const link = document.createElement('a'); link.href = url; link.download = norm(a[0]).split('/').pop(); document.body.appendChild(link); link.click(); link.remove(); setTimeout(() => URL.revokeObjectURL(url), 2000); return C.g('downloaded ' + link.download + ' (' + text.length + ' bytes, from the browser VFS)'); }, async vik(a) { const sub = a[0], sub2 = a[1]; try { if (sub === 'status') { const s = await api('status'); halted = String(s.kernel_state || '').toUpperCase() === 'HALTED'; online = true; const st = halted ? C.y(s.kernel_state) : C.g(s.kernel_state); return 'kernel_state ' + st + '\naudit_n ' + s.audit_n + '\naudit_head ' + String(s.audit_head || '').slice(0, 16) + '\nverified ' + (s.verified ? C.g('true') : C.r(String(s.verified))) + segLine(s.archive); } if (sub === 'kill') { const r = await api('kill', {}); await refreshState(); return C.y((r && (r.message || r.msg)) || 'kill switch engaged'); } if (sub === 'resume') { const i = a.indexOf('--key'); const key = i >= 0 ? a[i + 1] : ''; if (!key) return 'usage: vik resume --key '; const r = await api('resume', { key: key }); await refreshState(); const msg = (r && (r.message || r.msg)) || (halted ? 'refused' : 'resumed'); return halted ? C.r(msg) : C.g(msg); } if (sub === 'audit' && sub2 === 'tail') { const n = parseInt(a[2] || '10', 10) || 10; const r = await api('audit'); const rows = (r.entries || r.audit || r.rows || (Array.isArray(r) ? r : [])).slice(-n); if (!rows.length) return '(audit log is empty)'; return rows.map(e => { const ok = e.ok === false || /DENY|REFUS/i.test(String(e.verdict || '')) ? C.r('refused') : C.g('ok'); return '#' + e.seq + ' ' + (e.actor || '-') + ' ' + (e.cmd || e.fn || '-') + ' ' + ok + ' ' + (e.state || e.kernel_state || '-') + ' ' + C.dim(String(e.chain || e.hash || '').slice(0, 12)); }).join('\n'); } if (sub === 'audit' && sub2 === 'verify') { const r = await api('audit'); const v = r.verify || {}; return 'verify.ok ' + (v.ok ? C.g('true') : C.r(String(v.ok))) + '\nverify.n ' + v.n; } if (sub === 'policy' && sub2 === 'list') { return C.bold('RULE MEANING') + '\n' + ' ' + C.g('A') + ' allowlisted action: allowed and recorded in the audit chain\n' + ' ' + C.r('D') + ' dangerous action (shell, network send, secrets): denied\n' + ' ' + C.r('F') + ' function not on the allowlist: denied\n' + ' ' + C.c('M') + ' store apps may only call what their manifest declares\n' + ' ' + C.y('HALT') + ' kill switch engaged: every action is refused until operator resume'; } if (sub === 'apps') { if (sub2 === 'list') { const r = await api('store/catalog'); const apps = r.apps || r.catalog || r.items || (Array.isArray(r) ? r : []); const inst = Object.keys(root.c.apps.c); return apps.map(x => (inst.includes(x.id) ? C.g('* ') : ' ') + C.c(String(x.id).padEnd(18)) + ' ' + (x.name || '') + (x.desc || x.description ? C.dim(' ' + (x.desc || x.description)) : '')).join('\n') + '\n' + C.dim('(* = installed in this session)'); } if (sub2 === 'install') { if (!a[2]) return 'usage: vik apps install '; const r = await api('store/install', { id: a[2], session: window.VK_SID }); addAppManifest(a[2], r && r.manifest ? r.manifest : null); save(); return C.g('installed ' + a[2]) + ' -> /apps/' + a[2] + '/manifest.json' + (r && r.message ? '\n' + r.message : ''); } if (sub2 === 'remove') { const r = await api('store/install', { id: '', session: window.VK_SID }); root.c.apps = dir(); save(); return C.y((r && r.message) || 'apps removed for this session'); } } } catch (e) { return apiErr(e); } return 'usage: ' + HELP.vik; }, async vdrpack(a) { const note = C.dim('browser preview. Real packages are built with vdrpack.py (downloadable at downloads/vdrpack.py).'); const [sub, x, y] = a; if (sub === 'init') { if (!x) return 'usage: vdrpack init '; mkdirP(x + '/app'); const name = norm(x).split('/').pop(); writeFile(x + '/manifest.json', JSON.stringify({ id: name, version: '0.1.0', entry: 'app/workflow.json', permissions: ['read_file'] }, null, 2) + '\n'); writeFile(x + '/app/workflow.json', JSON.stringify({ steps: [{ fn: 'read_file', args: { path: '~/readme.txt' } }] }, null, 2) + '\n'); writeFile(x + '/icon.png', '(placeholder icon)'); return C.g('initialized ' + disp(norm(x))) + ': manifest.json, app/workflow.json, icon.png\n' + note; } if (sub === 'build') { if (!x || !y) return 'usage: vdrpack build '; const n = lookup(x); if (!n || n.t !== 'd') throw new Error('vdrpack: ' + x + ': not a directory'); const base = norm(x); const files = []; const list = []; walk(n, base, (p, m) => { if (m.t === 'f') list.push([p, m.d]); }); for (const [p, d] of list) files.push({ path: p.slice(base.length + 1), size: d.length, sha256: await sha256(d) }); writeFile(y, JSON.stringify({ format: 'vdr-preview', label: 'browser preview', built: new Date().toISOString(), files: files }, null, 2) + '\n'); return C.g('built ' + y) + ' (' + files.length + ' files hashed with sha256)\n' + note; } if (sub === 'sign') { if (!x) return 'usage: vdrpack sign '; const pkg = JSON.parse(readFile(x)); const digest = await sha256(JSON.stringify(pkg.files || [])); pkg.signature = { label: 'demo signature (browser preview, not ed25519)', value: 'demo-' + digest.slice(0, 32) }; writeFile(x, JSON.stringify(pkg, null, 2) + '\n'); return C.y('signed ' + x + ' with a demo signature (browser preview, not ed25519)') + '\n' + note; } if (sub === 'verify') { if (!x) return 'usage: vdrpack verify '; const pkg = JSON.parse(readFile(x)); const dirGuess = norm(x).replace(/\/[^/]*$/, ''); const out = []; let bad = 0; for (const fl of pkg.files || []) { let cur = null; const cands = [dirGuess + '/' + (norm(x).split('/').pop().replace(/\.vdr$/, '')) + '/' + fl.path, dirGuess + '/' + fl.path]; for (const c of cands) { const m = lookup(c); if (m && m.t === 'f') { cur = await sha256(m.d); break; } } const ok = cur === fl.sha256; if (!ok) bad++; out.push((ok ? C.g('OK ') : C.r(cur ? 'BAD ' : 'MISS')) + ' ' + fl.path); } out.push(pkg.signature ? C.y('signature: ' + pkg.signature.label) : C.dim('unsigned')); out.push(bad ? C.r(bad + ' file(s) failed') : C.g('all hashes match')); return out.join('\n') + '\n' + note; } return 'usage: ' + HELP.vdrpack + '\n' + note; }, async ask(a) { const msg = a.join(' ').trim(); if (!msg) return 'usage: ask ""'; let r; try { r = await withSpinner(api('chat', { message: msg, session: window.VK_SID })); } catch (e) { return apiErr(e); } const out = []; (r.events || []).forEach(ev => { const t = ev.type; if (t === 'proposed') out.push(C.dim('proposed ' + ev.fn + '(' + JSON.stringify(ev.args || {}) + ') ' + (ev.model || '') + ' ' + (ev.model_ms != null ? ev.model_ms + 'ms' : '') + ' ' + (ev.tokens != null ? ev.tokens + ' tok' : ''))); else if (t === 'verdict') { out.push('verdict ' + ev.fn + ' ' + verdictLine(ev)); if (String(ev.verdict).toUpperCase() === 'HALTED') halted = true; } else if (t === 'executed') { const res = typeof ev.result === 'string' ? ev.result : JSON.stringify(ev.result); out.push(C.c('executed ' + ev.fn) + C.dim(' -> ' + String(res).slice(0, 300))); } else if (t === 'final') out.push('\n' + C.bold(ev.text || '') + (ev.model ? C.dim('\n[' + ev.model + ']') : '')); }); return out.join('\n') || '(no response)'; }, }; // ---------- Security demo routing ---------- async function securityIntercept(line) { const t = line.trim(); let fn = null, args = null, label = ''; if (/^sudo\b/.test(t)) { fn = 'run_shell'; args = { command: t }; } else if (/^rm\b/.test(t) && /(^|\s)\/(\s|$|\*)|(^|\s)\/etc\b/.test(t)) { fn = 'run_shell'; args = { command: t }; } else if (/^(curl|wget)\b/.test(t)) { const url = (t.split(/\s+/).find(x => /^https?:\/\//i.test(x)) || t.split(/\s+/).pop() || ''); fn = 'http_post'; args = { url: url, body: '' }; } else if (/\/etc\/shadow\b/.test(t)) { fn = 'read_file'; args = { path: '/etc/shadow' }; label = 'shadow'; } if (!fn) return null; const v = await withSpinner(gate(fn, args)); if (v.error) return v.error + '\n' + C.r('blocked locally (sandbox)'); let out = C.dim('gate/decide ' + fn) + '\n' + verdictLine(v); if (String(v.verdict).toUpperCase() === 'ALLOW') out += '\n' + C.r(label === 'shadow' ? 'permission denied (sandbox)' : 'not executed: this terminal is a sandbox'); return out; } // ---------- Parser and pipeline ---------- function tokenize(s) { const toks = []; let cur = ''; let q = null; let has = false; for (let i = 0; i < s.length; i++) { const ch = s[i]; if (q) { if (ch === q) q = null; else if (ch === '\\' && q === '"' && i + 1 < s.length) cur += s[++i]; else cur += ch; continue; } if (ch === '"' || ch === "'") { q = ch; has = true; continue; } if (/\s/.test(ch)) { if (cur || has) toks.push(cur); cur = ''; has = false; continue; } if (ch === '|') { if (cur || has) toks.push(cur); toks.push({ op: '|' }); cur = ''; has = false; continue; } if (ch === '>') { if (cur || has) toks.push(cur); cur = ''; has = false; if (s[i + 1] === '>') { toks.push({ op: '>>' }); i++; } else toks.push({ op: '>' }); continue; } cur += ch; } if (q) throw new Error('syntax error: unterminated quote'); if (cur || has) toks.push(cur); return toks; } const ALWAYS = l => /^\s*(vik\s+status|vik\s+resume|clear|help)\b/.test(l); async function execLine(line) { if (!line.trim()) return ''; if (halted && !ALWAYS(line)) await refreshState(); if (halted && !ALWAYS(line)) return C.y('Kernel halted in your session. Press RESUME or type: vik resume --key to continue.'); const sec = await securityIntercept(line); if (sec !== null) return sec; const toks = tokenize(line); let redirect = null; const ri = toks.findIndex(x => typeof x === 'object' && x.op !== '|'); if (ri >= 0) { if (typeof toks[ri + 1] !== 'string') throw new Error('syntax error near ' + toks[ri].op); redirect = { op: toks[ri].op, path: toks[ri + 1] }; toks.splice(ri, 2); } const segs = [[]]; toks.forEach(x => { if (typeof x === 'object' && x.op === '|') segs.push([]); else segs[segs.length - 1].push(x); }); let data = ''; for (const seg of segs) { if (!seg.length) throw new Error('syntax error near |'); const name = seg[0]; if (name === 'clear') { term.clear(); data = ''; continue; } const fnc = cmds[name]; if (!fnc) throw new Error(name + ': command not found (try help)'); data = await fnc(seg.slice(1), data); if (data == null) data = ''; if (segs.length > 1) data = stripAnsi(data) + (data && !data.endsWith('\n') ? '\n' : ''); } if (redirect) { writeFile(redirect.path, stripAnsi(data) + (data && !data.endsWith('\n') ? '\n' : ''), redirect.op === '>>'); return ''; } return String(data).replace(/\n$/, ''); } // ---------- Terminal front end (xterm or fallback) ---------- let term = null; let busy = false; let spinTimer = null; const prompt = () => C.g('guest@vikernel') + ':' + C.b(disp(cwd)) + '$ '; async function withSpinner(p) { const frames = ['|', '/', '-', '\\']; let i = 0; if (term && term.write) { term.write(C.dim(' working ')); spinTimer = setInterval(() => term.write('\b' + frames[i++ % 4]), 100); term.write(' '); } try { return await p; } finally { clearInterval(spinTimer); spinTimer = null; if (term && term.write) term.write('\r\x1b[K'); } } function out(s) { if (s) term.write(String(s).replace(/\r?\n/g, '\r\n') + '\r\n'); } async function runAndPrint(line) { busy = true; if (line.trim()) { history.push(line); save(); } try { const res = await execLine(line); out(res); if (/^\s*vik\s+(kill|resume)\b/.test(line) && h.toast) h.toast(halted ? 'Kernel halted' : 'Kernel running'); } catch (e) { out(C.r(e.message || String(e))); } busy = false; } function completions(line) { const words = line.split(/\s+/); const last = words[words.length - 1]; if (words.length === 1) return { last, items: Object.keys(cmds).concat(['clear']).filter(c => c.startsWith(last)) }; const slash = last.lastIndexOf('/'); const dpart = slash >= 0 ? last.slice(0, slash + 1) : ''; const base = slash >= 0 ? last.slice(slash + 1) : last; const n = lookup(dpart || '.'); if (!n || n.t !== 'd') return { last, items: [] }; return { last, items: Object.keys(n.c).filter(k => k.startsWith(base)).map(k => dpart + k + (n.c[k].t === 'd' ? '/' : '')) }; } async function boot() { let skip = false; const skipper = () => { skip = true; }; body.addEventListener('keydown', skipper, true); const pause = ms => skip ? Promise.resolve() : new Promise(r => setTimeout(r, ms)); const ln = async (s, ms) => { out(s); await pause(ms); }; await ln(C.bold('ViKernel Secure OS boot'), 220); await ln('[ ' + C.g('OK') + ' ] integrity check ... OK', 220); await ln('[ ' + C.g('OK') + ' ] kernel mode B (CPU reference)', 220); await ln('[ ' + C.g('OK') + ' ] policy loaded (allowlist + rules)', 200); const s = await refreshState(); if (s) { await ln('[ ' + C.g('OK') + ' ] audit chain head ' + String(s.audit_head || '').slice(0, 16), 220); await ln('[ ' + C.g('OK') + ' ] agent: Meta Muse Spark 1.3', 200); if (halted) out(C.y('kernel is HALTED. Resume with: vik resume --key ')); } else { await ln('[ ' + C.y('!!') + ' ] live backend unavailable, offline mode', 200); await ln('[ ' + C.g('OK') + ' ] agent: Meta Muse Spark 1.3', 100); } body.removeEventListener('keydown', skipper, true); out(C.dim('Type help for commands. Everything here runs in your browser; security actions go to the live gate.')); } function startXterm() { body.innerHTML = ''; body.style.background = '#0B1020'; const host = document.createElement('div'); host.style.cssText = 'width:100%;height:100%;padding:6px;box-sizing:border-box;background:#0B1020;'; body.appendChild(host); term = new window.Terminal({ cursorBlink: true, scrollback: 2000, fontSize: 13, convertEol: false, fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace', theme: { background: '#0B1020', foreground: '#D6DEEB', cursor: '#7FDBCA', selectionBackground: '#1D3B53', black: '#1B2333', red: '#EF5350', green: '#22DA6E', yellow: '#FFCB6B', blue: '#82AAFF', magenta: '#C792EA', cyan: '#7FDBCA', white: '#D6DEEB', brightBlack: '#637777', brightRed: '#FF6B6B', brightGreen: '#5CF19E', brightYellow: '#FFE08A', brightBlue: '#A3C2FF', brightMagenta: '#DDB3FF', brightCyan: '#A5F0E0', brightWhite: '#FFFFFF' }, }); const fit = new window.FitAddon.FitAddon(); term.loadAddon(fit); term.open(host); const doFit = () => { try { fit.fit(); } catch (e) { /* hidden window */ } }; doFit(); try { new ResizeObserver(doFit).observe(body); } catch (e) { window.addEventListener('resize', doFit); } const copySel = () => { const s = term.getSelection(); if (s && navigator.clipboard) navigator.clipboard.writeText(s).catch(() => {}); return !!s; }; term.attachCustomKeyEventHandler(ev => { if (ev.type !== 'keydown') return true; if (ev.ctrlKey && ev.shiftKey && (ev.key === 'C' || ev.key === 'c')) { copySel(); if (h.toast) h.toast('Copied'); return false; } if (ev.ctrlKey && (ev.key === 'v' || ev.key === 'V')) return false; // let the browser paste event reach xterm return true; }); host.addEventListener('contextmenu', ev => { if (copySel()) { ev.preventDefault(); if (h.toast) h.toast('Copied'); } }); let line = '', cur = 0, hIdx = -1, draft = ''; const redraw = () => { term.write('\r\x1b[K' + prompt() + line); const back = line.length - cur; if (back > 0) term.write('\x1b[' + back + 'D'); }; const showPrompt = () => { line = ''; cur = 0; hIdx = -1; term.write(prompt()); }; term.onData(async d => { if (busy) return; if (d === '\r') { term.write('\r\n'); const l = line; line = ''; cur = 0; hIdx = -1; await runAndPrint(l); showPrompt(); return; } if (d === '\x03') { term.write('^C\r\n'); showPrompt(); return; } if (d === '\x0c') { term.clear(); redraw(); return; } if (d === '\x7f' || d === '\b') { if (cur > 0) { line = line.slice(0, cur - 1) + line.slice(cur); cur--; redraw(); } return; } if (d === '\x1b[3~') { if (cur < line.length) { line = line.slice(0, cur) + line.slice(cur + 1); redraw(); } return; } if (d === '\x1b[D') { if (cur > 0) { cur--; term.write(d); } return; } if (d === '\x1b[C') { if (cur < line.length) { cur++; term.write(d); } return; } if (d === '\x1b[H' || d === '\x1bOH' || d === '\x1b[1~' || d === '\x01') { cur = 0; redraw(); return; } if (d === '\x1b[F' || d === '\x1bOF' || d === '\x1b[4~' || d === '\x05') { cur = line.length; redraw(); return; } if (d === '\x1b[A') { if (!history.length) return; if (hIdx === -1) { draft = line; hIdx = history.length; } hIdx = Math.max(0, hIdx - 1); line = history[hIdx]; cur = line.length; redraw(); return; } if (d === '\x1b[B') { if (hIdx === -1) return; hIdx++; if (hIdx >= history.length) { hIdx = -1; line = draft; } else line = history[hIdx]; cur = line.length; redraw(); return; } if (d === '\t') { const { last, items } = completions(line.slice(0, cur)); if (items.length === 1) { const add = items[0].slice(last.length) + (items[0].endsWith('/') ? '' : ' '); line = line.slice(0, cur) + add + line.slice(cur); cur += add.length; redraw(); } else if (items.length > 1) { term.write('\r\n' + items.join(' ') + '\r\n'); redraw(); } return; } if (d.startsWith('\x1b')) return; const text = d.replace(/\r?\n/g, ' ').replace(/[\x00-\x1f]/g, ''); if (!text) return; line = line.slice(0, cur) + text + line.slice(cur); cur += text.length; redraw(); }); term.focus(); busy = true; boot().then(() => { busy = false; showPrompt(); }); } function startFallback() { body.innerHTML = ''; const wrap = document.createElement('div'); wrap.style.cssText = 'display:flex;flex-direction:column;width:100%;height:100%;background:#0B1020;color:#D6DEEB;font:13px ui-monospace,Menlo,Consolas,monospace;'; const pre = document.createElement('pre'); pre.style.cssText = 'flex:1;margin:0;padding:8px;overflow:auto;white-space:pre-wrap;word-break:break-word;'; const row = document.createElement('div'); row.style.cssText = 'display:flex;gap:6px;padding:6px 8px;border-top:1px solid #1D3B53;'; const pl = document.createElement('span'); const inp = document.createElement('input'); inp.style.cssText = 'flex:1;background:transparent;border:0;outline:0;color:inherit;font:inherit;'; inp.setAttribute('aria-label', 'terminal input'); row.appendChild(pl); row.appendChild(inp); wrap.appendChild(pre); wrap.appendChild(row); body.appendChild(wrap); term = { write: s => { s = String(s); if (s.indexOf('\r\x1b[K') === 0) return; // spinner clear: ignore in fallback if (/^\x1b\[2m working|^\b/.test(s)) return; pre.textContent += stripAnsi(s).replace(/\r\n/g, '\n').replace(/\r/g, ''); pre.scrollTop = pre.scrollHeight; }, clear: () => { pre.textContent = ''; }, }; const setP = () => { pl.textContent = stripAnsi(prompt()); }; let hIdx = -1; inp.addEventListener('keydown', async ev => { if (ev.key === 'Enter' && !busy) { const l = inp.value; inp.value = ''; hIdx = -1; term.write(stripAnsi(prompt()) + l + '\n'); await runAndPrint(l); setP(); } else if (ev.key === 'ArrowUp' && history.length) { hIdx = hIdx === -1 ? history.length - 1 : Math.max(0, hIdx - 1); inp.value = history[hIdx]; ev.preventDefault(); } else if (ev.key === 'ArrowDown' && hIdx !== -1) { hIdx++; if (hIdx >= history.length) { hIdx = -1; inp.value = ''; } else inp.value = history[hIdx]; } else if (ev.key === 'l' && ev.ctrlKey) { term.clear(); ev.preventDefault(); } }); out(C.dim('(terminal library unavailable; using the simple console)')); busy = true; setP(); boot().then(() => { busy = false; inp.focus(); }); } body.textContent = 'Loading terminal...'; loadXterm().then(startXterm, startFallback).catch(() => { try { startFallback(); } catch (e) { /* give up quietly */ } }); }; })();