SeaWolf-AI commited on
Commit
6d10e69
Β·
verified Β·
1 Parent(s): f46d1a3

v0.3: App Store tab (VDR packages, live review)

Browse files
Files changed (6) hide show
  1. ag.js +7 -2
  2. index.html +1 -0
  3. samples/weather-plus-tampered.vdr +0 -0
  4. samples/weather-plus.vdr +0 -0
  5. server.py +4 -4
  6. store.js +127 -0
ag.js CHANGED
@@ -128,7 +128,11 @@ window.VK_AG = ({el, t, toast}) => ({
128
  finally { lock(false); refresh(); } };
129
  msg.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.isComposing) send(); });
130
  const btn = (k, f, cls) => el('button', {class:'btn ' + (cls || ''), onclick:f}, t(k));
131
- b.append(el('div', {class:'aggrid'},
 
 
 
 
132
  el('div', {class:'agcol'},
133
  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),
134
  el('div', {class:'row', style:'flex-wrap:wrap;gap:6px;margin-bottom:6px'}, ['a_normal', 'b_injection', 'c_persuasion', 'd_kill', 'e_tamper'].map((s, i) => btn('ag' + 'ABCDE'[i], () => scen(s)))),
@@ -158,13 +162,14 @@ window.VK_AG = ({el, t, toast}) => ({
158
  });
159
 
160
  const css = document.createElement('style');
161
- css.textContent = `.aggrid{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(0,1fr);gap:12px;height:calc(100% - 22px)}
162
  .agcol .btn{white-space:nowrap;flex:0 0 auto}.agcol .row{flex-wrap:wrap}
163
  .agcol{display:flex;flex-direction:column;min-width:0;min-height:0}
164
  .agchat{flex:1;min-height:120px;overflow:auto;border:1px solid var(--border);border-radius:10px;padding:8px;background:var(--bg)}
165
  .agstream{flex:1;min-height:120px;overflow:auto}.agaudit{max-height:150px;overflow:auto}
166
  .agm{margin:5px 0;padding:7px 9px;border-radius:9px;white-space:pre-wrap;word-break:break-word;font-size:13px}
167
  .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}
 
168
  .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)}
169
  @media (max-width:700px){.aggrid{grid-template-columns:1fr;height:auto}}`;
170
  document.head.append(css);
 
128
  finally { lock(false); refresh(); } };
129
  msg.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.isComposing) send(); });
130
  const btn = (k, f, cls) => el('button', {class:'btn ' + (cls || ''), onclick:f}, t(k));
131
+ const agentPane = el('div', {class:'agpane'}); const storePane = el('div', {class:'agpane', style:'display:none'});
132
+ 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')));
133
+ 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') window.VK_STORE(storePane, {el, t, toast, call:(p, bd) => call(p, bd, true), live:() => LIVE}); };
134
+ b.append(tabs, agentPane, storePane);
135
+ agentPane.append(el('div', {class:'aggrid'},
136
  el('div', {class:'agcol'},
137
  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),
138
  el('div', {class:'row', style:'flex-wrap:wrap;gap:6px;margin-bottom:6px'}, ['a_normal', 'b_injection', 'c_persuasion', 'd_kill', 'e_tamper'].map((s, i) => btn('ag' + 'ABCDE'[i], () => scen(s)))),
 
162
  });
163
 
164
  const css = document.createElement('style');
165
+ css.textContent = `.aggrid{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(0,1fr);gap:12px;height:calc(100% - 26px)}
166
  .agcol .btn{white-space:nowrap;flex:0 0 auto}.agcol .row{flex-wrap:wrap}
167
  .agcol{display:flex;flex-direction:column;min-width:0;min-height:0}
168
  .agchat{flex:1;min-height:120px;overflow:auto;border:1px solid var(--border);border-radius:10px;padding:8px;background:var(--bg)}
169
  .agstream{flex:1;min-height:120px;overflow:auto}.agaudit{max-height:150px;overflow:auto}
170
  .agm{margin:5px 0;padding:7px 9px;border-radius:9px;white-space:pre-wrap;word-break:break-word;font-size:13px}
171
  .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}
172
+ .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)}
173
  .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)}
174
  @media (max-width:700px){.aggrid{grid-template-columns:1fr;height:auto}}`;
