mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-22 06:09:35 +08:00
Imports the CodeAtlasWeb companion frontend verbatim from BernhardJackiewicz/CodeAtlasWeb at commit 1825f9c850ac794066e02606012508b9d393c6aa as the new graph-ui and removes the previous React frontend. This is Bernhard Jackiewicz's work, contributed with his agreement and squashed here from his development history (41 commits, cycles W0 through W15). The maintainer only relocates it: every product decision, line of code and test below graph-ui/ is his. The seven galaxy files that originate in this repository's previous graph-ui, and the pinned 3D libraries, are listed in graph-ui/THIRD_PARTY.md. What it is: a local reading IDE on the CBM read surface. A read-only Monaco source reader, a semantic twin that follows the caret, entry, bug-hunt, change-scope and flow reading modes, deterministic source-cited explanations with an optional local-model wording pass (llama-server sidecar, off unless the reader starts one; no model or binary ships), a persistent galaxy with focus following in both directions, a live-agent overlay fed by a loopback bridge, explicit air-gap, provenance and index-limit messaging, and an offline acceptance and browser verification harness. It consumes POST /rpc (the read-only tool allowlist), GET /api/layout, and the atlas routes /api/tree, /api/trace, /api/flows and /api/flow, plus the real ingest_traces store for observed calls; all of these are on feat/atlas-r1 (#1860), which this change stacks on. Not imported: CLAUDE.md (an agent-instruction file, which stays local-only in this repository), and the recorded proof artifacts under graph-ui/verification/, which follow in a separate commit so they can be reviewed and dropped on their own. Build wiring, the dev-proxy port contract, the CSP allowance for the sidecar and bridge ports, and the control surfaces the previous frontend had (project indexing, ADR editor, logs) follow in separate maintainer commits. Design and tracking: #1964. Co-authored-by: Bernhard Jackiewicz <bj@techport.io> Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
164 lines
5.5 KiB
JavaScript
164 lines
5.5 KiB
JavaScript
/**
|
|
* Ein Stichprobenzaehler ueber den eigenen Prozessbaum: was hat gerade eine
|
|
* Gegenstelle offen, und liegt die auf Loopback?
|
|
*
|
|
* ## Warum ein Beweislauf das selbst misst, obwohl das Gate es auch tut
|
|
*
|
|
* tools/net-deny-gate.mjs beobachtet das Kommando VON AUSSEN und schreibt sein
|
|
* Ergebnis erst, wenn das Kommando fertig ist. Der Lauf selbst kann diese Zahl
|
|
* also nicht in sein eigenes Artefakt schreiben; er wuerde eine Datei lesen, die
|
|
* es zu seiner Laufzeit noch nicht gibt. Der Abnahmetest verlangt die Zahl aber
|
|
* in airgap.json, und eine abgeschriebene Zahl waere keine zweite Beobachtung,
|
|
* sondern dieselbe an einem zweiten Ort.
|
|
*
|
|
* Also misst der Lauf selbst, mit denselben Regeln: derselbe `classifyLine` und
|
|
* derselbe `isLoopbackHost` aus dem Gate, importiert und nicht nachgebaut. Nur
|
|
* die zwanzig Zeilen um `pgrep` und `lsof` herum stehen hier noch einmal. Das
|
|
* Gate umzubauen, damit es diese zwanzig Zeilen hergibt, waere eine Aenderung an
|
|
* dem Werkzeug, das jeden bisherigen Beweis dieses Projekts getragen hat, und
|
|
* zwar aus Bequemlichkeit eines neuen Laufs. Der Preis dafuer ist hoeher als
|
|
* zwanzig Zeilen.
|
|
*
|
|
* Die Grenze der Aussage ist dieselbe wie beim Gate und wird genauso genannt:
|
|
* eine Verbindung, die zwischen zwei Abtastungen aufgebaut, benutzt und
|
|
* geschlossen wird, kann durchrutschen. Deshalb steht die Zahl der Stichproben
|
|
* im Ergebnis. Ohne sie waere eine Null nicht lesbar.
|
|
*/
|
|
|
|
import { execFile } from 'node:child_process';
|
|
import { promisify } from 'node:util';
|
|
|
|
import { classifyLine } from '../net-deny-gate.mjs';
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
|
|
async function childPids(pid) {
|
|
try {
|
|
const { stdout } = await execFileAsync('pgrep', ['-P', String(pid)]);
|
|
return stdout.split('\n').map((line) => line.trim()).filter(Boolean).map(Number);
|
|
} catch {
|
|
// pgrep endet mit 1, wenn es keine Kinder gibt. Das ist der Normalfall.
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/** Der Prozessbaum unter (und einschliesslich) pid. */
|
|
async function processTree(pid) {
|
|
const seen = new Set();
|
|
const queue = [pid];
|
|
while (queue.length > 0) {
|
|
const current = queue.shift();
|
|
if (seen.has(current)) {
|
|
continue;
|
|
}
|
|
seen.add(current);
|
|
for (const child of await childPids(current)) {
|
|
if (!seen.has(child)) {
|
|
queue.push(child);
|
|
}
|
|
}
|
|
}
|
|
return [...seen];
|
|
}
|
|
|
|
async function sockets(pids) {
|
|
if (pids.length === 0) {
|
|
return { lines: [], error: null };
|
|
}
|
|
try {
|
|
const { stdout } = await execFileAsync(
|
|
'lsof',
|
|
['-a', '-i', '-n', '-P', '-p', pids.join(',')],
|
|
{ maxBuffer: 8 * 1024 * 1024 },
|
|
);
|
|
return { lines: stdout.split('\n').filter((line) => line.trim().length > 0), error: null };
|
|
} catch (err) {
|
|
// Exit 1 heisst bei lsof "nichts gefunden", nicht "kaputt".
|
|
if (err && err.code === 1) {
|
|
return { lines: [], error: null };
|
|
}
|
|
return { lines: [], error: err && err.message ? err.message : String(err) };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Den eigenen Prozessbaum abtasten, bis `stop()` gerufen wird.
|
|
*
|
|
* @param {{intervalMs?: number, pid?: number}} options
|
|
*/
|
|
export function startSocketSampler(options = {}) {
|
|
const intervalMs = options.intervalMs ?? 600;
|
|
const rootPid = options.pid ?? process.pid;
|
|
const state = {
|
|
samples: 0,
|
|
lsofErrors: 0,
|
|
maxProcessTreeSize: 0,
|
|
violations: [],
|
|
loopback: new Map(),
|
|
};
|
|
let running = true;
|
|
const started = Date.now();
|
|
|
|
const once = async () => {
|
|
const pids = await processTree(rootPid);
|
|
state.maxProcessTreeSize = Math.max(state.maxProcessTreeSize, pids.length);
|
|
const { lines, error } = await sockets(pids);
|
|
if (error !== null) {
|
|
state.lsofErrors += 1;
|
|
}
|
|
state.samples += 1;
|
|
for (const line of lines) {
|
|
const socket = classifyLine(line);
|
|
if (socket === null) {
|
|
continue;
|
|
}
|
|
if (socket.loopback) {
|
|
const key = `${socket.protocol} ${socket.remote}`;
|
|
state.loopback.set(key, (state.loopback.get(key) ?? 0) + 1);
|
|
continue;
|
|
}
|
|
state.violations.push({
|
|
atMs: Date.now() - started,
|
|
protocol: socket.protocol,
|
|
command: socket.command,
|
|
pid: socket.pid,
|
|
local: socket.local,
|
|
remote: socket.remote,
|
|
});
|
|
}
|
|
};
|
|
|
|
const loop = (async () => {
|
|
while (running) {
|
|
await once();
|
|
if (!running) {
|
|
break;
|
|
}
|
|
await new Promise((wake) => setTimeout(wake, intervalMs));
|
|
}
|
|
})();
|
|
|
|
return {
|
|
async stop() {
|
|
running = false;
|
|
await loop;
|
|
await once();
|
|
return {
|
|
samples: state.samples,
|
|
intervalMs,
|
|
durationMs: Date.now() - started,
|
|
maxProcessTreeSize: state.maxProcessTreeSize,
|
|
lsofErrors: state.lsofErrors,
|
|
outboundViolations: state.violations.length,
|
|
violations: state.violations,
|
|
loopbackConnections: [...state.loopback.entries()]
|
|
.map(([endpoint, seen]) => ({ endpoint, seen }))
|
|
.sort((a, b) => a.endpoint.localeCompare(b.endpoint)),
|
|
};
|
|
},
|
|
get samples() {
|
|
return state.samples;
|
|
},
|
|
};
|
|
}
|