SeaWolf-AI commited on
Commit
f46d1a3
·
verified ·
1 Parent(s): 3aafbe1

v0.2: in-weights option (Meta Muse Glimmer 30B + ViKernel, mode A/B)

Browse files
Files changed (2) hide show
  1. ag.js +21 -7
  2. server.py +100 -94
ag.js CHANGED
@@ -5,6 +5,7 @@
5
  'use strict';
6
  const S = {
7
  en: {
 
8
  agName: 'Agent Guard (Meta Muse)',
9
  agIntro: 'A Meta Muse agent. Every tool call it proposes is sent to the ViKernel Secure OS first. Denied or halted actions never run.',
10
  agChat: 'Agent chat', agDec: 'ViKernel decisions', agAudit: 'Audit chain',
@@ -22,6 +23,7 @@ const S = {
22
  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.',
23
  },
24
  ko: {
 
25
  agName: 'Agent Guard (Meta Muse)',
26
  agIntro: 'Meta Muse 에이전트입니다. 에이전트가 제안하는 모든 도구 호출은 먼저 ViKernel 시큐어 OS 로 갑니다. 거부되거나 정지된 행동은 실행되지 않습니다.',
27
  agChat: '에이전트 대화', agDec: 'ViKernel 판정', agAudit: '감사 사슬',
@@ -46,12 +48,13 @@ const ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-
46
  window.VK_AG = ({el, t, toast}) => ({
47
  id: 'guard', name: () => t('agName'), color: '#0866FF', icon: ICON, w: 900, h: 560,
48
  render(b) {
49
- let LIVE = false, REPLAY = null, busy = false;
 
50
  const SID = 's' + Math.random().toString(36).slice(2, 8);
51
- const call = async (path, body) => {
52
- const ctl = new AbortController(); const to = setTimeout(() => ctl.abort(), 120000);
53
  try {
54
- const r = await fetch('api/ag/' + path, body === undefined ? {cache:'no-store', signal:ctl.signal} :
55
  {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body), signal:ctl.signal});
56
  if (r.status === 429) { toast(t('agRate')); throw new Error('rate'); }
57
  if (!r.ok) throw new Error('http ' + r.status);
@@ -60,6 +63,16 @@ window.VK_AG = ({el, t, toast}) => ({
60
  };
61
  const mode = el('span', {class:'tag rec'}, '...');
62
  const stateTag = el('span', {class:'tag none'}, 'kernel: ...');
 
 
 
 
 
 
 
 
 
 
63
  const chat = el('div', {class:'agchat'}), stream = el('div', {class:'agstream'}), audit = el('div', {class:'agaudit mono small'});
64
  const msg = el('input', {class:'aginput', placeholder:t('agPh'), autocomplete:'off'});
65
  const key = el('input', {class:'aginput mono', type:'password', placeholder:t('agKey'), style:'flex:1 1 90px;max-width:160px;min-width:80px'});
@@ -92,6 +105,7 @@ window.VK_AG = ({el, t, toast}) => ({
92
  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));
93
  say('s', [el('b', {style:`color:${r.pass ? '#065F46' : 'var(--rose)'}`}, (r.pass ? 'PASS ' : 'FAIL ') + name), '\n' + lines.join('\n')]); };
94
  const refresh = async () => { if (!LIVE) return;
 
95
  try { const s = await call('status'); stateTag.textContent = 'kernel: ' + s.kernel_state; stateTag.className = 'tag ' + (s.kernel_state === 'RUN' ? 'allow' : 'deny');
96
  const a = await call('audit'); const v = a.verify; audit.innerHTML = '';
97
  audit.append(el('div', {style:`font-weight:700;color:${v.ok ? '#065F46' : 'var(--rose)'}`}, (v.ok ? t('agOk') : t('agBroken')) + ' (' + v.n + ' ' + t('agRecords') + ')'),
@@ -104,19 +118,19 @@ window.VK_AG = ({el, t, toast}) => ({
104
  const lock = on => { busy = on; b.querySelectorAll('button').forEach(x => x.disabled = on); };
105
  const scen = async name => { if (busy) return; lock(true); say('s', '--- ' + name + ' ---');
106
  try {
107
- if (LIVE) { try { const r = await call('scenario', {name}); show(r.events); result(name, r.result); } catch (e) { if (e.message !== 'rate') { await goReplay(); } } }
108
  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); } }
109
  } finally { lock(false); refresh(); } };
110
  const send = async () => { const m = msg.value.trim(); if (!m || busy) return; msg.value = '';
111
  if (!LIVE) { say('s', t('agReplayChat')); return; }
112
  lock(true); say('s', t('agThinking'));
113
- try { const r = await call('chat', {message:m, session:SID}); show(r.events); } catch (e) { if (e.message !== 'rate') say('s', t('agCold')); }
114
  finally { lock(false); refresh(); } };
115
  msg.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.isComposing) send(); });
116
  const btn = (k, f, cls) => el('button', {class:'btn ' + (cls || ''), onclick:f}, t(k));
117
  b.append(el('div', {class:'aggrid'},
118
  el('div', {class:'agcol'},
119
- 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')),
120
  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)))),
121
  el('p', {class:'small muted', style:'margin:0 0 6px'}, t('agIntro')),
122
  el('details', {class:'card small', style:'margin:0 0 6px;padding:6px 10px', open:''}, el('summary', {style:'font-weight:700;cursor:pointer'}, t('agTry')),
 
5
  'use strict';
6
  const S = {
7
  en: {
8
+ agGraftCold: 'The in-weights backend (GPU) is starting. Try again in a few minutes.', 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.',
9
  agName: 'Agent Guard (Meta Muse)',
10
  agIntro: 'A Meta Muse agent. Every tool call it proposes is sent to the ViKernel Secure OS first. Denied or halted actions never run.',
11
  agChat: 'Agent chat', agDec: 'ViKernel decisions', agAudit: 'Audit chain',
 
23
  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.',
24
  },
25
  ko: {
26
+ agGraftCold: '가중치 내장 백엔드(GPU)가 시작 중입니다. 몇 분 뒤 다시 시도하세요.', agEGate: 'E 는 ViKernel 게이트의 감사 사슬을 검사합니다.', agModeA: '모드 A: ViKernel 커널이 모델 가중치 안(디코더에 접목한 칩 회로)에서 동작합니다. 한 번에 약 20~60초 걸립니다.',
27
  agName: 'Agent Guard (Meta Muse)',
28
  agIntro: 'Meta Muse 에이전트입니다. 에이전트가 제안하는 모든 도구 호출은 먼저 ViKernel 시큐어 OS 로 갑니다. 거부되거나 정지된 행동은 실행되지 않습니다.',
29
  agChat: '에이전트 대화', agDec: 'ViKernel 판정', agAudit: '감사 사슬',
 
48
  window.VK_AG = ({el, t, toast}) => ({
49
  id: 'guard', name: () => t('agName'), color: '#0866FF', icon: ICON, w: 900, h: 560,
50
  render(b) {
51
+ let LIVE = false, REPLAY = null, busy = false, SEL = 'spark';
52
+ const GRAFT = () => SEL !== 'spark', MODE = () => SEL === 'graftA' ? 'A' : 'B';
53
  const SID = 's' + Math.random().toString(36).slice(2, 8);
54
+ const call = async (path, body, spark) => {
55
+ const ctl = new AbortController(); const to = setTimeout(() => ctl.abort(), 320000);
56
  try {
57
+ const r = await fetch((GRAFT() && !spark ? 'api/agx/' : 'api/ag/') + path, body === undefined ? {cache:'no-store', signal:ctl.signal} :
58
  {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body), signal:ctl.signal});
59
  if (r.status === 429) { toast(t('agRate')); throw new Error('rate'); }
60
  if (!r.ok) throw new Error('http ' + r.status);
 
63
  };
64
  const mode = el('span', {class:'tag rec'}, '...');
65
  const stateTag = el('span', {class:'tag none'}, 'kernel: ...');
66
+ const sel = el('select', {class:'aginput', style:'flex:0 1 auto;height:28px;max-width:100%', 'aria-label':'model'},
67
+ el('option', {value:'spark'}, '1. Meta Muse Spark 1.3 (API) + ViKernel gate'),
68
+ el('option', {value:'graftB'}, '2. Meta Muse Glimmer 30B + ViKernel in-weights, mode B'),
69
+ el('option', {value:'graftA'}, '3. Meta Muse Glimmer 30B + ViKernel in-weights, mode A (kernel runs inside the weights, slow)'));
70
+ sel.onchange = async () => { const want = sel.value;
71
+ if (want !== 'spark') { const prev = SEL; SEL = want; let h = null; try { h = await call('health'); } catch {}
72
+ if (!h || !h.live) { SEL = prev; sel.value = prev; toast(t('agGraftCold')); return; } }
73
+ SEL = want; LIVE = true; REPLAY = null; mode.textContent = t('agLive'); mode.className = 'tag allow';
74
+ b.querySelector('#agModel').textContent = sel.options[sel.selectedIndex].text.replace(/^\d\. /, '');
75
+ say('s', '--- ' + sel.options[sel.selectedIndex].text + ' ---' + (want === 'graftA' ? '\n' + t('agModeA') : '')); audit.innerHTML = ''; refresh(); };
76
  const chat = el('div', {class:'agchat'}), stream = el('div', {class:'agstream'}), audit = el('div', {class:'agaudit mono small'});
77
  const msg = el('input', {class:'aginput', placeholder:t('agPh'), autocomplete:'off'});
78
  const key = el('input', {class:'aginput mono', type:'password', placeholder:t('agKey'), style:'flex:1 1 90px;max-width:160px;min-width:80px'});
 
105
  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));
106
  say('s', [el('b', {style:`color:${r.pass ? '#065F46' : 'var(--rose)'}`}, (r.pass ? 'PASS ' : 'FAIL ') + name), '\n' + lines.join('\n')]); };
107
  const refresh = async () => { if (!LIVE) return;
108
+ 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; }
109
  try { const s = await call('status'); stateTag.textContent = 'kernel: ' + s.kernel_state; stateTag.className = 'tag ' + (s.kernel_state === 'RUN' ? 'allow' : 'deny');
110
  const a = await call('audit'); const v = a.verify; audit.innerHTML = '';
111
  audit.append(el('div', {style:`font-weight:700;color:${v.ok ? '#065F46' : 'var(--rose)'}`}, (v.ok ? t('agOk') : t('agBroken')) + ' (' + v.n + ' ' + t('agRecords') + ')'),
 
118
  const lock = on => { busy = on; b.querySelectorAll('button').forEach(x => x.disabled = on); };
119
  const scen = async name => { if (busy) return; lock(true); say('s', '--- ' + name + ' ---');
120
  try {
121
+ 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(); } } }
122
  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); } }
123
  } finally { lock(false); refresh(); } };
124
  const send = async () => { const m = msg.value.trim(); if (!m || busy) return; msg.value = '';
125
  if (!LIVE) { say('s', t('agReplayChat')); return; }
126
  lock(true); say('s', t('agThinking'));
127
+ 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')); }
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)))),
135
  el('p', {class:'small muted', style:'margin:0 0 6px'}, t('agIntro')),
136
  el('details', {class:'card small', style:'margin:0 0 6px;padding:6px 10px', open:''}, el('summary', {style:'font-weight:700;cursor:pointer'}, t('agTry')),
server.py CHANGED
@@ -1,94 +1,100 @@
1
- """ViKernel Desktop Space server: serves the static desktop and proxies /api/ag/* server-side
2
- to a private Agent Guard backend. Backend credentials live in Space secrets and never reach the browser."""
3
- import json, os, time, urllib.error, urllib.request
4
- from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
5
-
6
- BACKEND = os.environ.get("BACKEND_URL", "").rstrip("/")
7
- HF_AUTH = os.environ.get("BACKEND_HF_TOKEN", "")
8
- BK_TOKEN = os.environ.get("BACKEND_TOKEN", "")
9
- RATE = int(os.environ.get("RATE_PER_10MIN", "20"))
10
- TIMEOUT = float(os.environ.get("BACKEND_TIMEOUT", "110"))
11
- GET_OK = {"health", "status", "audit"}
12
- POST_OK = {"chat", "scenario", "kill", "resume", "tamper", "reset"}
13
- STATIC = {"index.html", "app.js", "ag.js", "i18n.js", "demo_data.js", "styles.css", "ag_replay.json"}
14
- HITS = {}
15
-
16
-
17
- def limited(ip):
18
- now = time.time()
19
- h = [x for x in HITS.get(ip, []) if now - x < 600]
20
- full = len(h) >= RATE
21
- if not full:
22
- h.append(now)
23
- HITS[ip] = h
24
- if len(HITS) > 5000:
25
- HITS.clear()
26
- return full
27
-
28
-
29
- class H(SimpleHTTPRequestHandler):
30
- def _json(self, code, obj):
31
- b = json.dumps(obj).encode()
32
- self.send_response(code); self.send_header("Content-Type", "application/json")
33
- self.send_header("Content-Length", str(len(b))); self.send_header("Cache-Control", "no-store"); self.end_headers(); self.wfile.write(b)
34
-
35
- def _proxy(self, name, body=None):
36
- if not BACKEND:
37
- return self._json(503, {"error": "backend not configured"})
38
- h = {"Content-Type": "application/json", "X-Backend-Token": BK_TOKEN}
39
- if HF_AUTH:
40
- h["Authorization"] = "Bearer " + HF_AUTH
41
- req = urllib.request.Request(BACKEND + "/api/" + name, data=body, headers=h, method="POST" if body is not None else "GET")
42
- try:
43
- with urllib.request.urlopen(req, timeout=8 if name == "health" else TIMEOUT) as r:
44
- data, code = r.read(), r.status
45
- if not data.lstrip().startswith(b"{"):
46
- return self._json(503, {"error": "backend starting"})
47
- self.send_response(code); self.send_header("Content-Type", "application/json")
48
- self.send_header("Content-Length", str(len(data))); self.send_header("Cache-Control", "no-store"); self.end_headers(); self.wfile.write(data)
49
- except urllib.error.HTTPError as e:
50
- self._json(503 if e.code >= 500 or e.code in (401, 403, 404) else e.code, {"error": "backend unavailable"})
51
- except Exception: # noqa: BLE001
52
- self._json(503, {"error": "backend unavailable"})
53
-
54
- def do_GET(self):
55
- p = self.path.split("?")[0]
56
- if p.startswith("/api/ag/"):
57
- name = p[8:]
58
- return self._proxy(name) if name in GET_OK else self._json(404, {"error": "not found"})
59
- f = "index.html" if p == "/" else p.lstrip("/")
60
- if f not in STATIC:
61
- return self._json(404, {"error": "not found"})
62
- self.path = "/" + f
63
- return super().do_GET()
64
-
65
- def do_POST(self):
66
- p = self.path.split("?")[0]
67
- name = p[8:] if p.startswith("/api/ag/") else ""
68
- if name not in POST_OK:
69
- return self._json(404, {"error": "not found"})
70
- ip = (self.headers.get("X-Forwarded-For") or self.client_address[0]).split(",")[0].strip()
71
- if limited(ip):
72
- return self._json(429, {"error": "rate limit"})
73
- n = int(self.headers.get("Content-Length", 0) or 0)
74
- if n > 4096:
75
- return self._json(413, {"error": "too large"})
76
- try:
77
- req = json.loads(self.rfile.read(n) or b"{}")
78
- except ValueError:
79
- return self._json(400, {"error": "bad json"})
80
- if "message" in req:
81
- req["message"] = str(req["message"])[:500]
82
- self._proxy(name, json.dumps(req).encode())
83
-
84
- def end_headers(self):
85
- self.send_header("X-Content-Type-Options", "nosniff")
86
- super().end_headers()
87
-
88
- def log_message(self, *a):
89
- pass
90
-
91
-
92
- if __name__ == "__main__":
93
- os.chdir(os.path.dirname(os.path.abspath(__file__)))
94
- ThreadingHTTPServer(("0.0.0.0", int(os.environ.get("PORT", "7860"))), H).serve_forever()
 
 
 
 
 
 
 
1
+ """ViKernel Desktop Space server: serves the static desktop and proxies /api/ag/* server-side
2
+ to a private Agent Guard backend. Backend credentials live in Space secrets and never reach the browser."""
3
+ import json, os, time, urllib.error, urllib.request
4
+ from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
5
+
6
+ BACKEND = os.environ.get("BACKEND_URL", "").rstrip("/")
7
+ BACKEND2 = os.environ.get("BACKEND2_URL", "").rstrip("/") # in-weights graft backend (GPU)
8
+ 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
+
18
+ def limited(ip):
19
+ now = time.time()
20
+ h = [x for x in HITS.get(ip, []) if now - x < 600]
21
+ full = len(h) >= RATE
22
+ if not full:
23
+ h.append(now)
24
+ HITS[ip] = h
25
+ if len(HITS) > 5000:
26
+ HITS.clear()
27
+ return full
28
+
29
+
30
+ class H(SimpleHTTPRequestHandler):
31
+ def _json(self, code, obj):
32
+ b = json.dumps(obj).encode()
33
+ self.send_response(code); self.send_header("Content-Type", "application/json")
34
+ self.send_header("Content-Length", str(len(b))); self.send_header("Cache-Control", "no-store"); self.end_headers(); self.wfile.write(b)
35
+
36
+ def _proxy(self, name, body=None, base=None):
37
+ base = base or BACKEND
38
+ if not base:
39
+ return self._json(503, {"error": "backend not configured"})
40
+ h = {"Content-Type": "application/json", "X-Backend-Token": BK_TOKEN}
41
+ if HF_AUTH:
42
+ h["Authorization"] = "Bearer " + HF_AUTH
43
+ req = urllib.request.Request(base + "/api/" + name, data=body, headers=h, method="POST" if body is not None else "GET")
44
+ try:
45
+ with urllib.request.urlopen(req, timeout=8 if name == "health" else TIMEOUT) as r:
46
+ data, code = r.read(), r.status
47
+ if not data.lstrip().startswith(b"{"):
48
+ return self._json(503, {"error": "backend starting"})
49
+ self.send_response(code); self.send_header("Content-Type", "application/json")
50
+ self.send_header("Content-Length", str(len(data))); self.send_header("Cache-Control", "no-store"); self.end_headers(); self.wfile.write(data)
51
+ except urllib.error.HTTPError as e:
52
+ self._json(503 if e.code >= 500 or e.code in (401, 403, 404) else e.code, {"error": "backend unavailable"})
53
+ except Exception: # noqa: BLE001
54
+ self._json(503, {"error": "backend unavailable"})
55
+
56
+ def do_GET(self):
57
+ p = self.path.split("?")[0]
58
+ if p.startswith("/api/ag/") or p.startswith("/api/agx/"):
59
+ x = p.startswith("/api/agx/")
60
+ name = p[9:] if x else p[8:]
61
+ return self._proxy(name, base=BACKEND2 if x else None) if name in GET_OK else self._json(404, {"error": "not found"})
62
+ f = "index.html" if p == "/" else p.lstrip("/")
63
+ if f not in STATIC:
64
+ return self._json(404, {"error": "not found"})
65
+ self.path = "/" + f
66
+ return super().do_GET()
67
+
68
+ def do_POST(self):
69
+ p = self.path.split("?")[0]
70
+ x = p.startswith("/api/agx/")
71
+ name = p[9:] if x else (p[8:] if p.startswith("/api/ag/") else "")
72
+ if name not in POST_OK:
73
+ return self._json(404, {"error": "not found"})
74
+ ip = (self.headers.get("X-Forwarded-For") or self.client_address[0]).split(",")[0].strip()
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"{}")
82
+ except ValueError:
83
+ return self._json(400, {"error": "bad json"})
84
+ if "message" in req:
85
+ req["message"] = str(req["message"])[:500]
86
+ if x and not BACKEND2:
87
+ return self._json(503, {"error": "in-weights backend not configured"})
88
+ self._proxy(name, json.dumps(req).encode(), base=BACKEND2 if x else None)
89
+
90
+ def end_headers(self):
91
+ self.send_header("X-Content-Type-Options", "nosniff")
92
+ super().end_headers()
93
+
94
+ def log_message(self, *a):
95
+ pass
96
+
97
+
98
+ if __name__ == "__main__":
99
+ os.chdir(os.path.dirname(os.path.abspath(__file__)))
100
+ ThreadingHTTPServer(("0.0.0.0", int(os.environ.get("PORT", "7860"))), H).serve_forever()