Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ All notable changes to engram are documented here. Format based on

## [Unreleased]

### Added — v4.0 "Mesh + Spine" Phase 1 foundation (in progress, target ship: 2026-05-25)

Federation foundation. Loopback transport + cross-machine TLS land in subsequent v4.0 phases. This commit ships the identity, signing, audit, and PII-stripping layers — everything that makes the wire safe before the wire exists.

- **`src/mesh/types.ts`** — Envelope, MessageType, TrustScore, AuditEntry. Five message types defined: `peer.hello`, `peer.audit`, `mistake.shared`, `pattern.shared`, `decision.shared`. Wire constants: protocol v1, 64KB envelope cap, 5-min clock tolerance, 24h replay cache TTL. `computeTrust()` implements the `0.4*success + 0.2*uptime + 0.2*threat + 0.2*integrity` aggregate, clamped to [0,1].
- **`src/mesh/jcs.ts`** — RFC 8785 canonical JSON serialization. ~150 LoC pure JS, no native dep. Produces deterministic byte sequences for ed25519 signing. Throws on non-finite numbers, circular references, functions, symbols, bigint. `canonicalizeEnvelopeForSigning()` strips `sig` before serializing.
- **`src/mesh/identity.ts`** — ed25519 keypair generation, persistence, sign/verify. Uses Node's built-in `crypto` module (Node 12+ has stable ed25519). Storage at `~/.engram/mesh/`: `private.key` (DER, 0600), `public.key` (DER, 0644), `fingerprint` (base64url SHA-256). `initIdentity()` is idempotent. `loadIdentity()` throws if uninitialized. Cross-platform (private key mode check skipped on Windows).
- **`src/mesh/pii-gate.ts`** — 14-category PII stripper. Categories: email, AWS access keys, JWT, Bearer tokens, ETH/BTC addresses, SSN, phone (US + E.164 international), IPv4 (incl. CIDR), filesystem paths, hostnames, high-entropy tokens (Shannon ≥ 4.0), Luhn-validated credit cards. `stripPiiDeep()` recurses through arrays/objects. Tested against fixture corpus at `tests/fixtures/pii-zoo.json`.
- **`src/mesh/audit.ts`** — append-only JSONL audit log at `~/.engram/mesh/audit.jsonl`. Same atomicity contract as `intelligence/hook-log.ts`: never throws, 10MB rotation cap, swallow-on-error.
- **CLI: `engram mesh init`, `engram mesh status`, `engram mesh audit`** — three commands wired into `src/cli.ts`. Verified end-to-end against an isolated `HOME` directory.
- **+97 tests** across `tests/mesh/{types,jcs,identity,pii-gate,audit}.test.ts`. Hermetic — use `mkdtempSync` for filesystem state, no real `~/.engram/` interaction.

PRD: `~/Desktop/Projects/Engram/01-prds/05-v4-mesh-spine-PRD.md`. RFC for the wire format: `~/Desktop/Projects/Engram/02-architecture/rfcs/RFC-0001-mesh-wire-format.md` (decision: JSON over WebSocket, defer Protobuf to RFC-0002 if v4.1 telemetry justifies migration).

## [3.4.0] — 2026-05-02 — "Universal Spine"

The release that turns engram from a Claude Code tool into a universal context spine across every major AI coding tool. Same engram, same graph, same 89.1% reduction — now plugged into 8 IDEs out of the box.
Expand Down
59 changes: 59 additions & 0 deletions docs/install.html
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,7 @@
eng<span class="accent">r</span><span class="accent">a</span>m
</a>
<ul class="nav-links">
<li><a class="nav-link" href="#coming-next">v4.0</a></li>
<li><a class="nav-link" href="#whats-new">v3.4</a></li>
<li><a class="nav-link" href="#install">Install</a></li>
<li><a class="nav-link" href="#how">How it works</a></li>
Expand Down Expand Up @@ -943,6 +944,64 @@ <h1 class="hero-headline reveal">
</div>
</section>

