over / src /host-model.ts
luguog's picture
Upload folder using huggingface_hub
045d917 verified
Raw
History Blame Contribute Delete
23.4 kB
import crypto from "node:crypto";
import { execSync } from "node:child_process";
import { createHash } from "node:crypto";
import { readFileSync, statSync, readdirSync, existsSync } from "node:fs";
import { join, relative, extname, basename } from "node:path";
import { hostname, platform, arch, cpus, totalmem, freemem } from "node:os";
export interface MachineGraphNode {
id: string;
type: "file" | "process" | "port" | "project" | "artifact" | "service";
properties: Record<string, unknown>;
}
export interface MachineGraph {
hostId: string;
timestamp: string;
nodes: MachineGraphNode[];
}
export interface CorpusEntry {
resource_id: string;
path: string;
name: string;
extension: string;
hash: string;
size: number;
modified: string;
project?: string;
semanticTags: string[];
sensitivity: number;
}
export interface OperatingState {
hostname: string;
platform: string;
arch: string;
cpuCount: number;
totalMemoryMB: number;
freeMemoryMB: number;
processes: ProcessInfo[];
listeningPorts: PortInfo[];
uptime: number;
}
export interface ProcessInfo {
pid: number;
name: string;
command: string;
listeningPort?: number;
}
export interface PortInfo {
port: number;
protocol: string;
address: string;
state: string;
}
export interface PolicyEntry {
principal: string;
action: string;
resource: string;
conditions: Record<string, unknown>;
decision: "permit" | "forbid";
}
export interface Receipt {
id: string;
timestamp: string;
principal: string;
action: string;
resource: string;
resourceId: string;
decision: "permit" | "deny";
result: "success" | "failure" | "denied";
contentHash?: string;
disclosureTier?: number;
details: string;
signature: string;
}
export interface DisclosureLayer {
tier: number;
name: string;
description: string;
keyId: string;
encryptedContent: string;
iv: string;
keyLifetimeMs: number;
expiresAt: string;
revoked: boolean;
}
export interface Mailbox {
id: string;
hostId: string;
recipientPublicKey: string;
resourceId: string;
contentHash: string;
operations: string[];
issuedAt: string;
expiresAt: string;
decayPolicy: string;
layers: DisclosureLayer[];
claimed: boolean;
claimChallenge?: string;
attendanceLog: AttendanceRecord[];
}
export interface AttendanceRecord {
timestamp: string;
operation: string;
tier: number;
result: "success" | "denied" | "expired";
receiptId: string;
}
export interface HostModel {
hostId: string;
hostname: string;
graph: MachineGraph;
corpus: CorpusEntry[];
state: OperatingState;
policies: PolicyEntry[];
receipts: Receipt[];
mailboxes: Map<string, Mailbox>;
keyPair: { publicKey: string; privateKey: string };
}
const SENSITIVITY_BY_EXT: Record<string, number> = {
".pem": 5, ".key": 5, ".env": 4, ".secret": 5,
".p12": 5, ".pfx": 5, ".keystore": 5,
".ts": 1, ".js": 1, ".py": 1, ".rs": 1,
".md": 1, ".json": 2, ".yaml": 2, ".yml": 2,
".sql": 3, ".db": 3, ".sqlite": 3,
};
function hashContent(content: Buffer | string): string {
return createHash("sha256").update(content).digest("hex");
}
function generateHostId(): string {
const raw = `${hostname()}-${platform()}-${arch()}-${cpus().length}`;
return `host_${hashContent(raw).substring(0, 12)}`;
}
function generateKeyPair(): { publicKey: string; privateKey: string } {
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
return {
publicKey: publicKey.export({ type: "spki", format: "pem" }).toString(),
privateKey: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
};
}
export function signData(privateKey: string, data: string): string {
const key = crypto.createPrivateKey(privateKey);
return crypto.sign(null, Buffer.from(data), key).toString("base64url");
}
export function verifySignature(publicKey: string, data: string, signature: string): boolean {
try {
const key = crypto.createPublicKey(publicKey);
return crypto.verify(null, Buffer.from(data), key, Buffer.from(signature, "base64url"));
} catch {
return false;
}
}
function encryptLayer(plaintext: string, tierKey: Buffer): { content: string; iv: string } {
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv("aes-256-gcm", tierKey, iv);
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
const tag = cipher.getAuthTag();
return {
content: Buffer.concat([encrypted, tag]).toString("base64"),
iv: iv.toString("base64"),
};
}
export function decryptLayer(encryptedContent: string, iv: string, tierKey: Buffer): string {
const data = Buffer.from(encryptedContent, "base64");
const tag = data.subarray(data.length - 16);
const ciphertext = data.subarray(0, data.length - 16);
const decipher = crypto.createDecipheriv("aes-256-gcm", tierKey, Buffer.from(iv, "base64"));
decipher.setAuthTag(tag);
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
}
function deriveTierKey(masterKey: Buffer, tier: number): Buffer {
return crypto.createHash("sha256").update(masterKey).update(Buffer.from([tier])).digest();
}
function scanDirectory(dir: string, maxDepth: number, currentDepth: number = 0): CorpusEntry[] {
if (currentDepth >= maxDepth || !existsSync(dir)) return [];
const entries: CorpusEntry[] = [];
try {
const items = readdirSync(dir, { withFileTypes: true });
for (const item of items) {
if (item.name.startsWith(".") || item.name === "node_modules" || item.name === ".git") continue;
const fullPath = join(dir, item.name);
if (item.isDirectory()) {
entries.push(...scanDirectory(fullPath, maxDepth, currentDepth + 1));
} else if (item.isFile()) {
try {
const stat = statSync(fullPath);
const content = readFileSync(fullPath);
const hash = hashContent(content);
const ext = extname(item.name);
const sensitivity = SENSITIVITY_BY_EXT[ext] ?? 1;
entries.push({
resource_id: `filecap://${generateHostId()}/object_${hash.substring(0, 16)}/version_${hash.substring(0, 8)}`,
path: fullPath,
name: item.name,
extension: ext,
hash,
size: stat.size,
modified: stat.mtime.toISOString(),
semanticTags: deriveSemanticTags(item.name, ext, fullPath),
sensitivity,
});
} catch {
// Skip unreadable files
}
}
}
} catch {
// Skip inaccessible directories
}
return entries;
}
function deriveSemanticTags(name: string, ext: string, path: string): string[] {
const tags: string[] = [];
const lower = name.toLowerCase();
if (ext === ".ts" || ext === ".js") tags.push("code");
if (ext === ".md") tags.push("documentation");
if (ext === ".json") tags.push("config");
if (ext === ".py") tags.push("code", "python");
if (ext === ".rs") tags.push("code", "rust");
if (lower.includes("test")) tags.push("test");
if (lower.includes("spec")) tags.push("spec");
if (lower.includes("proof")) tags.push("proof");
if (lower.includes("key") || lower.includes("secret")) tags.push("sensitive");
if (path.includes("src/")) tags.push("source");
if (path.includes("docs/")) tags.push("docs");
if (path.includes("test")) tags.push("test");
return tags;
}
function getRunningProcesses(): ProcessInfo[] {
const processes: ProcessInfo[] = [];
try {
if (platform() === "darwin") {
const output = execSync("ps aux | head -50", { encoding: "utf8", timeout: 3000 });
const lines = output.split("\n").slice(1);
for (const line of lines) {
const parts = line.trim().split(/\s+/);
if (parts.length >= 11) {
const pid = parseInt(parts[1], 10);
const command = parts.slice(10).join(" ");
const name = parts[10] ?? "unknown";
if (!isNaN(pid) && pid > 0) {
processes.push({ pid, name, command });
}
}
}
} else {
const output = execSync("ps aux | head -50", { encoding: "utf8", timeout: 3000 });
const lines = output.split("\n").slice(1);
for (const line of lines) {
const parts = line.trim().split(/\s+/);
if (parts.length >= 11) {
const pid = parseInt(parts[1], 10);
const command = parts.slice(10).join(" ");
const name = parts[10] ?? "unknown";
if (!isNaN(pid) && pid > 0) {
processes.push({ pid, name, command });
}
}
}
}
} catch {
// ps failed — return empty
}
return processes.slice(0, 30);
}
function getListeningPorts(): PortInfo[] {
const ports: PortInfo[] = [];
try {
const output = execSync("lsof -i -P -n 2>/dev/null | grep LISTEN | head -20", {
encoding: "utf8",
timeout: 3000,
});
for (const line of output.split("\n")) {
const parts = line.trim().split(/\s+/);
if (parts.length >= 9) {
const addr = parts[8] ?? "";
const portMatch = addr.match(/:(\d+)$/);
if (portMatch) {
ports.push({
port: parseInt(portMatch[1], 10),
protocol: parts[7] ?? "tcp",
address: addr.split(":")[0] ?? "*",
state: "LISTEN",
});
}
}
}
} catch {
// lsof failed
}
return ports;
}
export function buildHostModel(scanRoots: string[]): HostModel {
const hostId = generateHostId();
const keyPair = generateKeyPair();
const corpus: CorpusEntry[] = [];
for (const root of scanRoots) {
corpus.push(...scanDirectory(root, 4));
}
const state: OperatingState = {
hostname: hostname(),
platform: platform(),
arch: arch(),
cpuCount: cpus().length,
totalMemoryMB: Math.round(totalmem() / 1024 / 1024),
freeMemoryMB: Math.round(freemem() / 1024 / 1024),
processes: getRunningProcesses(),
listeningPorts: getListeningPorts(),
uptime: process.uptime(),
};
const graphNodes: MachineGraphNode[] = [];
for (const entry of corpus) {
graphNodes.push({
id: entry.resource_id,
type: "file",
properties: {
name: entry.name,
hash: entry.hash,
extension: entry.extension,
size: entry.size,
modified: entry.modified,
semanticTags: entry.semanticTags,
sensitivity: entry.sensitivity,
project: entry.project,
},
});
}
for (const proc of state.processes) {
graphNodes.push({
id: `proc:${proc.pid}`,
type: "process",
properties: {
pid: proc.pid,
name: proc.name,
command: proc.command,
listeningPort: proc.listeningPort,
},
});
}
for (const port of state.listeningPorts) {
graphNodes.push({
id: `port:${port.port}`,
type: "port",
properties: {
port: port.port,
protocol: port.protocol,
address: port.address,
state: port.state,
},
});
}
const defaultPolicies: PolicyEntry[] = [
{
principal: "*",
action: "read",
resource: "file[sensitivity<=2]",
conditions: {},
decision: "permit",
},
{
principal: "*",
action: "read",
resource: "file[sensitivity>=4]",
conditions: {},
decision: "forbid",
},
{
principal: "*",
action: "search",
resource: "*",
conditions: {},
decision: "permit",
},
{
principal: "*",
action: "describe",
resource: "*",
conditions: {},
decision: "permit",
},
{
principal: "*",
action: "verify",
resource: "*",
conditions: {},
decision: "permit",
},
{
principal: "*",
action: "write",
resource: "*",
conditions: {},
decision: "forbid",
},
];
return {
hostId,
hostname: hostname(),
graph: {
hostId,
timestamp: new Date().toISOString(),
nodes: graphNodes,
},
corpus,
state,
policies: defaultPolicies,
receipts: [],
mailboxes: new Map(),
keyPair,
};
}
export function createMailbox(
model: HostModel,
recipientPublicKey: string,
resourcePath: string,
operations: string[],
decayPolicy: string = "full→extract→summary→receipt"
): Mailbox {
const hostId = model.hostId;
const mailboxId = `mbx_${crypto.randomUUID().substring(0, 12)}`;
let content: string;
let contentHash: string;
let resourceId: string;
try {
const raw = readFileSync(resourcePath);
content = raw.toString("utf8");
contentHash = hashContent(raw);
resourceId = `filecap://${hostId}/object_${contentHash.substring(0, 16)}/version_${contentHash.substring(0, 8)}`;
} catch {
content = "";
contentHash = hashContent(resourcePath);
resourceId = `filecap://${hostId}/object_${contentHash.substring(0, 16)}`;
}
const masterKey = crypto.randomBytes(32);
const now = Date.now();
const tiers = [
{ tier: 0, name: "full", desc: "Full file content", lifetimeMs: 15 * 60 * 1000 },
{ tier: 1, name: "extract", desc: "Selected exact passages", lifetimeMs: 60 * 60 * 1000 },
{ tier: 2, name: "structured", desc: "Structured extraction", lifetimeMs: 6 * 60 * 60 * 1000 },
{ tier: 3, name: "summary", desc: "Semantic summary and embeddings", lifetimeMs: 7 * 24 * 60 * 60 * 1000 },
{ tier: 4, name: "receipt", desc: "Hash, provenance and access receipt", lifetimeMs: Infinity },
];
const layers: DisclosureLayer[] = tiers.map((t) => {
const tierKey = deriveTierKey(masterKey, t.tier);
let plaintext: string;
switch (t.tier) {
case 0:
plaintext = content;
break;
case 1:
plaintext = content.split("\n").slice(0, 50).join("\n");
break;
case 2:
plaintext = JSON.stringify({
name: basename(resourcePath),
size: content.length,
hash: contentHash,
firstLines: content.split("\n").slice(0, 10),
lineCount: content.split("\n").length,
}, null, 2);
break;
case 3:
plaintext = JSON.stringify({
name: basename(resourcePath),
hash: contentHash,
size: content.length,
summary: `File contains ${content.split("\n").length} lines, ${content.length} bytes`,
tags: deriveSemanticTags(basename(resourcePath), extname(resourcePath), resourcePath),
});
break;
case 4:
plaintext = JSON.stringify({
resourceId,
hash: contentHash,
hostId,
mailboxId,
provenance: "HostModel receipt layer",
});
break;
default:
plaintext = "";
}
const encrypted = encryptLayer(plaintext, tierKey);
const expiresAt = t.lifetimeMs === Infinity ? "permanent" : new Date(now + t.lifetimeMs).toISOString();
return {
tier: t.tier,
name: t.name,
description: t.desc,
keyId: `key_${mailboxId}_t${t.tier}`,
encryptedContent: encrypted.content,
iv: encrypted.iv,
keyLifetimeMs: t.lifetimeMs,
expiresAt,
revoked: false,
};
});
const claimChallenge = crypto.randomBytes(16).toString("base64url");
const mailbox: Mailbox = {
id: mailboxId,
hostId,
recipientPublicKey,
resourceId,
contentHash,
operations,
issuedAt: new Date(now).toISOString(),
expiresAt: new Date(now + 24 * 60 * 60 * 1000).toISOString(),
decayPolicy,
layers,
claimed: false,
claimChallenge,
attendanceLog: [],
};
model.mailboxes.set(mailboxId, mailbox);
return mailbox;
}
export function getCurrentDecayTier(mailbox: Mailbox): number {
const now = Date.now();
const issued = new Date(mailbox.issuedAt).getTime();
const tierLifetimes = [
15 * 60 * 1000,
60 * 60 * 1000,
6 * 60 * 60 * 1000,
7 * 24 * 60 * 60 * 1000,
Infinity,
];
const elapsed = now - issued;
for (let i = 0; i < tierLifetimes.length; i++) {
if (elapsed < tierLifetimes[i]) {
if (mailbox.layers[i] && !mailbox.layers[i].revoked) {
return i;
}
}
}
return 4;
}
export function revokeExpiredLayers(mailbox: Mailbox): void {
const now = Date.now();
const issued = new Date(mailbox.issuedAt).getTime();
const tierLifetimes = [
15 * 60 * 1000,
60 * 60 * 1000,
6 * 60 * 60 * 1000,
7 * 24 * 60 * 60 * 1000,
Infinity,
];
for (let i = 0; i < mailbox.layers.length; i++) {
const elapsed = now - issued;
if (elapsed >= tierLifetimes[i] && !mailbox.layers[i].revoked) {
mailbox.layers[i].revoked = true;
}
}
}
export function evaluatePolicy(
model: HostModel,
principal: string,
action: string,
resource: string,
context: Record<string, unknown> = {}
): { decision: "permit" | "deny"; matchedPolicy?: PolicyEntry } {
let permitMatch: PolicyEntry | undefined;
let forbidMatch: PolicyEntry | undefined;
for (const policy of model.policies) {
if (policy.principal !== "*" && policy.principal !== principal) continue;
if (policy.action !== "*" && policy.action !== action) continue;
if (policy.resource === "*" || matchesResource(policy.resource, resource)) {
if (policy.decision === "permit" && !permitMatch) {
permitMatch = policy;
}
if (policy.decision === "forbid" && !forbidMatch) {
forbidMatch = policy;
}
}
}
if (forbidMatch) {
return { decision: "deny", matchedPolicy: forbidMatch };
}
if (permitMatch) {
return { decision: "permit", matchedPolicy: permitMatch };
}
return { decision: "deny" };
}
function matchesResource(pattern: string, resource: string): boolean {
if (pattern === resource) return true;
if (pattern.includes("[") && pattern.includes("]")) {
const baseMatch = pattern.split("[")[0];
if (resource.startsWith(baseMatch)) {
const condStr = pattern.substring(pattern.indexOf("[") + 1, pattern.lastIndexOf("]"));
const conditions = condStr.split("][").map((c) => c.replace(/[\[\]]/g, ""));
for (const cond of conditions) {
if (cond.includes("<=")) {
const [key, val] = cond.split("<=");
const resourceVal = extractResourceProperty(resource, key);
if (resourceVal !== undefined && resourceVal > parseInt(val, 10)) return false;
} else if (cond.includes(">=")) {
const [key, val] = cond.split(">=");
const resourceVal = extractResourceProperty(resource, key);
if (resourceVal !== undefined && resourceVal < parseInt(val, 10)) return false;
} else if (cond.includes("=")) {
const [key, val] = cond.split("=");
const resourceVal = extractResourceProperty(resource, key);
if (resourceVal !== undefined && String(resourceVal) !== val) return false;
}
}
return true;
}
}
return false;
}
function extractResourceProperty(resource: string, key: string): number | undefined {
if (key === "sensitivity") {
const match = resource.match(/sensitivity=(\d+)/);
return match ? parseInt(match[1], 10) : undefined;
}
return undefined;
}
export function createReceipt(
model: HostModel,
principal: string,
action: string,
resource: string,
resourceId: string,
decision: "permit" | "deny",
result: "success" | "failure" | "denied",
details: string,
contentHash?: string,
disclosureTier?: number
): Receipt {
const id = `receipt_${crypto.randomUUID().substring(0, 12)}`;
const timestamp = new Date().toISOString();
const receiptData = JSON.stringify({
id, timestamp, principal, action, resource, resourceId, decision, result, details, contentHash, disclosureTier,
});
const signature = signData(model.keyPair.privateKey, receiptData);
const receipt: Receipt = {
id, timestamp, principal, action, resource, resourceId, decision, result,
details, contentHash, disclosureTier, signature,
};
model.receipts.push(receipt);
return receipt;
}
export function resolveMachineXPath(model: HostModel, selector: string): MachineGraphNode[] {
const results: MachineGraphNode[] = [];
const parsed = parseMXPath(selector);
if (!parsed) return results;
for (const node of model.graph.nodes) {
if (parsed.type && node.type !== parsed.type) continue;
let matches = true;
for (const [key, condition] of Object.entries(parsed.conditions)) {
const nodeValue = node.properties[key];
if (nodeValue === undefined) {
matches = false;
break;
}
if (condition.op === "=" && String(nodeValue) !== condition.value) {
matches = false;
break;
}
if (condition.op === "<=" && Number(nodeValue) > Number(condition.value)) {
matches = false;
break;
}
if (condition.op === ">=" && Number(nodeValue) < Number(condition.value)) {
matches = false;
break;
}
if (condition.op === "<" && Number(nodeValue) >= Number(condition.value)) {
matches = false;
break;
}
if (condition.op === ">" && Number(nodeValue) <= Number(condition.value)) {
matches = false;
break;
}
if (condition.op === "contains" && !String(nodeValue).includes(condition.value)) {
matches = false;
break;
}
}
if (matches) {
results.push(node);
}
}
return results;
}
interface ParsedMXPath {
type?: string;
conditions: Record<string, { op: string; value: string }>;
}
function parseMXPath(selector: string): ParsedMXPath | null {
const cleaned = selector.replace(/^mx:\/\//, "").replace(/^\/+/, "");
const parts = cleaned.split("/");
const lastSegment = parts[parts.length - 1] || parts[0] || "";
const typeMatch = lastSegment.match(/^(\w+)\[/);
const type = typeMatch ? typeMatch[1] : (lastSegment.match(/^(\w+)$/) ? lastSegment : undefined);
const conditions: Record<string, { op: string; value: string }> = {};
const condMatches = lastSegment.matchAll(/\[([^\]]+)\]/g);
for (const match of condMatches) {
const cond = match[1];
const operators = ["<=", ">=", "!=", "=", "<", ">"];
let matched = false;
for (const op of operators) {
const idx = cond.indexOf(op);
if (idx > 0) {
const key = cond.substring(0, idx).trim();
const value = cond.substring(idx + op.length).trim().replace(/^["']|["']$/g, "");
conditions[key] = { op, value };
matched = true;
break;
}
}
if (!matched) {
conditions[cond] = { op: "contains", value: cond };
}
}
return { type, conditions };
}
export function getHostSummary(model: HostModel): Record<string, unknown> {
return {
hostId: model.hostId,
hostname: model.hostname,
platform: model.state.platform,
arch: model.state.arch,
cpuCount: model.state.cpuCount,
totalMemoryMB: model.state.totalMemoryMB,
freeMemoryMB: model.state.freeMemoryMB,
corpusSize: model.corpus.length,
processCount: model.state.processes.length,
listeningPortCount: model.state.listeningPorts.length,
graphNodeCount: model.graph.nodes.length,
mailboxCount: model.mailboxes.size,
receiptCount: model.receipts.length,
publicKey: model.keyPair.publicKey,
};
}