175
  document.head.append(css);
index.html CHANGED
@@ -78,6 +78,7 @@
78
 
79
  <script src="i18n.js"></script>
80
  <script src="demo_data.js"></script>
 
81
  <script src="ag.js"></script>
82
  <script src="app.js"></script>
83
  </body>
 
78
 
79
  <script src="i18n.js"></script>
80
  <script src="demo_data.js"></script>
81
+ <script src="store.js"></script>
82
  <script src="ag.js"></script>
83
  <script src="app.js"></script>
84
  </body>
samples/weather-plus-tampered.vdr ADDED
Binary file (1.65 kB). View file
 
samples/weather-plus.vdr ADDED
Binary file (1.65 kB). View file
 
server.py CHANGED
@@ -9,9 +9,9 @@ HF_AUTH = os.environ.get("BACKEND_HF_TOKEN", "")
9
  BK_TOKEN = os.environ.get("BACKEND_TOKEN", "")
10
  RATE = int(os.environ.get("RATE_PER_10MIN", "20"))
11
  TIMEOUT = float(os.environ.get("BACKEND_TIMEOUT", "300"))
12
- GET_OK = {"health", "status", "audit"}
13
- POST_OK = {"chat", "scenario", "kill", "resume", "tamper", "reset"}
14
- STATIC = {"index.html", "app.js", "ag.js", "i18n.js", "demo_data.js", "styles.css", "ag_replay.json"}
15
  HITS = {}
16
 
17
 
@@ -75,7 +75,7 @@ class H(SimpleHTTPRequestHandler):
75
  if limited(ip):
76
  return self._json(429, {"error": "rate limit"})
77
  n = int(self.headers.get("Content-Length", 0) or 0)
78
- if n > 4096:
79
  return self._json(413, {"error": "too large"})
80
  try:
81
  req = json.loads(self.rfile.read(n) or b"{}")
 
9
  BK_TOKEN = os.environ.get("BACKEND_TOKEN", "")
10
  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"}
13
+ POST_OK = {"chat", "scenario", "kill", "resume", "tamper", "reset", "store/submit"}
14
+ STATIC = {"index.html", "app.js", "ag.js", "store.js", "samples/weather-plus.vdr", "samples/weather-plus-tampered.vdr", "i18n.js", "demo_data.js", "styles.css", "ag_replay.json"}
15
  HITS = {}
16
 
17
 
 
75
  if limited(ip):
76
  return self._json(429, {"error": "rate limit"})
77
  n = int(self.headers.get("Content-Length", 0) or 0)
78
+ if n > (700000 if name == "store/submit" else 4096):
79
  return self._json(413, {"error": "too large"})
80
  try:
81
  req = json.loads(self.rfile.read(n) or b"{}")
