Esqueleto inicial: sabia-core + sabia-cli + UI local, OpenWolf

- sabia-core: descoberta de interfaces, controle de modo monitor/canal
  via iw (ponte até ter nl80211 nativo), captura crua 802.11 via
  AF_PACKET (sem libpcap), parsing de radiotap e quadros de
  gerenciamento (beacon/probe/EAPOL), agregação em tabela de APs/
  clientes, escrita de .pcap, trilha de auditoria encadeada por hash,
  auto-atualização via GitHub releases (curl, sem dependências de
  rede extras)
- sabia-cli: `sabia iface`, `sabia scan`, `sabia update`,
  `sabia audit-verify`, `sabia ui` (servidor local + navegador,
  protegido por token, mesma filosofia do Guará)
- OpenWolf inicializado para memória de projeto entre agentes
This commit is contained in:
Lucas committed 2026-10-23 08:22:04 -03:00
commit e989413018
76 files changed
+8386

No files matched your search

+424
View File
@@ -0,0 +1,424 @@
import * as fs from "node:fs"
import * as path from "node:path"
import type { AnatomyEntry } from "./types.js"
// ── Markdown format (canonical — mirrors src/hooks/anatomy-store.ts) ────────
// The auto-generated header block: never captured as preamble, or every
// import→render cycle would duplicate it.
const AUTO_HEADER = /^(?:# anatomy\.md\s*$|> Auto-maintained by OpenWolf\.|> Files:\s*\d+\s*tracked)/
// Legacy symbol sub-bullets (2.0.0–2.0.3 renders): mechanical output, dropped
// rather than preserved. Any OTHER indented bullet is hand-written and kept.
const SYMBOL_SUBBULLET = /^ {2}- (?:fn|class|method|section) `[^`]+` L\d+-\d+ \(~\d+ tok\)$/
/** Strip leading/trailing blank lines so preserved blocks don't grow by two lines per write. */
function trimBlankEdges(lines: string[]): string[] {
let start = 0
let end = lines.length
while (start < end && lines[start].trim() === "") start++
while (end > start && lines[end - 1].trim() === "") end--
return lines.slice(start, end)
}
export function parseAnatomyWithRaw(content: string): {
sections: Map<string, AnatomyEntry[]>
rawLines: Map<string, string[]>
preamble: string[]
} {
const sections = new Map<string, AnatomyEntry[]>()
const rawLines = new Map<string, string[]>()
const preamble: string[] = []
let currentSection = ""
for (const raw of content.split("\n")) {
const line = raw.replace(/\r$/, "")
const sm = line.match(/^## (.+)/)
if (sm) {
currentSection = sm[1].trim()
if (!sections.has(currentSection)) sections.set(currentSection, [])
continue
}
if (!currentSection) {
// Above the first section heading: preserve everything hand-written
// (issue #61), skipping only our own generated header block.
if (!AUTO_HEADER.test(line)) preamble.push(line)
continue
}
const em = line.match(/^- `([^`]+)`(?:\s+—\s+(.+?))?\s*\(~(\d+)\s+tok\)$/)
if (em) {
sections.get(currentSection)!.push({
file: em[1],
description: em[2] || "",
tokens: parseInt(em[3], 10),
})
continue
}
if (line.trim() === "") continue
if (SYMBOL_SUBBULLET.test(line)) continue
if (!rawLines.has(currentSection)) rawLines.set(currentSection, [])
rawLines.get(currentSection)!.push(line)
}
return { sections, rawLines, preamble: trimBlankEdges(preamble) }
}
export function parseAnatomy(content: string): Map<string, AnatomyEntry[]> {
return parseAnatomyWithRaw(content).sections
}
// serializeAnatomy was removed: it could not carry rawLines or preamble, and
// every writer must go through renderStore so preserved content survives
// (issue #61).
export function extractDescription(filePath: string): string {
const MAX_DESC = 150
const basename = path.basename(filePath)
const ext = path.extname(basename).toLowerCase()
const known: Record<string, string> = {
"package.json": "Node.js package manifest",
"tsconfig.json": "TypeScript configuration",
".gitignore": "Git ignore rules",
"README.md": "Project documentation",
}
if (known[basename]) return known[basename]
let content: string
try {
const fd = fs.openSync(filePath, "r")
const buf = Buffer.alloc(12288)
const n = fs.readSync(fd, buf, 0, 12288, 0)
fs.closeSync(fd)
content = buf.subarray(0, n).toString("utf-8")
} catch {
return ""
}
if (!content.trim()) return ""
const cap = (s: string) => s.length <= MAX_DESC ? s : s.slice(0, MAX_DESC - 3) + "..."
if (ext === ".md" || ext === ".mdx") {
const m = content.match(/^#{1,2}\s+(.+)$/m)
if (m) return cap(m[1].trim())
}
if (ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".jsx") {
if (basename === "page.tsx" || basename === "page.js") return "Next.js page component"
if (basename === "layout.tsx" || basename === "layout.js") return "Next.js layout"
const exports = (content.match(/export\s+(?:async\s+)?(?:function|class|const|interface|type|enum)\s+(\w+)/g) || [])
.map(e => e.match(/(\w+)$/)?.[1]).filter(Boolean) as string[]
if (exports.length > 0 && exports.length <= 5) return `Exports ${exports.join(", ")}`
if (exports.length > 5) return cap(`Exports ${exports.slice(0, 4).join(", ")} + ${exports.length - 4} more`)
}
const declM = content.match(/(?:function|class|const|interface|type|enum)\s+(\w+)/)
if (declM) return `Declares ${declM[1]}`
return ""
}
// ── Durable store + lock (mirrors src/hooks/anatomy-store.ts, F2b) ──────────
import * as crypto from "node:crypto"
import * as os from "node:os"
export const STORE_FILE = "anatomy-index.json"
const LOCK_STALE_MS = 10_000
export const LOCK_BUDGET_MS = 2_000
export function sha256(text: string): string {
return crypto.createHash("sha256").update(text).digest("hex")
}
export interface SymbolEntry {
name: string
kind: "fn" | "class" | "method" | "section"
startLine: number
endLine: number
tokens: number
}
export interface StoreFileEntry {
description: string
tokens: number
/** sha256 (first 16 hex chars) of file content when last indexed. */
hash?: string
size?: number
mtimeMs?: number
updatedAt: string
source: "hook" | "scan" | "md-import"
symbols?: SymbolEntry[]
}
export interface AnatomyStoreData {
version: 1
meta: {
lastScanned: string
fileCount: number
hits: number
misses: number
/** sha256 of the markdown this store last rendered — skew detection key. */
renderedHash: string
storeUpdatedAt: string
}
/** Keyed by full normalized relative path, e.g. "src/hooks/shared.ts". */
files: Record<string, StoreFileEntry>
/** Non-conforming anatomy.md lines preserved verbatim, keyed by section. */
rawLines?: Record<string, string[]>
/**
* Hand-written content above the first `## ` heading (issue #61). A typed
* field rather than a reserved rawLines key: old compiled hooks still
* installed in projects ignore an unknown field and round-trip it safely,
* whereas a magic "" section key would render as a stray `## ` heading.
*/
preamble?: string[]
}
export function newStore(): AnatomyStoreData {
const now = new Date().toISOString()
return { version: 1, meta: { lastScanned: now, fileCount: 0, hits: 0, misses: 0, renderedHash: "", storeUpdatedAt: now }, files: {} }
}
export function loadStore(wolfDir: string): AnatomyStoreData | null {
try {
const parsed = JSON.parse(fs.readFileSync(path.join(wolfDir, STORE_FILE), "utf-8"))
if (parsed && parsed.version === 1 && parsed.files && parsed.meta) return parsed as AnatomyStoreData
return null
} catch {
return null
}
}
export function saveStore(wolfDir: string, store: AnatomyStoreData): void {
store.meta.fileCount = Object.keys(store.files).length
store.meta.storeUpdatedAt = new Date().toISOString()
const filePath = path.join(wolfDir, STORE_FILE)
const tmp = filePath + "." + crypto.randomBytes(4).toString("hex") + ".tmp"
const body = JSON.stringify(store, null, 2)
try {
fs.writeFileSync(tmp, body, "utf-8")
fs.renameSync(tmp, filePath)
} catch {
try { fs.writeFileSync(filePath, body, "utf-8") } catch {}
try { fs.unlinkSync(tmp) } catch {}
}
}
/** Section key for a relpath: "src/hooks/" or "./" for root files. */
export function sectionKeyOf(relPath: string): string {
const dir = path.dirname(relPath).split(path.sep).join("/")
return dir === "." ? "./" : dir + "/"
}
/**
* Render the store to markdown, byte-identical to the legacy format. Symbols
* deliberately do NOT render: they live in anatomy-index.json and reach the
* model through the per-file pre-read hint. Rendering them roughly 2.4x'd
* anatomy.md, and agents instructed to consult anatomy.md paid that bill
* wholesale every session.
*/
export function renderStore(store: AnatomyStoreData): string {
const bySection = new Map<string, Array<{ file: string; entry: StoreFileEntry }>>()
for (const [relPath, entry] of Object.entries(store.files)) {
const key = sectionKeyOf(relPath)
if (!bySection.has(key)) bySection.set(key, [])
bySection.get(key)!.push({ file: relPath.slice(relPath.lastIndexOf("/") + 1), entry })
}
const lines: string[] = [
"# anatomy.md",
"",
`> Auto-maintained by OpenWolf. Last scanned: ${store.meta.lastScanned}`,
`> Files: ${Object.keys(store.files).length} tracked | Anatomy hits: ${store.meta.hits} | Misses: ${store.meta.misses}`,
"",
]
const preamble = trimBlankEdges(store.preamble ?? [])
if (preamble.length > 0) {
lines.push(...preamble, "")
}
const rawBySection = store.rawLines ?? {}
const keys = [...new Set([...bySection.keys(), ...Object.keys(rawBySection)])].sort()
for (const key of keys) {
lines.push(`## ${key}`)
lines.push("")
const raws = rawBySection[key] ?? []
for (const raw of raws) {
lines.push(raw)
}
const entries = (bySection.get(key) ?? []).sort((a, b) => a.file.localeCompare(b.file))
if (raws.length > 0 && entries.length > 0) lines.push("")
for (const { file, entry } of entries) {
const desc = entry.description ? ` — ${entry.description}` : ""
lines.push(`- \`${file}\`${desc} (~${entry.tokens} tok)`)
}
lines.push("")
}
return lines.join("\n")
}
/** Write the rendered markdown atomically and pin its hash in the store. */
export function renderToFile(wolfDir: string, store: AnatomyStoreData): void {
const content = renderStore(store)
store.meta.renderedHash = sha256(content)
const anatomyPath = path.join(wolfDir, "anatomy.md")
const tmp = anatomyPath + "." + crypto.randomBytes(4).toString("hex") + ".tmp"
try {
fs.writeFileSync(tmp, content, "utf-8")
fs.renameSync(tmp, anatomyPath)
} catch {
try { fs.writeFileSync(anatomyPath, content, "utf-8") } catch {}
try { fs.unlinkSync(tmp) } catch {}
}
}
/**
* Absorb out-of-band edits to anatomy.md (old compiled hooks, agent/human
* hand-edits) into the store. ADDITIVE-ONLY:
* - md entry differs → md wins description/tokens (newer intent)
* - md entry absent from store → added (source "md-import")
* - store entry absent from md → KEPT unless the file is gone from disk
* (deletions are exclusively the full scanner's job)
* Symbols always survive (they only flow store → render).
*/
export function importFromMarkdown(store: AnatomyStoreData, mdContent: string, projectRoot: string): void {
// Corruption guard: an empty/unreadable anatomy.md must never wipe the
// preserved hand-written content in the store.
if (!mdContent.trim()) return
const { sections, rawLines, preamble } = parseAnatomyWithRaw(mdContent)
if (rawLines.size > 0) {
store.rawLines = Object.fromEntries(rawLines)
} else {
delete store.rawLines
}
if (preamble.length > 0) {
store.preamble = preamble
} else {
delete store.preamble
}
const seen = new Set<string>()
for (const [sectionKey, entries] of sections) {
const dir = sectionKey === "./" ? "" : sectionKey
for (const e of entries) {
const relPath = (dir + e.file).split("\\").join("/")
seen.add(relPath)
const existing = store.files[relPath]
if (!existing) {
store.files[relPath] = { description: e.description, tokens: e.tokens, updatedAt: new Date().toISOString(), source: "md-import" }
} else if (existing.description !== e.description || existing.tokens !== e.tokens) {
existing.description = e.description
existing.tokens = e.tokens
existing.updatedAt = new Date().toISOString()
existing.source = "md-import"
}
}
}
// Entries the md no longer lists: keep unless the file is really gone.
for (const relPath of Object.keys(store.files)) {
if (seen.has(relPath)) continue
if (!fs.existsSync(path.join(projectRoot, relPath))) delete store.files[relPath]
}
}
/**
* Read-side lookup: resolve a file to its anatomy entry. Store-first with an
* O(1) relpath key; falls back to a suffix scan (paths outside the root) and
* finally to parsing anatomy.md for projects that predate the store.
* `normalizedFile` and `projectDir` use forward slashes.
*/
export function lookupEntry(
wolfDir: string,
projectDir: string,
normalizedFile: string
): { file: string; description: string; tokens: number; symbols?: SymbolEntry[]; size?: number; mtimeMs?: number } | null {
const rel = normalizedFile.startsWith(projectDir + "/")
? normalizedFile.slice(projectDir.length + 1)
: normalizedFile.startsWith("/") ? null : normalizedFile
const store = loadStore(wolfDir)
if (store) {
const toResult = (rp: string, e: StoreFileEntry) => ({
file: rp.slice(rp.lastIndexOf("/") + 1),
description: e.description,
tokens: e.tokens,
symbols: e.symbols,
size: e.size,
mtimeMs: e.mtimeMs,
})
const hit = rel ? store.files[rel] : undefined
if (hit) return toResult(rel!, hit)
for (const [rp, e] of Object.entries(store.files)) {
if (normalizedFile === rp || normalizedFile.endsWith("/" + rp)) {
return toResult(rp, e)
}
}
return null
}
// Pre-store project: legacy markdown scan.
let md: string
try {
md = fs.readFileSync(path.join(wolfDir, "anatomy.md"), "utf-8")
} catch {
return null
}
for (const [sectionKey, entries] of parseAnatomy(md)) {
const dir = sectionKey === "./" ? "" : sectionKey
for (const entry of entries) {
const entryRelPath = (dir + entry.file).split("\\").join("/")
if (normalizedFile === entryRelPath || normalizedFile.endsWith("/" + entryRelPath)) {
return entry
}
}
}
return null
}
/**
* Standard writer entry point: load the store (bootstrapping from anatomy.md
* on first contact), and absorb any md-side divergence before the caller
* mutates. Call ONLY while holding the anatomy lock.
*/
export function loadStoreReconciled(wolfDir: string, projectRoot: string): AnatomyStoreData {
let store = loadStore(wolfDir)
let md: string | null = null
try { md = fs.readFileSync(path.join(wolfDir, "anatomy.md"), "utf-8") } catch {}
if (!store) {
store = newStore()
if (md) importFromMarkdown(store, md, projectRoot)
return store
}
if (md !== null && sha256(md) !== store.meta.renderedHash) importFromMarkdown(store, md, projectRoot)
return store
}
function lockSleep(ms: number): void {
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)
}
export function withAnatomyLock<T>(wolfDir: string, budgetMs: number, fn: () => T): T | null {
const lockPath = path.join(wolfDir, "anatomy-index.lock")
const deadline = Date.now() + budgetMs
while (true) {
try {
fs.writeFileSync(lockPath, JSON.stringify({ pid: process.pid, hostname: os.hostname(), acquiredAt: Date.now() }), { flag: "wx" })
break
} catch {}
let stale = false
try {
const body = JSON.parse(fs.readFileSync(lockPath, "utf-8"))
stale = typeof body.acquiredAt !== "number" || Date.now() - body.acquiredAt > LOCK_STALE_MS
if (!stale && body.hostname === os.hostname() && typeof body.pid === "number") {
try { process.kill(body.pid, 0) } catch (err) { stale = (err as NodeJS.ErrnoException).code === "ESRCH" }
}
} catch {
try { stale = Date.now() - fs.statSync(lockPath).mtimeMs > LOCK_STALE_MS } catch {}
}
if (stale) {
const graveyard = lockPath + "." + crypto.randomBytes(4).toString("hex") + ".stale"
try { fs.renameSync(lockPath, graveyard); try { fs.unlinkSync(graveyard) } catch {} } catch {}
}
if (Date.now() >= deadline) return null
lockSleep(25 + Math.floor(Math.random() * 25))
}
try {
return fn()
} finally {
try { fs.unlinkSync(lockPath) } catch {}
}
}