<!-- ================== WHAT'S COMING IN v4.0 ================== -->
<section class="block" id="coming-next" style="border-top: 1px solid var(--border);">
<div class="container">
<div class="section-head">
<div>
<div class="section-eyebrow">// v4.0 · in flight</div>
<h2 class="section-title">Mesh + Spine: federation lands May 25.</h2>
</div>
<div class="section-meta">Phase 1 foundation already merged · 1007 tests passing · target ship: 2026-05-25</div>
</div>

<p style="color: var(--text-secondary); max-width: 720px; margin-bottom: 2rem;">
v3.4 made engram the <em>universal</em> context spine across 8 IDEs. v4.0 makes it the
<em>federated</em> spine. Engram instances on different machines exchange mistakes, ADRs, and
patterns through an opt-in mesh. PII-stripped at the wire layer. mTLS plus ed25519 identity.
Local-first stays the default. Federation is opt-in only.
</p>

<div class="v3-grid">
<article class="v3-card">
<div class="v3-card-tag">mesh</div>
<h3 class="v3-card-title"><span class="accent">◆</span> Federation protocol</h3>
<p class="v3-card-body">
ed25519 identity, JCS-canonical signing, 14-category PII gate, 64KB envelope cap, replay
protection via ULID + 24h cache. Behavioral trust scoring auto-downgrades on
signature mismatch or PII-leak attempt.
</p>
</article>

<article class="v3-card">
<div class="v3-card-tag">decisions</div>
<h3 class="v3-card-title"><span class="accent">◇</span> ADR miner</h3>
<p class="v3-card-body">
Architecture Decision Records become queryable like code. Engram surfaces a relevant
ADR above mistakes when its keywords match. Federate them through the mesh and a fix
in one team's repo immunizes every other repo on the network.
</p>
</article>

<article class="v3-card">
<div class="v3-card-tag">cost lens v2</div>
<h3 class="v3-card-title"><span class="accent">$</span> Live token-savings dashboard</h3>
<p class="v3-card-body">
<code>engram cost --watch</code> for terminal live-tail. <code>~/.engram/cost-config.json</code>
for custom rate overrides. Telegram pipe via Jarvis when <code>JARVIS_TELEGRAM_TOKEN</code>
is set. Weekly digest auto-posts to your team channel.
</p>
</article>
</div>

<p style="color: var(--text-secondary); margin-top: 2rem; font-size: 0.95rem;">
Track v4.0 progress on <a href="https://github.com/NickCirv/engram/discussions" style="color: var(--accent);">GitHub Discussions</a>
or subscribe at <a href="https://engram.substack.com" style="color: var(--accent);">engram.substack.com</a>
for the launch deep-dive when it ships.
</p>
</div>
</section>

<!-- ================== WHAT'S NEW IN v3.0 ================== -->
<section class="block" id="whats-new" style="border-top: 1px solid var(--border);">
<div class="container">
Expand Down
74 changes: 74 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,80 @@ program
console.log();
});

// ── mesh (v4.0) ─────────────────────────────────────────────────────────────
// Federation foundation. Loopback / cross-machine TLS lands in subsequent
// phases of v4.0. This release ships identity + audit + status only.
const mesh = program.command("mesh").description("Federation (v4.0): identity, peers, audit");

mesh
.command("init")
.description("Initialize ed25519 identity at ~/.engram/mesh/")
.action(async () => {
const m = await import("./mesh/index.js");
const id = m.initIdentity();
m.logAudit({ ts: "", action: "key_rotate", peer: id.fingerprint, reason: "init" });
console.log(chalk.bold("\nengram mesh — identity\n"));
console.log(` fingerprint: ${chalk.cyan(id.fingerprint)}`);
console.log(` private key: ${chalk.dim(id.privatePath)} (mode 0600)`);
console.log(` public key: ${chalk.dim(id.publicPath)}`);
console.log(` audit log: ${chalk.dim(id.dir + "/audit.jsonl")}`);
console.log(
chalk.dim(
`\n Cross-machine federation lands in v4.0 cross-machine phase (target May 12).\n` +
` Loopback connectivity ships first. Use \`engram mesh status\` to track readiness.\n`,
),
);
});

