115 lines
6.3 KiB
Python
115 lines
6.3 KiB
Python
import base64
|
|
import hmac
|
|
import json
|
|
import os
|
|
import sys
|
|
import threading
|
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
# Embedded Python uses an isolated ._pth and does not add the script directory.
|
|
# Resolve sibling modules from this service's source, not an old runtime copy.
|
|
sys.path.insert(0,os.path.dirname(os.path.abspath(__file__)))
|
|
sys.dont_write_bytecode=True
|
|
from windows_containment import contain_inspector
|
|
# Apply limits before loading parsers, OCR libraries or native model workers.
|
|
# An unavailable Windows boundary fails startup rather than silently weakening it.
|
|
CONTAINMENT=contain_inspector() if __name__=="__main__" else {"active":False,"kind":"not-service-entrypoint"}
|
|
# Broker imports no parser, OCR or model library. All untrusted document work
|
|
# runs under a capability-free Windows AppContainer, with no network permission.
|
|
MAX_BYTES = 20 * 1024 * 1024
|
|
SANDBOX = None
|
|
SANDBOX_ENV = {}
|
|
if __name__ == "__main__":
|
|
if sys.platform != 'win32': raise SystemExit('Production parser isolation requires Windows')
|
|
from pathlib import Path
|
|
from windows_appcontainer import AppContainer
|
|
source = Path(__file__).resolve().parent
|
|
python_root = Path(sys.executable).resolve().parent
|
|
# Development and packaged roots are derived from our own installation;
|
|
# externally configured model paths must already be copied into that tree.
|
|
installation = source.parent.parent if source.name == 'inspector' and source.parent.name == 'apps' else source.parent
|
|
semantic_root = installation / ('.runtime/windows/semantic' if source.parent.name == 'apps' else 'semantic')
|
|
expected_python = installation / ('.runtime/windows/python' if source.parent.name == 'apps' else 'python')
|
|
if python_root != expected_python.resolve():
|
|
raise SystemExit('Parser runtime must reside in the private installed runtime')
|
|
roots = [source, python_root]
|
|
if semantic_root.is_dir(): roots.append(semantic_root)
|
|
for key in ['EMBEDDING_MODEL_DIR', 'EMBEDDING_WORKER_PATH']:
|
|
value = os.environ.get(key)
|
|
if value:
|
|
resolved = Path(value).resolve()
|
|
if not resolved.is_relative_to(semantic_root.resolve()):
|
|
raise SystemExit('Model artifacts must reside in the private installed runtime')
|
|
SANDBOX_ENV[key] = str(resolved)
|
|
SANDBOX_ENV.update({'SEARCHI_FORCE_CPU':'1', 'GGML_CPU_ONLY':'1', 'GGML_DISABLE_VULKAN':'1'})
|
|
SANDBOX = AppContainer(roots)
|
|
CONTAINMENT['parserIsolation'] = {'active':True, 'kind':'windows-appcontainer-v1', 'networkCapabilities':0, 'privatePipes':True, 'readOnlyRuntime':True}
|
|
|
|
|
|
def dispatch(route, request):
|
|
if SANDBOX is None: raise RuntimeError('Isolated parser unavailable')
|
|
payload = json.dumps({'route':route, 'data':request['data'], 'policy':request.get('policy',{})}, allow_nan=False).encode('utf-8')
|
|
code, output = SANDBOX.run(sys.executable, [str(Path(__file__).with_name('sandbox_worker.py'))],
|
|
payload, environment=SANDBOX_ENV, timeout=29, max_output=8*1024*1024)
|
|
if code: raise RuntimeError('Isolated parser failed')
|
|
result = json.loads(output)
|
|
if not isinstance(result, dict): raise RuntimeError('Isolated parser response invalid')
|
|
if route in ['/inspect','/scan-download']:
|
|
if result.get('decision') not in ['allow','block'] or not isinstance(result.get('coverage',{}).get('complete'),bool):
|
|
raise RuntimeError('Isolated parser decision invalid')
|
|
if result['decision']=='allow' and not result['coverage']['complete']:
|
|
raise RuntimeError('Incomplete parser coverage cannot release content')
|
|
from stage_timings import enabled, validated
|
|
diagnostic = result.pop('_diagnosticStages', None)
|
|
if enabled(request.get('policy', {})):
|
|
diagnostic = validated(diagnostic)
|
|
if diagnostic is not None: result['_diagnosticStages'] = diagnostic
|
|
return result
|
|
|
|
TOKEN=os.environ.get("INSPECTOR_TOKEN", "")
|
|
if len(TOKEN)<32: raise SystemExit("INSPECTOR_TOKEN must contain at least 32 characters")
|
|
LOCK=threading.Lock()
|
|
|
|
def watch_parent():
|
|
parent=os.environ.get('INSPECTOR_PARENT_PID')
|
|
if not parent or sys.platform!='win32': return
|
|
import ctypes
|
|
kernel=ctypes.WinDLL('kernel32',use_last_error=True)
|
|
kernel.OpenProcess.argtypes=[ctypes.c_uint32,ctypes.c_int,ctypes.c_uint32]
|
|
kernel.OpenProcess.restype=ctypes.c_void_p
|
|
kernel.WaitForSingleObject.argtypes=[ctypes.c_void_p,ctypes.c_uint32]
|
|
handle=kernel.OpenProcess(0x00100000,False,int(parent))
|
|
if not handle: os._exit(0)
|
|
while kernel.WaitForSingleObject(handle,1000)==258: pass
|
|
os._exit(0)
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
def log_message(self, *_args): pass
|
|
def do_POST(self):
|
|
if not hmac.compare_digest(self.headers.get("Authorization", ""), "Bearer "+TOKEN): return self.reply(401,{"error":"Inspector authentication required"})
|
|
request=None;data=None
|
|
try:
|
|
length=int(self.headers.get("Content-Length", "0"))
|
|
if length<=0 or length>MAX_BYTES*2: return self.reply(413,{"error":"Inspection request too large"})
|
|
self.connection.settimeout(30)
|
|
request=json.loads(self.rfile.read(length))
|
|
data=base64.b64decode(request["data"],validate=True)
|
|
if len(data)>MAX_BYTES: return self.reply(413,{"error":"Content too large"})
|
|
with LOCK:
|
|
if self.path not in ['/inspect','/scan-download','/reference-document','/embed']:
|
|
return self.reply(404,{"error":"Not found"})
|
|
result=dispatch(self.path,request)
|
|
return self.reply(200,result)
|
|
except Exception: return self.reply(422,{"error":"Content inspection failed"})
|
|
finally:
|
|
if isinstance(request,dict): request.clear()
|
|
data=None
|
|
def reply(self,status,data):
|
|
body=json.dumps(data,allow_nan=False).encode();self.send_response(status);self.send_header("Content-Type","application/json");self.send_header("Content-Length",str(len(body)));self.send_header("Cache-Control","no-store");self.end_headers();self.wfile.write(body)
|
|
|
|
if __name__=="__main__":
|
|
threading.Thread(target=watch_parent,daemon=True).start()
|
|
server=HTTPServer(("127.0.0.1",int(os.environ.get("INSPECTOR_PORT","4311"))),Handler)
|
|
server.timeout=30
|
|
print(json.dumps({"port":server.server_address[1],"containment":CONTAINMENT}),flush=True)
|
|
server.serve_forever()
|