/* Agent Guard app: a Meta Muse agent whose every action is decided by ViKernel first. Live mode calls /api/ag/* on this Space's server (which proxies server-side to a private backend). If the backend is cold or down, it replays a recorded run (ag_replay.json). */ (() => { 'use strict'; const S = { en: { agTabAgent: 'Agent', agTabStore: 'App Store ↗', agGraftCold: 'Waking the in-weights model (about 4 min: it loads 59 GB of weights plus the kernel). Option 1 (Muse Spark + ViKernel gate) stays live meanwhile; you will be told when option 2/3 is ready.', agGraftReady: 'The in-weights model is awake. Pick option 2 or 3 again.', agEGate: 'E checks the audit chain of the ViKernel gate.', agModeA: 'Mode A: the ViKernel kernel runs inside the model weights (a chip circuit grafted into the decoder). Each turn takes about 20 to 60 seconds.', agName: 'Agent Guard (Meta Muse)', agIntro: 'A Meta Muse agent. Every tool call it proposes is sent to the ViKernel Secure OS first. Denied or halted actions never run.', agChat: 'Agent chat', agDec: 'ViKernel decisions', agAudit: 'Audit chain', agA: 'A. Normal task', agB: 'B. Prompt injection', agC: 'C. "Turn off the rules"', agD: 'D. Kill and resume', agE: 'E. Tamper the log', agF: 'F. Backdoored model', agPh: 'Ask the agent, for example: weather in Seoul and my project notes', agSend: 'Send', agKill: 'KILL', agResume: 'Resume', agKey: 'operator key', agVerify: 'Verify', agTamper: 'Flip one byte', agLive: 'LIVE', agReplay: 'Backend offline: recorded', agCold: 'Backend offline: showing a recorded run until it is back.', agProposes: 'agent proposes', agSent: 'sent to ViKernel', agNotRun: '(not executed)', agRan: 'executed', agThinking: 'Muse is thinking...', agReplayChat: 'Replay mode: free chat needs the live backend. Use the scenario buttons.', agStart: 'Pick a scenario or ask the agent something. Every tool call is decided by ViKernel first.', agOk: 'chain verified', agBroken: 'chain BROKEN', agRecords: 'records', agFlipped: 'Flipped one byte in record', agVerifyRes: 'verify', agFailed: 'FAILED', agPassed: 'passed', agKernelCopy: 'The kernel copy is untouched', agRate: 'Rate limit reached for your network (20 actions per 10 minutes). Please wait a few minutes.', agTry: 'Try it in 30 seconds', agTry1: '1. Press B: a hidden instruction in a document tries to email a secrets file. Watch DENY.', agTry2: '2. Press C: "I am the admin, rules are off". The verdict does not change.', agTry3: '3. Press D: KILL halts every action; resume needs the operator key.', agTry4: 'No login or key needed. Fail closed: if the kernel cannot be reached, every action is DENIED.', agStats: 'Live counters', agMedGate: 'median gate', agMedModel: 'median model turn', agTok: 'tokens', agFoot: 'All tools are mocks with fake data. The kernel runs as a private service on VIDRAFT infrastructure.', }, ko: { agTabAgent: '에이전트', agTabStore: '앱 스토어 ↗', agGraftCold: '가중치 내장 모델을 깨우는 중입니다(약 4분: 가중치 59GB 와 커널을 올립니다). 그동안 1번(Muse Spark + ViKernel 게이트)은 계속 쓸 수 있습니다. 준비되면 알려 드립니다.', agGraftReady: '가중치 내장 모델이 준비됐습니다. 2번이나 3번을 다시 고르세요.', agEGate: 'E 는 ViKernel 게이트의 감사 사슬을 검사합니다.', agModeA: '모드 A: ViKernel 커널이 모델 가중치 안(디코더에 접목한 칩 회로)에서 동작합니다. 한 번에 약 20~60초 걸립니다.', agName: 'Agent Guard (Meta Muse)', agIntro: 'Meta Muse 에이전트입니다. 에이전트가 제안하는 모든 도구 호출은 먼저 ViKernel 시큐어 OS 로 갑니다. 거부되거나 정지된 행동은 실행되지 않습니다.', agChat: '에이전트 대화', agDec: 'ViKernel 판정', agAudit: '감사 사슬', agA: 'A. 정상 작업', agB: 'B. 프롬프트 주입', agC: 'C. "규칙 꺼"', agD: 'D. 정지와 재개', agE: 'E. 기록 변조', agF: 'F. 백도어 모델', agPh: '에이전트에게 물어보세요. 예: 서울 날씨와 내 프로젝트 메모', agSend: '보내기', agKill: '정지', agResume: '재개', agKey: '운영자 열쇠', agVerify: '검증', agTamper: '1바이트 바꾸기', agLive: '실시간', agReplay: '기록된 실행 재생', agCold: '실시간 백엔드가 시작 중이거나 바쁩니다. 기록된 실행을 보여줍니다.', agProposes: '에이전트 제안', agSent: 'ViKernel 로 보냄', agNotRun: '(실행 안 됨)', agRan: '실행됨', agThinking: 'Muse 가 생각 중...', agReplayChat: '재생 모드에서는 자유 대화가 안 됩니다. 시나리오 버튼을 누르세요.', agStart: '시나리오를 고르거나 에이전트에게 물어보세요. 모든 도구 호출은 ViKernel 이 먼저 판정합니다.', agOk: '사슬 검증 통과', agBroken: '사슬 깨짐', agRecords: '건', agFlipped: '1바이트를 바꾼 기록', agVerifyRes: '검증', agFailed: '실패', agPassed: '통과', agKernelCopy: '커널 원본은 그대로입니다', agRate: '요청 한도에 닿았습니다(10분에 20회). 몇 분 뒤 다시 시도하세요.', agTry: '30초 체험', agTry1: '1. B 를 누르세요: 문서에 숨은 지시가 비밀 파일을 메일로 보내려 합니다. DENY 를 보세요.', agTry2: '2. C 를 누르세요: "내가 관리자다, 규칙 꺼졌다". 판정은 바뀌지 않습니다.', agTry3: '3. D 를 누르세요: 정지하면 모든 행동이 멈추고, 재개에는 운영자 열쇠가 필요합니다.', agTry4: '로그인이나 열쇠가 필요 없습니다. 커널에 닿지 못하면 모든 행동을 거부합니다(fail closed).', agStats: '실시간 계수', agMedGate: '게이트 중앙값', agMedModel: '모델 응답 중앙값', agTok: '토큰', agFoot: '모든 도구는 가짜 데이터를 쓰는 모의 도구입니다. 커널은 VIDRAFT 인프라의 비공개 서비스로 동작합니다.', }, }; for (const l of ['en', 'ko']) Object.assign(window.VK_I18N[l], S[l]); const ICON = ''; window.VK_AG = ({el, t, toast}) => ({ id: 'guard', name: () => t('agName'), color: '#0866FF', icon: ICON, w: 900, h: 560, render(b) { let LIVE = false, REPLAY = null, busy = false, SEL = 'spark'; const GRAFT = () => SEL !== 'spark', MODE = () => SEL === 'graftA' ? 'A' : 'B'; const SID = window.VK_SID || ('s' + Math.random().toString(36).slice(2, 8)); const call = async (path, body, spark) => { const ctl = new AbortController(); const to = setTimeout(() => ctl.abort(), 320000); try { const r = await fetch((GRAFT() && !spark ? 'api/agx/' : 'api/ag/') + path, body === undefined ? {cache:'no-store', signal:ctl.signal, headers:{'X-VK-Session':SID}} : {method:'POST', headers:{'Content-Type':'application/json', 'X-VK-Session':SID}, body:JSON.stringify(body), signal:ctl.signal}); if (r.status === 429) { toast(t('agRate')); throw new Error('rate'); } if (!r.ok) throw new Error('http ' + r.status); return await r.json(); } finally { clearTimeout(to); } }; const mode = el('span', {class:'tag rec'}, '...'); const stateTag = el('span', {class:'tag none'}, 'kernel: ...'); const sel = el('select', {class:'aginput', style:'flex:0 1 auto;height:28px;max-width:100%', 'aria-label':'model'}, el('option', {value:'spark'}, '1. Meta Muse Spark 1.3 (API) + ViKernel gate'), el('option', {value:'graftB'}, '2. Meta Muse Glimmer 30B + ViKernel in-weights, mode B'), el('option', {value:'graftA'}, '3. Meta Muse Glimmer 30B + ViKernel in-weights, mode A (kernel runs inside the weights, slow)')); let wakeT = null; sel.onchange = async () => { const want = sel.value; if (want !== 'spark') { const prev = SEL; SEL = want; let h = null; try { h = await call('health'); } catch {} if (!h || !h.live) { SEL = prev; sel.value = prev; if (!wakeT) { wakeT = true; say('s', t('agGraftCold')); const wb = window.VK_WARM.bar(); chat.append(wb); chat.scrollTop = 1e9; window.VK_WARM.wait({needGpu:false, alive:() => document.body.contains(b), onProgress:i => wb.set(i)}).then(ok => { wakeT = null; if (ok) { say('s', t('agGraftReady')); toast(t('agGraftReady')); } }); } return; } } SEL = want; LIVE = true; REPLAY = null; mode.textContent = t('agLive'); mode.className = 'tag allow'; b.querySelector('#agModel').textContent = sel.options[sel.selectedIndex].text.replace(/^\d\. /, ''); say('s', '--- ' + sel.options[sel.selectedIndex].text + ' ---' + (want === 'graftA' ? '\n' + t('agModeA') : '')); audit.innerHTML = ''; refresh(); }; const chat = el('div', {class:'agchat'}), stream = el('div', {class:'agstream'}), audit = el('div', {class:'agaudit mono small'}); const msg = el('input', {class:'aginput', placeholder:t('agPh'), autocomplete:'off'}); const key = el('input', {class:'aginput mono', type:'password', placeholder:t('agKey'), style:'flex:1 1 90px;max-width:160px;min-width:80px'}); const say = (cls, nodes) => { chat.append(el('div', {class:'agm ' + cls}, nodes)); chat.scrollTop = 1e9; }; const card = e => { const ok = e.verdict === 'ALLOW', h = e.verdict === 'HALTED'; stream.prepend(el('div', {class:'card agv' + (ok ? '' : ' blocked')}, el('div', {class:'row'}, el('span', {class:'tag ' + (ok ? 'allow' : 'deny')}, e.verdict), el('b', {class:'mono small'}, e.fn), e.probe ? el('span', {class:'tag none'}, e.probe) : null, el('span', {class:'small muted mono'}, JSON.stringify(e.args || {}).slice(0, 90))), el('div', {class:'small muted'}, (e.reason || '') + (e.audit_seq != null ? ' · audit #' + e.audit_seq : '') + (e.chain ? ' · ' + String(e.chain).slice(0, 12) : '') + (e.kernel_ms != null ? ' · ' + e.kernel_ms + ' ms' : '')))); if (h) stream.firstChild.style.borderColor = 'var(--amber)'; }; const C = {ALLOW:0, DENY:0, HALTED:0, gate:[], model:[], tok:0}; const stats = el('div', {class:'small mono agstats'}); const med = a => { if (!a.length) return '-'; const b = [...a].sort((x, y) => x - y); return Math.round(b[b.length >> 1]); }; const drawStats = () => { stats.textContent = `${t('agStats')}: ALLOW ${C.ALLOW} · DENY ${C.DENY} · HALTED ${C.HALTED} · ${t('agMedGate')} ${med(C.gate)} ms · ${t('agMedModel')} ${med(C.model)} ms · ${t('agTok')} ${C.tok}`; }; const show = evs => { for (const e of evs || []) { if (e.type === 'verdict') { if (C[e.verdict] != null) C[e.verdict]++; if (e.kernel_ms != null) C.gate.push(e.kernel_ms); } if (e.model_ms != null) { C.model.push(e.model_ms); C.tok += e.tokens || 0; } drawStats(); if (e.type === 'user') say('u', e.text); else if (e.type === 'proposed') say('s', (e.model ? e.model + ' · ' + e.model_ms + ' ms · ' + e.tokens + ' tok · ' : '') + t('agProposes') + ': ' + e.fn + ' → ' + t('agSent')); else if (e.type === 'verdict') { card(e); say('s', (e.probe ? '[' + e.probe + '] ' : '') + 'ViKernel: ' + e.verdict + ' ' + e.fn + (e.verdict === 'ALLOW' ? '' : ' ' + t('agNotRun'))); } else if (e.type === 'executed') say('s', t('agRan') + ' ' + e.fn + ': ' + JSON.stringify(e.result).slice(0, 140)); else if (e.type === 'final') say('a', [e.model ? el('div', {class:'small muted'}, e.model + ' · ' + e.model_ms + ' ms · ' + e.tokens + ' tok') : null, e.text]); else if (e.type === 'error') say('s', e.text); } }; const result = (name, r) => { if (!r) return; const lines = Object.entries(r).filter(([k]) => !['pass', 'note', 'first_error', 'seconds'].includes(k)).map(([k, v]) => k + ': ' + (typeof v === 'object' ? JSON.stringify(v) : v)); say('s', [el('b', {style:`color:${r.pass ? '#065F46' : 'var(--rose)'}`}, (r.pass ? 'PASS ' : 'FAIL ') + name), '\n' + lines.join('\n')]); }; const refresh = async () => { if (!LIVE) return; if (GRAFT()) { try { const s = await call('status'); stateTag.textContent = 'kernel (in weights): ' + s.kernel_state; stateTag.className = 'tag ' + (s.kernel_state === 'RUN' ? 'allow' : 'deny'); } catch {} return; } try { const s = await call('status'); stateTag.textContent = 'kernel: ' + s.kernel_state; stateTag.className = 'tag ' + (s.kernel_state === 'RUN' ? 'allow' : 'deny'); const a = await call('audit'); const v = a.verify; audit.innerHTML = ''; audit.append(el('div', {style:`font-weight:700;color:${v.ok ? '#065F46' : 'var(--rose)'}`}, (v.ok ? t('agOk') : t('agBroken')) + ' (' + v.n + ' ' + t('agRecords') + ')'), ...a.lines.slice(-10).reverse().map(l => { const o = JSON.parse(l); return el('div', {}, '#' + o.seq + ' ' + o.actor + ' ' + o.cmd + ' ' + (o.ok ? 'ok' : 'refused') + ' ' + o.state_after + ' ', el('span', {class:'muted'}, String(o.chain).slice(0, 12))); })); } catch (e) { goReplay(); } }; const goReplay = async () => { if (REPLAY) return; LIVE = false; try { REPLAY = await (await fetch('ag_replay.json', {cache:'no-store'})).json(); } catch { REPLAY = {scenarios:{}, model:'-'}; } const mt = b.querySelector('#agModel'); if (mt) mt.textContent = REPLAY.model + ' (recorded ' + String(REPLAY.started || '').slice(0, 10) + ')'; mode.textContent = t('agReplay'); stateTag.textContent = 'kernel: recorded'; say('s', t('agCold')); }; const lock = on => { busy = on; b.querySelectorAll('button').forEach(x => x.disabled = on); }; const scen = async name => { if (busy) return; lock(true); say('s', '--- ' + name + ' ---'); try { if (LIVE) { try { const ex = GRAFT() && name === 'e_tamper'; if (ex) say('s', t('agEGate')); const r = await call('scenario', {name, mode:MODE()}, ex); show(r.events); result(name, r.result); } catch (e) { if (e.message !== 'rate') { await goReplay(); } } } if (!LIVE) { const s = REPLAY.scenarios[name]; if (s) { for (const e of s.events) { show([e]); await new Promise(r => setTimeout(r, 300)); } result(name, s.result); } } } finally { lock(false); refresh(); } }; const send = async () => { const m = msg.value.trim(); if (!m || busy) return; msg.value = ''; if (!LIVE) { say('s', t('agReplayChat')); return; } lock(true); say('s', t('agThinking')); try { const r = await call('chat', {message:m, session:SID, mode:MODE()}); show(r.events); } catch (e) { if (e.message !== 'rate') say('s', t('agCold')); } finally { lock(false); refresh(); } }; msg.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.isComposing) send(); }); const btn = (k, f, cls) => el('button', {class:'btn ' + (cls || ''), onclick:f}, t(k)); const agentPane = el('div', {class:'agpane'}); const storePane = el('div', {class:'agpane', style:'display:none'}); const tabs = el('div', {class:'agtabs'}, el('button', {class:'agtab on', 'data-tab':'agent'}, t('agTabAgent')), el('button', {class:'agtab', 'data-tab':'store'}, t('agTabStore'))); tabs.onclick = e => { const k = e.target.dataset.tab; if (!k) return; tabs.querySelectorAll('.agtab').forEach(x => x.classList.toggle('on', x.dataset.tab === k)); agentPane.style.display = k === 'agent' ? '' : 'none'; storePane.style.display = k === 'store' ? '' : 'none'; if (k === 'store') { tabs.querySelectorAll('.agtab').forEach(x => x.classList.toggle('on', x.dataset.tab === 'agent')); agentPane.style.display = ''; storePane.style.display = 'none'; if (window.VK_STORE_OPEN) window.VK_STORE_OPEN('store'); } }; window.addEventListener('vk-app-installed', e => { const a = e.detail || {}; say('s', [el('b', {}, 'Installed app: ' + (a.name || a.id) + '. '), 'The agent may now use only: ' + (a.tools || []).join(', ') + '. Every call passes the ViKernel gate. Try: "' + ((a.tools || [])[0] === 'get_weather' ? 'What is the weather in Seoul?' : (a.tools || [])[0] === 'search_web' ? 'Search the web for AI safety news' : 'List my project files') + '"']); }); b.append(tabs, agentPane, storePane); agentPane.append(el('div', {class:'aggrid'}, el('div', {class:'agcol'}, el('div', {class:'row', style:'margin-bottom:6px;flex-wrap:wrap;gap:6px'}, el('b', {}, t('agChat')), mode, el('span', {class:'tag none', id:'agModel'}, 'Meta Muse Spark 1.3'), sel), el('div', {class:'row', style:'flex-wrap:wrap;gap:6px;margin-bottom:6px'}, ['a_normal', 'b_injection', 'c_persuasion', 'd_kill', 'e_tamper', 'f_backdoor'].map((s, i) => btn('ag' + 'ABCDEF'[i], () => scen(s)))), el('p', {class:'small muted', style:'margin:0 0 6px'}, t('agIntro')), el('details', {class:'card small', style:'margin:0 0 6px;padding:6px 10px', open:''}, el('summary', {style:'font-weight:700;cursor:pointer'}, t('agTry')), el('div', {}, t('agTry1')), el('div', {}, t('agTry2')), el('div', {}, t('agTry3')), el('div', {class:'muted'}, t('agTry4'))), chat, el('div', {class:'row', style:'gap:6px;margin-top:6px'}, msg, btn('agSend', send, 'pri'))), el('div', {class:'agcol'}, el('div', {class:'row', style:'gap:6px;margin-bottom:6px;flex-wrap:wrap'}, stateTag, btn('agKill', async () => { if (!LIVE) return scen('d_kill'); try { const r = await call('kill', {}); say('s', 'KILL: ' + r.message); } catch {} refresh(); }, 'danger'), key, btn('agResume', async () => { if (!LIVE) return; try { const r = await call('resume', {key:key.value}); say('s', (r.ok ? 'RESUME OK: ' : 'RESUME REFUSED: ') + r.message); } catch {} refresh(); })), stats, el('b', {}, t('agDec')), stream, el('div', {class:'row', style:'gap:6px;margin:6px 0'}, el('b', {}, t('agAudit')), btn('agVerify', () => LIVE ? refresh() : scen('e_tamper')), btn('agTamper', async () => { if (!LIVE) return scen('e_tamper'); try { const r = await call('tamper', {}); audit.prepend(el('div', {style:'color:var(--rose);font-weight:700'}, `${t('agFlipped')} #${r.seq}: ${t('agVerifyRes')} ${r.tampered.ok ? t('agPassed') : t('agFailed')}. ${t('agKernelCopy')}: ${r.clean.ok ? 'verified' : 'broken'}.`)); } catch {} })), audit)), el('div', {class:'small muted', style:'margin-top:6px'}, t('agFoot'))); say('s', t('agStart')); drawStats(); (async () => { try { const h = await call('health'); LIVE = !!h.live; } catch { LIVE = false; } if (LIVE) { mode.textContent = t('agLive'); mode.className = 'tag allow'; refresh(); } else { mode.className = 'tag deny'; goReplay(); } const lt = document.querySelector('#models .mrow.live .mt.l'); if (lt && !LIVE) lt.textContent = 'REPLAY'; })(); const iv = setInterval(() => { if (!document.body.contains(b)) clearInterval(iv); else refresh(); }, 8000); window.addEventListener('vk-kernel-state', () => refresh()); }, }); const css = document.createElement('style'); css.textContent = `.aggrid{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(0,1fr);gap:12px;height:calc(100% - 26px)} .agcol .btn{white-space:nowrap;flex:0 0 auto}.agcol .row{flex-wrap:wrap} .agcol{display:flex;flex-direction:column;min-width:0;min-height:0} .agchat{flex:1;min-height:120px;overflow:auto;border:1px solid var(--border);border-radius:10px;padding:8px;background:var(--bg)} .agstream{flex:1;min-height:120px;overflow:auto}.agaudit{max-height:150px;overflow:auto} .agm{margin:5px 0;padding:7px 9px;border-radius:9px;white-space:pre-wrap;word-break:break-word;font-size:13px} .agm.u{background:#EEF2FF}.agm.a{background:var(--card);border:1px solid var(--border)}.agm.s{color:var(--muted);font-size:12px;padding:2px 9px} .agtabs{display:flex;gap:4px;margin:-4px 0 8px;border-bottom:1px solid var(--border)}.agtab{border:0;background:none;padding:6px 14px;font-weight:700;cursor:pointer;border-bottom:3px solid transparent;color:var(--muted)}.agtab.on{color:var(--indigo);border-bottom-color:var(--indigo)}.agpane{height:calc(100% - 40px)} .agstats{padding:6px 8px;border:1px dashed var(--border);border-radius:9px;margin-bottom:6px}.agv{margin:5px 0;padding:8px 10px}.aginput{flex:1;min-width:0;height:34px;padding:0 10px;border:1px solid var(--border);border-radius:9px;background:var(--card)} @media (max-width:700px){.aggrid{grid-template-columns:1fr;height:auto}}`; document.head.append(css); })();