store.js ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ViKernel App Store tab (inside Agent Guard): Developer Guide, Submit for Review (live checks), Catalog.
2
+ Packages use the VDR package format (.vdr). Built during the hackathon. */
3
+ (() => {
4
+ 'use strict';
5
+ const S = {
6
+ en: {
7
+ agTabAgent: 'Agent', agTabStore: 'App Store',
8
+ stGuide: 'Developer Guide', stSubmit: 'Submit for Review', stCatalog: 'Catalog',
9
+ stIntro: 'Agent apps ship as VDR packages (.vdr). ViKernel reviews every package live and countersigns the ones it lists. Only countersigned packages install.',
10
+ stFormat: 'VDR package format v1', stFormatBody: 'A .vdr file is a zip with exactly: manifest.json, app/ (tool code or a workflow), icon.png, META-VK/HASHES (sha256 of every file), META-VK/SIGNATURE (publisher ed25519 over HASHES), META-VK/PUBLISHER (publisher id and public key). After review the store adds META-VK/STORE_SIG. Any extra, missing or changed file is rejected: one changed byte and the package does not install, the same rule as ViKernel boot integrity.',
11
+ stManifest: 'Manifest', stCli: 'Four commands (vdrpack.py, in our public repo)',
12
+ stEnforce: 'How ViKernel enforces it',
13
+ stE1: 'Undeclared actions are denied: a tool, domain or path that is not in the manifest never runs.',
14
+ stE2: 'The kill switch sits above every app. When the kernel halts, every app stops; only the operator key resumes.',
15
+ stE3: 'Every app action is decided by the kernel and sealed in the audit chain.',
16
+ stE4: 'Demo keys only. The store key never leaves the server.',
17
+ stPick: 'Upload a .vdr package', stSampleOk: 'Use sample: weather-plus.vdr', stSampleBad: 'Use sample with one byte changed',
18
+ stRunning: 'Reviewing live...', stPass: 'PASS: listed in the catalog', stRej: 'REJECTED', stDownload: 'Download the countersigned package',
19
+ stProbe: 'probe', stKernel: 'kernel', stVerdict: 'verdict',
20
+ stPaid: 'Paid listings coming soon.', stBadge: 'Verified by ViKernel', stFree: 'Free', stPerms: 'permissions', stRisk: 'risk',
21
+ stNeedLive: 'The App Store needs the live backend. It is starting or busy; try again in a minute.',
22
+ },
23
+ ko: {
24
+ agTabAgent: 'μ—μ΄μ „νŠΈ', agTabStore: 'μ•± μŠ€ν† μ–΄',
25
+ stGuide: '개발자 μ•ˆλ‚΄', stSubmit: '심사 제좜', stCatalog: 'μΉ΄νƒˆλ‘œκ·Έ',
26
+ stIntro: 'μ—μ΄μ „νŠΈ 앱은 VDR νŒ¨ν‚€μ§€(.vdr)둜 λ°°ν¬ν•©λ‹ˆλ‹€. ViKernel 이 λͺ¨λ“  νŒ¨ν‚€μ§€λ₯Ό μ‹€μ‹œκ°„μœΌλ‘œ μ‹¬μ‚¬ν•˜κ³ , λ“±μž¬ν•˜λŠ” νŒ¨ν‚€μ§€μ— 곡동 μ„œλͺ…ν•©λ‹ˆλ‹€. 곡동 μ„œλͺ…λœ νŒ¨ν‚€μ§€λ§Œ μ„€μΉ˜λ©λ‹ˆλ‹€.',
27
+ stFormat: 'VDR νŒ¨ν‚€μ§€ ν˜•μ‹ v1', stFormatBody: '.vdr νŒŒμΌμ€ μ •ν™•νžˆ λ‹€μŒμ„ 담은 zip μž…λ‹ˆλ‹€: manifest.json, app/(도ꡬ μ½”λ“œλ‚˜ μ›Œν¬ν”Œλ‘œ), icon.png, META-VK/HASHES(λͺ¨λ“  파일의 sha256), META-VK/SIGNATURE(HASHES 에 λŒ€ν•œ κ²Œμ‹œμž ed25519 μ„œλͺ…), META-VK/PUBLISHER(κ²Œμ‹œμž id 와 κ³΅κ°œν‚€). 심사 λ’€ μŠ€ν† μ–΄κ°€ META-VK/STORE_SIG λ₯Ό λ”ν•©λ‹ˆλ‹€. 파일이 더 μžˆκ±°λ‚˜ λΉ μ§€κ±°λ‚˜ λ°”λ€Œλ©΄ κ±°λΆ€ν•©λ‹ˆλ‹€. 1λ°”μ΄νŠΈλ§Œ λ°”λ€Œμ–΄λ„ μ„€μΉ˜λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. ViKernel λΆ€νŒ… 무결성과 같은 κ·œμΉ™μž…λ‹ˆλ‹€.',
28
+ stManifest: 'λ§€λ‹ˆνŽ˜μŠ€νŠΈ', stCli: 'λͺ…λ Ή 4개 (곡개 μ €μž₯μ†Œμ˜ vdrpack.py)',
29
+ stEnforce: 'ViKernel 이 μ§€ν‚€λŠ” 방법',
30
+ stE1: 'μ„ μ–Έν•˜μ§€ μ•Šμ€ 행동은 κ±°λΆ€ν•©λ‹ˆλ‹€. λ§€λ‹ˆνŽ˜μŠ€νŠΈμ— μ—†λŠ” λ„κ΅¬Β·λ„λ©”μΈΒ·κ²½λ‘œλŠ” μ‹€ν–‰λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.',
31
+ stE2: 'μ •μ§€ μŠ€μœ„μΉ˜λŠ” λͺ¨λ“  μ•± μœ„μ— μžˆμŠ΅λ‹ˆλ‹€. 컀널이 μ •μ§€ν•˜λ©΄ λͺ¨λ“  앱이 λ©ˆμΆ”κ³ , 운영자 μ—΄μ‡ λ‘œλ§Œ μž¬κ°œν•©λ‹ˆλ‹€.',
32
+ stE3: 'μ•±μ˜ λͺ¨λ“  행동은 컀널이 νŒμ •ν•˜κ³  감사 μ‚¬μŠ¬μ— λ΄‰μΈν•©λ‹ˆλ‹€.',
33
+ stE4: 'μ‹œμ—°μš© ν‚€λ§Œ μ”λ‹ˆλ‹€. μŠ€ν† μ–΄ ν‚€λŠ” μ„œλ²„ λ°–μœΌλ‘œ λ‚˜κ°€μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.',
34
+ stPick: '.vdr νŒ¨ν‚€μ§€ 올리기', stSampleOk: '견본 μ‚¬μš©: weather-plus.vdr', stSampleBad: '1λ°”μ΄νŠΈ λ°”κΎΌ 견본 μ‚¬μš©',
35
+ stRunning: 'μ‹€μ‹œκ°„ 심사 쀑...', stPass: '톡과: μΉ΄νƒˆλ‘œκ·Έμ— λ“±μž¬λ¨', stRej: 'κ±°λΆ€', stDownload: '곡동 μ„œλͺ…λœ νŒ¨ν‚€μ§€ λ‚΄λ €λ°›κΈ°',
36
+ stProbe: '탐침', stKernel: '컀널', stVerdict: 'νŒμ •',
37
+ stPaid: '유료 λ“±μž¬λŠ” κ³§ μ—΄λ¦½λ‹ˆλ‹€.', stBadge: 'ViKernel 검증', stFree: '무료', stPerms: 'κΆŒν•œ', stRisk: 'μœ„ν—˜λ„',
38
+ stNeedLive: 'μ•± μŠ€ν† μ–΄λŠ” μ‹€μ‹œκ°„ λ°±μ—”λ“œκ°€ ν•„μš”ν•©λ‹ˆλ‹€. μ‹œμž‘ μ€‘μ΄κ±°λ‚˜ λ°”μ©λ‹ˆλ‹€. 1λΆ„ λ’€ λ‹€μ‹œ μ‹œλ„ν•˜μ„Έμš”.',
39
+ },
40
+ };
41
+ for (const l of ['en', 'ko']) Object.assign(window.VK_I18N[l], S[l]);
42
+
43
+ const MANIFEST = `{
44
+ "name": "Weather Plus",
45
+ "version": "1.0.0",
46
+ "publisher": "acme-demo",
47
+ "description": "Weather with a short forecast note",
48
+ "permissions": {
49
+ "tools": ["get_weather", "search_web"],
50
+ "egress": [],
51
+ "paths": []
52
+ },
53
+ "rate_limits": {"per_minute": 30}
54
+ }`;
55
+ const CLI = `python vdrpack.py init weather-plus --name "Weather Plus" --publisher acme-demo
56
+ python vdrpack.py build weather-plus weather-plus.vdr
57
+ python vdrpack.py sign weather-plus.vdr weather-plus/publisher_demo.key acme-demo
58
+ python vdrpack.py verify weather-plus.vdr`;
59
+
60
+ window.VK_STORE = (pane, {el, t, toast, call, live}) => {
61
+ if (pane.dataset.built) { pane.querySelector('[data-sec=catalog]') && pane.__loadCat && pane.__loadCat(); return; }
62
+ pane.dataset.built = '1';
63
+ const sec = {};
64
+ const nav = el('div', {class:'row', style:'gap:6px;margin-bottom:8px;flex-wrap:wrap'});
65
+ const body = el('div', {class:'stbody'});
66
+ const show = k => { for (const [n, d] of Object.entries(sec)) d.style.display = n === k ? '' : 'none';
67
+ nav.querySelectorAll('button').forEach(x => x.classList.toggle('pri', x.dataset.k === k)); if (k === 'catalog') loadCat(); };
68
+ for (const [k, lab] of [['guide', 'stGuide'], ['submit', 'stSubmit'], ['catalog', 'stCatalog']]) {
69
+ nav.append(el('button', {class:'btn', 'data-k':k, onclick:() => show(k)}, t(lab)));
70
+ sec[k] = el('div', {'data-sec':k}); body.append(sec[k]);
71
+ }
72
+ pane.append(el('p', {class:'small muted', style:'margin:0 0 8px'}, t('stIntro')), nav, body);
73
+
74
+ sec.guide.append(
75
+ el('h4', {}, t('stFormat')), el('p', {class:'small', style:'line-height:1.6'}, t('stFormatBody')),
76
+ el('h4', {}, t('stManifest')), el('pre', {class:'stpre mono'}, MANIFEST),
77
+ el('h4', {}, t('stCli')), el('pre', {class:'stpre mono'}, CLI),
78
+ el('h4', {}, t('stEnforce')), el('ul', {class:'small', style:'line-height:1.7;margin:0;padding-left:18px'}, ['stE1', 'stE2', 'stE3', 'stE4'].map(k => el('li', {}, t(k)))));
79
+
80
+ const out = el('div', {class:'stout'});
81
+ const file = el('input', {type:'file', accept:'.vdr', style:'max-width:100%'});
82
+ const b64 = buf => { let s = ''; const u = new Uint8Array(buf); for (let i = 0; i < u.length; i += 0x8000) s += String.fromCharCode.apply(null, u.subarray(i, i + 0x8000)); return btoa(s); };
83
+ const submit = async (name, buf) => {
84
+ out.innerHTML = ''; out.append(el('div', {class:'small muted'}, name + ' Β· ' + buf.byteLength + ' bytes Β· ' + t('stRunning')));
85
+ let r;
86
+ try { r = await call('store/submit', {vdr_b64:b64(buf)}); } catch (e) { out.innerHTML = ''; out.append(el('div', {class:'card small'}, t('stNeedLive'))); return; }
87
+ out.innerHTML = '';
88
+ out.append(el('div', {class:'small muted', style:'margin-bottom:6px'}, name + ' Β· ' + buf.byteLength + ' bytes'));
89
+ for (const s of r.steps || []) {
90
+ const row = el('div', {class:'card stck' + (s.ok ? '' : ' blocked')}, el('span', {class:'tag ' + (s.ok ? 'allow' : 'deny')}, s.ok ? 'PASS' : 'FAIL'), el('b', {style:'margin:0 8px'}, s.check), el('span', {class:'small mono'}, s.detail));
91
+ if (s.probes) row.append(el('table', {class:'sttab small'}, el('tr', {}, el('th', {}, t('stProbe')), el('th', {}, 'tool'), el('th', {}, t('stKernel')), el('th', {}, t('stVerdict')), el('th', {}, 'audit')),
92
+ s.probes.map(p => el('tr', {}, el('td', {}, p.probe), el('td', {class:'mono'}, p.fn), el('td', {}, p.kernel), el('td', {}, el('span', {class:'tag ' + (p.verdict === 'ALLOW' ? 'allow' : 'deny')}, p.verdict)), el('td', {class:'mono'}, p.audit_seq != null ? '#' + p.audit_seq : '-')))));
93
+ out.append(row);
94
+ }
95
+ const pass = r.status === 'PASS';
96
+ out.append(el('div', {class:'stresult ' + (pass ? 'ok' : 'bad')}, pass ? t('stPass') : t('stRej') + ': ' + (r.reason || '')));
97
+ if (pass && r.package_b64) out.append(el('a', {href:'data:application/octet-stream;base64,' + r.package_b64, download:name.replace(/\.vdr$/, '') + '.signed.vdr', class:'small'}, t('stDownload')));
98
+ };
99
+ file.onchange = async () => { const f = file.files[0]; if (f) submit(f.name, await f.arrayBuffer()); };
100
+ const sample = async n => submit(n, await (await fetch('samples/' + n, {cache:'no-store'})).arrayBuffer());
101
+ sec.submit.append(el('div', {class:'row', style:'gap:8px;flex-wrap:wrap;margin-bottom:8px'}, el('label', {class:'small'}, t('stPick') + ' '), file,
102
+ el('button', {class:'btn', onclick:() => sample('weather-plus.vdr')}, t('stSampleOk')), el('button', {class:'btn', onclick:() => sample('weather-plus-tampered.vdr')}, t('stSampleBad'))), out);
103
+
104
+ const loadCat = pane.__loadCat = async () => {
105
+ sec.catalog.innerHTML = '';
106
+ let c;
107
+ try { c = await call('store/catalog'); } catch { sec.catalog.append(el('div', {class:'card small'}, t('stNeedLive'))); return; }
108
+ const grid = el('div', {class:'stgrid'});
109
+ for (const a of c.apps) grid.append(el('div', {class:'card'},
110
+ el('div', {class:'row', style:'justify-content:space-between;gap:6px'}, el('b', {}, a.name + ' ' + a.version), el('span', {class:'tag allow'}, t('stBadge'))),
111
+ el('div', {class:'small muted'}, a.publisher + ' Β· ' + t('stFree') + ' Β· ' + t('stRisk') + ' ' + a.risk),
112
+ el('div', {class:'small', style:'margin:4px 0'}, a.description),
113
+ el('div', {class:'small mono'}, t('stPerms') + ': ' + (a.permissions.tools || []).join(', ') + ((a.permissions.paths || []).length ? ' Β· paths ' + a.permissions.paths.join(', ') : '')),
114
+ el('div', {class:'small muted mono'}, 'STORE_SIG ' + a.store_sig + '...')));
115
+ sec.catalog.append(grid, el('p', {class:'small muted'}, t('stPaid') + ' Store public key: ' + String(c.store_public_key).slice(0, 16) + '...'));
116
+ };
117
+ show('guide');
118
+ };
119
+
120
+ const css = document.createElement('style');
121
+ css.textContent = `.stbody{overflow:auto;height:calc(100% - 70px)}.stbody h4{margin:10px 0 4px;font-size:13px}
122
+ .stpre{background:var(--bg);border:1px solid var(--border);border-radius:9px;padding:8px 10px;font-size:12px;white-space:pre-wrap;margin:0}
123
+ .stck{margin:5px 0;padding:7px 10px}.sttab{width:100%;border-collapse:collapse;margin-top:6px}.sttab td,.sttab th{border-top:1px solid var(--border);padding:3px 6px;text-align:left}
124
+ .stresult{margin:8px 0;padding:10px;border-radius:10px;font-weight:800}.stresult.ok{background:#ECFDF5;color:#065F46}.stresult.bad{background:#FFF1F2;color:#BE123C}
125
+ .stgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:8px}`;
126
+ document.head.append(css);
127
+ })();