mesh
.command("status")
.description("Show mesh identity, peer health, recent audit entries")
.action(async () => {
const m = await import("./mesh/index.js");
let id;
try {
id = m.loadIdentity();
} catch {
console.log(
chalk.yellow(
" mesh not initialized. Run `engram mesh init` to create an identity.",
),
);
return;
}
const audit = m.readAudit();
console.log(chalk.bold("\nengram mesh status\n"));
console.log(` fingerprint: ${chalk.cyan(id.fingerprint)}`);
console.log(` audit events: ${audit.length}`);
if (audit.length > 0) {
console.log(chalk.dim(" recent:"));
for (const e of audit.slice(-5)) {
console.log(
chalk.dim(` ${e.ts} ${e.action.padEnd(14)} ${e.peer ?? "-"}`),
);
}
}
console.log("");
});

mesh
.command("audit")
.description("Tail the mesh audit log")
.option("-n, --lines <n>", "Number of lines to show", "20")
.action(async (opts: { lines: string }) => {
const m = await import("./mesh/index.js");
const audit = m.readAudit();
const limit = Math.max(1, Math.min(1000, parseInt(opts.lines, 10) || 20));
const tail = audit.slice(-limit);
if (tail.length === 0) {
console.log(chalk.dim(" (no audit events yet — run `engram mesh init` first)"));
return;
}
for (const e of tail) {
console.log(`${e.ts} ${e.action.padEnd(14)} ${e.peer ?? "-"} ${e.reason ?? ""}`);
}
});

// ── cost lens (v3.3) ────────────────────────────────────────────────────────
program
.command("cost")
Expand Down
75 changes: 75 additions & 0 deletions src/mesh/audit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* v4.0 Mesh — append-only audit log.
*
* Every mesh send, receive, reject, and trust event is recorded as a JSONL
* line at ~/.engram/mesh/audit.jsonl. Append-only, size-capped, never
* throws.
*
* Shape mirrors `intelligence/hook-log.ts` — same atomicity contract, same
* rotation policy, same swallow-on-error discipline. If logging fails, the
* mesh keeps working.
*/
import { appendFileSync, existsSync, renameSync, statSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { meshDir } from "./identity.js";
import type { AuditEntry } from "./types.js";

/** Maximum size in bytes before rotation fires. 10 MB. */
export const MESH_AUDIT_MAX_BYTES = 10 * 1024 * 1024;

const LOG_FILENAME = "audit.jsonl";
const LOG_ROTATED_FILENAME = "audit.jsonl.1";

/**
* Append one entry to the audit log. Adds `ts` automatically if not set.
* Never throws — failures are silently swallowed.
*/
export function logAudit(entry: AuditEntry, dir: string = meshDir()): void {
try {
const logPath = join(dir, LOG_FILENAME);
rotateIfNeeded(dir);
const withTs = entry.ts ? entry : { ...entry, ts: new Date().toISOString() };
appendFileSync(logPath, JSON.stringify(withTs) + "\n");
} catch {
// Logging must never break the mesh.
}
}

/** Rotate the audit log if it has crossed the size cap. Destructive on .1. */
export function rotateIfNeeded(dir: string = meshDir()): void {
try {
const logPath = join(dir, LOG_FILENAME);
if (!existsSync(logPath)) return;
const size = statSync(logPath).size;
if (size < MESH_AUDIT_MAX_BYTES) return;
const rotatedPath = join(dir, LOG_ROTATED_FILENAME);
renameSync(logPath, rotatedPath);
} catch {
// Silent failure on rotation — recoverable on the next attempt.
}
}

/**
* Read the current audit log as parsed entries. Skips malformed lines.
* Does NOT include the rotated `.1` file — callers that want history can
* read both.
*/
export function readAudit(dir: string = meshDir()): AuditEntry[] {
try {
const logPath = join(dir, LOG_FILENAME);
if (!existsSync(logPath)) return [];
const raw = readFileSync(logPath, "utf8");
const entries: AuditEntry[] = [];
for (const line of raw.split("\n")) {
if (!line.trim()) continue;
try {
entries.push(JSON.parse(line) as AuditEntry);
} catch {
// Skip malformed.
}
}
return entries;
} catch {
return [];
}
}
Loading
Loading