From 63a8409c31984c5be2ee986e76b66ca09f1ed901 Mon Sep 17 00:00:00 2001 From: Aleksei Fedorov Date: Tue, 16 Jun 2026 19:59:00 +0000 Subject: [PATCH] service communication channels + filesystem --- index.html | 6 +- kernel_ai/services/process_inspect.py | 108 +++++++++++ static/js/main.js | 246 ++++++++++++++++++++++++++ 3 files changed, 357 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index 1459a69..3aedc4f 100755 --- a/index.html +++ b/index.html @@ -84,13 +84,13 @@

Linux Kernel Ring 0 Visualization

- + - + @@ -108,6 +108,6 @@

Linux Kernel Ring 0 Visualization

console.error('❌ RightSemicircleMenuManager class NOT loaded!'); } - + diff --git a/kernel_ai/services/process_inspect.py b/kernel_ai/services/process_inspect.py index f0a8e15..481bcaf 100644 --- a/kernel_ai/services/process_inspect.py +++ b/kernel_ai/services/process_inspect.py @@ -4,6 +4,7 @@ import os import re +import time import psutil @@ -363,6 +364,112 @@ def get_process_cpu_info(pid): return {"error": str(e)} +_NAMESPACE_TYPES = [ + ("mnt", "MNT", "mount points / filesystem view"), + ("pid", "PID", "process id tree"), + ("net", "NET", "network stack / interfaces / ports"), + ("ipc", "IPC", "SysV IPC / POSIX message queues"), + ("uts", "UTS", "hostname / domain name"), + ("user", "USER", "uid/gid mapping & capabilities"), +] +_HOST_NS_CACHE = {"ts": 0.0, "inodes": {}} + + +def _host_namespace_inodes(): + """Namespace inodes of PID 1 act as the host/init reference set.""" + now = time.time() + if _HOST_NS_CACHE["inodes"] and now - _HOST_NS_CACHE["ts"] < 30: + return _HOST_NS_CACHE["inodes"] + inodes = {} + for ns_name, _label, _desc in _NAMESPACE_TYPES: + inodes[ns_name] = _system_view_service.read_namespace_inode(1, ns_name) + _HOST_NS_CACHE["inodes"] = inodes + _HOST_NS_CACHE["ts"] = now + return inodes + + +def _namespace_peer_pids(self_pid, isolated_inodes, limit=240): + """PIDs that share every isolated namespace inode with ``self_pid``. + + These are the process's "cell mates" — the rest of its container/sandbox. + Only the isolated namespaces are compared (and we early-out on first + mismatch), so a single-namespace sandbox costs one readlink per process. + """ + peers = [] + if not isolated_inodes: + return peers + ns_items = list(isolated_inodes.items()) + for proc in psutil.process_iter(["pid"]): + try: + pid = proc.info["pid"] + if pid == self_pid: + continue + for ns_name, inode in ns_items: + if _system_view_service.read_namespace_inode(pid, ns_name) != inode: + break + else: + peers.append(pid) + if len(peers) >= limit: + break + except (psutil.NoSuchProcess, psutil.AccessDenied, KeyError): + continue + return peers + + +def get_process_namespace_fingerprint(pid): + """Per-process namespace isolation profile relative to the host (PID 1). + + For each namespace type, reports whether the process lives in its own + namespace (isolated) or shares the host's. When the process is isolated in + at least one namespace, also resolves its co-resident peers (container + mates). Used by the dossier fingerprint + containment halo. + """ + host = _host_namespace_inodes() + namespaces = [] + isolated_count = 0 + readable = 0 + isolated_inodes = {} + for ns_name, label, desc in _NAMESPACE_TYPES: + inode = _system_view_service.read_namespace_inode(pid, ns_name) + host_inode = host.get(ns_name) + if inode: + readable += 1 + isolated = bool(inode and host_inode and inode != host_inode) + if isolated: + isolated_count += 1 + isolated_inodes[ns_name] = inode + namespaces.append( + { + "id": ns_name, + "label": label, + "description": desc, + "inode": inode, + "host_inode": host_inode, + "isolated": isolated, + } + ) + total = len(_NAMESPACE_TYPES) + if isolated_count >= total: + verdict = "fully isolated" + elif isolated_count == 0: + verdict = "host process" + else: + verdict = "partially sandboxed" + + peer_pids = _namespace_peer_pids(pid, isolated_inodes) + return { + "namespaces": namespaces, + "isolated_count": isolated_count, + "total": total, + "readable": readable, + "containerized": isolated_count > 0, + "verdict": verdict, + "peer_pids": peer_pids, + "peer_count": len(peer_pids), + "container_size": len(peer_pids) + 1 if isolated_count > 0 else 0, + } + + def get_process_fds_info(pid): try: proc = psutil.Process(pid) @@ -473,6 +580,7 @@ def get_process_fds_info(pid): "open_files": open_files[:20], "connections": connections[:20], "descriptors": descriptors[:40], + "namespace_fingerprint": get_process_namespace_fingerprint(pid), } except (psutil.NoSuchProcess, psutil.AccessDenied) as e: return {"error": f"Access denied or process not found: {str(e)}"} diff --git a/static/js/main.js b/static/js/main.js index 6040caa..67450e7 100755 --- a/static/js/main.js +++ b/static/js/main.js @@ -975,6 +975,22 @@ function renderProcessDossier() { height: 142, anchor: { x: menuX + menuW - 42, y: menuY + 12 } }); + + renderNamespaceFingerprint(layer, processData, pinnedProcessDossier.details, { + x: menuX, + y: Math.max(88, menuY - 150), + width: 226, + height: 126, + anchor: { x: menuX + 28, y: menuY + 12 } + }); + + const nsFingerprint = pinnedProcessDossier.details?.fdsData?.namespace_fingerprint; + const containmentPeers = nsFingerprint && Array.isArray(nsFingerprint.peer_pids) + ? nsFingerprint.peer_pids + : []; + if (containmentPeers.length) { + drawContainmentHalo(layer, processData, containmentPeers); + } } function renderFdDescriptorMap(layer, processData, details = {}, box) { @@ -1090,6 +1106,236 @@ function renderFdDescriptorMap(layer, processData, details = {}, box) { } } +const NS_FINGERPRINT_PLACEHOLDER = [ + { id: 'mnt', label: 'MNT', isolated: false }, + { id: 'pid', label: 'PID', isolated: false }, + { id: 'net', label: 'NET', isolated: false }, + { id: 'ipc', label: 'IPC', isolated: false }, + { id: 'uts', label: 'UTS', isolated: false }, + { id: 'user', label: 'USER', isolated: false } +]; + +function renderNamespaceFingerprint(layer, processData, details = {}, box) { + const fdsData = details.fdsData || {}; + const fp = fdsData.namespace_fingerprint || null; + const nsList = fp && Array.isArray(fp.namespaces) ? fp.namespaces : NS_FINGERPRINT_PLACEHOLDER; + const isolatedCount = fp ? Number(fp.isolated_count || 0) : 0; + const total = fp ? Number(fp.total || nsList.length) : nsList.length; + const verdict = fp ? String(fp.verdict || '') : 'reading…'; + + layer.append('path') + .attr('d', `M${box.anchor.x},${box.anchor.y} C${box.anchor.x + 18},${box.anchor.y - 42} ${box.x + 24},${box.y + box.height + 18} ${box.x + 32},${box.y + box.height - 4}`) + .attr('fill', 'none') + .attr('stroke', 'rgba(20, 20, 20, 0.22)') + .attr('stroke-width', 0.8); + + layer.append('rect') + .attr('x', box.x) + .attr('y', box.y) + .attr('width', box.width) + .attr('height', box.height) + .attr('rx', 6) + .attr('fill', 'rgba(238, 238, 228, 0.76)') + .attr('stroke', 'rgba(24, 24, 24, 0.18)') + .attr('stroke-width', 0.85); + + layer.append('rect') + .attr('x', box.x + 8) + .attr('y', box.y + 8) + .attr('width', box.width - 16) + .attr('height', 22) + .attr('rx', 3) + .attr('fill', 'rgba(24, 24, 24, 0.84)'); + + layer.append('text') + .attr('x', box.x + 16) + .attr('y', box.y + 23) + .attr('font-family', 'Share Tech Mono, monospace') + .attr('font-size', '8.5px') + .attr('font-weight', '700') + .attr('fill', '#f2f2ea') + .text('NAMESPACE ISOLATION'); + + layer.append('text') + .attr('x', box.x + box.width - 16) + .attr('y', box.y + 23) + .attr('text-anchor', 'end') + .attr('font-family', 'Share Tech Mono, monospace') + .attr('font-size', '7px') + .attr('font-weight', '700') + .attr('fill', isolatedCount > 0 ? '#9fe0c2' : '#cfcfc8') + .text(`${isolatedCount}/${total}`); + + layer.append('text') + .attr('x', box.x + 16) + .attr('y', box.y + 42) + .attr('font-family', 'Share Tech Mono, monospace') + .attr('font-size', '6.8px') + .attr('fill', isolatedCount > 0 ? 'rgba(20, 90, 64, 0.9)' : 'rgba(36, 36, 36, 0.6)') + .text(verdict.toUpperCase()); + + const cols = 3; + const gap = 7; + const cellW = (box.width - 32 - gap * (cols - 1)) / cols; + const cellH = 24; + const gridY = box.y + 52; + + nsList.slice(0, 6).forEach((ns, i) => { + const c = i % cols; + const r = Math.floor(i / cols); + const x = box.x + 16 + c * (cellW + gap); + const y = gridY + r * (cellH + 7); + const isolated = !!ns.isolated; + + layer.append('rect') + .attr('x', x) + .attr('y', y) + .attr('width', cellW) + .attr('height', cellH) + .attr('rx', 4) + .attr('fill', isolated ? 'rgba(24, 24, 24, 0.86)' : 'rgba(255, 255, 255, 0.5)') + .attr('stroke', isolated ? 'rgba(20, 90, 64, 0.55)' : 'rgba(24, 24, 24, 0.16)') + .attr('stroke-width', isolated ? 1 : 0.7) + .style('pointer-events', fp ? 'all' : 'none') + .style('cursor', fp ? 'help' : 'default') + .on('mouseenter', (event) => showNamespaceCellTooltip(event, ns)) + .on('mousemove', (event) => { + d3.selectAll('.ns-fp-tooltip') + .style('left', `${event.pageX + 12}px`) + .style('top', `${event.pageY - 10}px`); + }) + .on('mouseleave', () => d3.selectAll('.ns-fp-tooltip').remove()); + + // Filled node = own (isolated) namespace; hollow ring = shares host's. + layer.append('circle') + .attr('cx', x + 9) + .attr('cy', y + cellH / 2) + .attr('r', 3) + .attr('fill', isolated ? '#7fd6b0' : 'none') + .attr('stroke', isolated ? 'none' : 'rgba(24, 24, 24, 0.42)') + .attr('stroke-width', 0.9); + + layer.append('text') + .attr('x', x + 18) + .attr('y', y + cellH / 2 + 2.5) + .attr('font-family', 'Share Tech Mono, monospace') + .attr('font-size', '7.5px') + .attr('font-weight', '700') + .attr('fill', isolated ? '#f2f2ea' : '#2f2f2f') + .text(ns.label || String(ns.id || '').toUpperCase()); + + layer.append('text') + .attr('x', x + cellW - 5) + .attr('y', y + cellH / 2 + 2.5) + .attr('text-anchor', 'end') + .attr('font-family', 'Share Tech Mono, monospace') + .attr('font-size', '5.5px') + .attr('fill', isolated ? '#9fe0c2' : 'rgba(36, 36, 36, 0.45)') + .text(isolated ? 'OWN' : 'host'); + }); + + const peerCount = fp ? Number(fp.peer_count || 0) : 0; + const footer = fp + ? (isolatedCount > 0 + ? `CO-RESIDENT: ${peerCount} proc${peerCount === 1 ? '' : 's'}` + : 'CO-RESIDENT: host namespace (shared)') + : 'hover a cell for inode'; + layer.append('text') + .attr('x', box.x + 16) + .attr('y', box.y + box.height - 9) + .attr('font-family', 'Share Tech Mono, monospace') + .attr('font-size', '6.3px') + .attr('fill', peerCount > 0 ? 'rgba(20, 90, 64, 0.85)' : 'rgba(36, 36, 36, 0.5)') + .text(footer); +} + +function showNamespaceCellTooltip(event, ns) { + d3.selectAll('.ns-fp-tooltip').remove(); + const isolated = !!ns.isolated; + const inode = ns.inode || 'n/a'; + const hostInode = ns.host_inode || 'n/a'; + const statusLine = isolated + ? 'OWN namespace — isolated from host' + : 'shares the host namespace'; + d3.select('body') + .append('div') + .attr('class', 'tooltip ns-fp-tooltip') + .style('position', 'absolute') + .style('background', 'rgba(0, 0, 0, 0.88)') + .style('color', '#fff') + .style('padding', '8px 10px') + .style('border-radius', '4px') + .style('font-size', '11px') + .style('font-family', 'Share Tech Mono, monospace') + .style('pointer-events', 'none') + .style('z-index', '1300') + .style('left', `${event.pageX + 12}px`) + .style('top', `${event.pageY - 10}px`) + .html( + `${ns.label || String(ns.id || '').toUpperCase()} NAMESPACE
` + + `${ns.description || ''}
` + + `
` + + `${statusLine}
` + + `inode: ${inode}
` + + `host inode: ${hostInode}` + ); +} + +// Containment halo: ring the selected process and its container/sandbox mates +// (processes sharing every isolated namespace inode) on the process map. +function drawContainmentHalo(layer, processData, peerPids) { + const selfPid = processData.pid; + const drawn = [{ pid: selfPid, self: true }] + .concat(peerPids.slice(0, 120).map((pid) => ({ pid, self: false }))); + let visibleMates = 0; + + drawn.forEach(({ pid, self }) => { + const group = svg.select(`.process-node-group[data-pid="${pid}"]`); + if (group.empty()) return; + const circle = group.select('circle.process-node'); + if (circle.empty()) return; + const cx = Number(circle.attr('cx')); + const cy = Number(circle.attr('cy')); + if (!Number.isFinite(cx) || !Number.isFinite(cy)) return; + if (!self) visibleMates += 1; + + const halo = layer.append('circle') + .attr('cx', cx) + .attr('cy', cy) + .attr('r', self ? 9 : 6.5) + .attr('fill', 'none') + .attr('stroke', self ? 'rgba(36, 150, 104, 0.95)' : 'rgba(72, 174, 124, 0.7)') + .attr('stroke-width', self ? 1.8 : 1.05) + .attr('stroke-dasharray', self ? 'none' : '3 3') + .style('pointer-events', 'none'); + + if (self) { + const pulse = () => { + halo.attr('r', 9).attr('opacity', 0.95) + .transition().duration(1700).ease(d3.easeSinOut) + .attr('r', 15).attr('opacity', 0) + .on('end', function () { pulse(); }); + }; + pulse(); + } + }); + + if (visibleMates > 0) { + const selfGroup = svg.select(`.process-node-group[data-pid="${selfPid}"]`); + const selfCircle = selfGroup.empty() ? null : selfGroup.select('circle.process-node'); + if (selfCircle && !selfCircle.empty()) { + layer.append('text') + .attr('x', Number(selfCircle.attr('cx')) + 12) + .attr('y', Number(selfCircle.attr('cy')) - 10) + .attr('font-family', 'Share Tech Mono, monospace') + .attr('font-size', '7px') + .attr('font-weight', '700') + .attr('fill', 'rgba(28, 120, 84, 0.92)') + .text(`container · ${visibleMates} on map`); + } + } +} + function clearPinnedProcessDossier() { if (!pinnedProcessDossier) return; const pinnedPid = pinnedProcessDossier.process?.pid;