Files
ECC/scripts/lib/memory-vault-format.js
T
Affaan MustafaandGitHub 4d0b501b05 feat: add cross-harness memory vault (#2581)
Add a local-first, cross-harness memory vault with CLI and MCP surfaces, bounded search and storage, harness-scoped visibility, setup guidance, and comprehensive tests.
2026-07-26 02:46:59 -07:00

310 lines
10 KiB
JavaScript

'use strict';
const { TextDecoder } = require('util');
const MEMORY_SCHEMA_VERSION = 'ecc.memory.v1';
const MEMORY_KINDS = Object.freeze([
'context',
'decision',
'fact',
'handoff',
'lesson',
'note',
'preference',
'runbook',
]);
const MEMORY_SCOPES = Object.freeze(['project', 'team', 'user']);
const MEMORY_TRUST_STATES = Object.freeze(['unreviewed']);
const MEMORY_STATUSES = Object.freeze(['active', 'rejected', 'superseded']);
const MAX_BODY_BYTES = 64 * 1024;
const MAX_DOCUMENT_BYTES = 128 * 1024;
const MAX_TITLE_CHARS = 200;
const MAX_TAGS = 32;
const MAX_LINKS = 64;
const MAX_TARGETS = 32;
const MEMORY_ID_PATTERN = /^mem_[a-z0-9][a-z0-9_-]{2,127}$/;
const SLUG_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
const ISO_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
const FRONTMATTER_FIELDS = Object.freeze([
['schema', 'schema'],
['id', 'id'],
['title', 'title'],
['kind', 'kind'],
['scope', 'scope'],
['trust', 'trust'],
['status', 'status'],
['source_harness', 'sourceHarness'],
['target_harnesses', 'targetHarnesses'],
['tags', 'tags'],
['links', 'links'],
['created_at', 'createdAt'],
['updated_at', 'updatedAt'],
]);
const FRONTMATTER_KEYS = new Map(FRONTMATTER_FIELDS);
const FATAL_UTF8_DECODER = new TextDecoder('utf-8', { fatal: true });
const SECRET_PATTERNS = Object.freeze([
{ label: 'provider API key', pattern: /\bsk-[A-Za-z0-9_-]{16,}\b/i },
{ label: 'Stripe key', pattern: /\b(?:sk|rk)_live_[A-Za-z0-9]{16,}\b/ },
{ label: 'npm token', pattern: /\bnpm_[A-Za-z0-9]{20,}\b/ },
{ label: 'Hugging Face token', pattern: /\bhf_[A-Za-z0-9]{20,}\b/ },
{ label: 'GitHub token', pattern: /\bgh[pors]_[A-Za-z0-9]{16,}\b/ },
{ label: 'GitHub token', pattern: /\bgithub_pat_[A-Za-z0-9_]{16,}\b/ },
{ label: 'Google API key', pattern: /\bAIza[A-Za-z0-9_-]{16,}\b/ },
{ label: 'Slack token', pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/ },
{ label: 'AWS access key', pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/ },
{ label: 'private key', pattern: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/ },
]);
function hasUnsafeControlCharacters(value, allowBodyWhitespace = false) {
return Array.from(value).some(character => {
const codePoint = character.codePointAt(0);
const allowedWhitespace = allowBodyWhitespace
&& (codePoint === 0x09 || codePoint === 0x0a || codePoint === 0x0d);
const isControl = (codePoint <= 0x1f && !allowedWhitespace)
|| (codePoint >= 0x7f && codePoint <= 0x9f);
const isBidirectionalFormatting = (
(codePoint >= 0x202a && codePoint <= 0x202e)
|| (codePoint >= 0x2066 && codePoint <= 0x2069)
);
return isControl || isBidirectionalFormatting;
});
}
function asNonEmptyString(value, label, maxChars = 10_000) {
if (typeof value !== 'string' || value.trim().length === 0) {
throw new Error(`${label} must be a non-empty string.`);
}
const normalized = value.trim();
if (normalized.length > maxChars) {
throw new Error(`${label} is too long (maximum ${maxChars} characters).`);
}
if (hasUnsafeControlCharacters(normalized)) {
throw new Error(`${label} must not contain control or bidirectional formatting characters.`);
}
return normalized;
}
function validateEnum(value, allowed, label) {
const normalized = asNonEmptyString(value, label, 64);
if (!allowed.includes(normalized)) {
throw new Error(`${label} must be one of: ${allowed.join(', ')}.`);
}
return normalized;
}
function validateSlug(value, label) {
const normalized = asNonEmptyString(value, label, 64);
if (!SLUG_PATTERN.test(normalized)) {
throw new Error(`${label} must be a lowercase letters/numbers slug.`);
}
return normalized;
}
function validateMemoryId(value) {
const normalized = asNonEmptyString(value, 'memory id', 132);
if (!MEMORY_ID_PATTERN.test(normalized)) {
throw new Error('memory id must match mem_<lowercase-id> and cannot contain a path.');
}
return normalized;
}
function uniqueStrings(values, { label, limit, validator }) {
if (!Array.isArray(values)) {
throw new Error(`${label} must be an array.`);
}
if (values.length > limit) {
throw new Error(`${label} has too many values (maximum ${limit}).`);
}
return values.reduce((result, value) => {
const normalized = validator(value);
if (result.includes(normalized)) {
throw new Error(`${label} must not contain duplicate values.`);
}
return [...result, normalized];
}, []);
}
function validateTimestamp(value, label) {
const normalized = asNonEmptyString(value, label, 64);
const parsed = new Date(normalized);
if (
!ISO_TIMESTAMP_PATTERN.test(normalized)
|| Number.isNaN(parsed.getTime())
|| parsed.toISOString() !== normalized
) {
throw new Error(`${label} must be an ISO-8601 timestamp.`);
}
return normalized;
}
function normalizeBody(value) {
if (typeof value !== 'string') {
throw new Error('memory body must be a string.');
}
if (hasUnsafeControlCharacters(value, true)) {
throw new Error('memory body must not contain unsafe control or bidirectional formatting characters.');
}
const normalized = value.trim();
if (normalized.length === 0) {
throw new Error('memory body must contain non-whitespace context.');
}
if (Buffer.byteLength(normalized, 'utf8') > MAX_BODY_BYTES) {
throw new Error(`memory body is too large (maximum ${MAX_BODY_BYTES} bytes).`);
}
return normalized;
}
function normalizeMemory(memory) {
if (!memory || typeof memory !== 'object' || Array.isArray(memory)) {
throw new Error('memory must be an object.');
}
const targetHarnesses = uniqueStrings(memory.targetHarnesses, {
label: 'target harnesses',
limit: MAX_TARGETS,
validator: value => validateSlug(value, 'target harness'),
});
if (targetHarnesses.length === 0) {
throw new Error('target harnesses must contain at least one harness or "all".');
}
if (memory.schema !== MEMORY_SCHEMA_VERSION) {
throw new Error('Unsupported memory schema.');
}
return {
schema: memory.schema,
id: validateMemoryId(memory.id),
title: asNonEmptyString(memory.title, 'memory title', MAX_TITLE_CHARS),
kind: validateEnum(memory.kind, MEMORY_KINDS, 'memory kind'),
scope: validateEnum(memory.scope, MEMORY_SCOPES, 'memory scope'),
trust: validateEnum(memory.trust, MEMORY_TRUST_STATES, 'memory trust'),
status: validateEnum(memory.status, MEMORY_STATUSES, 'memory status'),
sourceHarness: validateSlug(memory.sourceHarness, 'source harness'),
targetHarnesses,
tags: uniqueStrings(memory.tags, {
label: 'tags',
limit: MAX_TAGS,
validator: value => validateSlug(value, 'tag'),
}),
links: uniqueStrings(memory.links, {
label: 'links',
limit: MAX_LINKS,
validator: validateMemoryId,
}),
createdAt: validateTimestamp(memory.createdAt, 'created_at'),
updatedAt: validateTimestamp(memory.updatedAt, 'updated_at'),
body: normalizeBody(memory.body),
};
}
function serializeMemoryDocument(memory) {
const normalized = normalizeMemory(memory);
const metadata = FRONTMATTER_FIELDS.map(([serializedKey, objectKey]) => (
`${serializedKey}: ${JSON.stringify(normalized[objectKey])}`
)).join('\n');
const body = normalized.body.length > 0 ? `\n\n${normalized.body}` : '';
return `---\n${metadata}\n---${body}\n`;
}
function decodeUtf8(buffer, label = 'text') {
try {
return FATAL_UTF8_DECODER.decode(buffer);
} catch {
throw new Error(`${label} must contain valid UTF-8 text.`);
}
}
function parseFrontmatterLine(line, sourcePath, seen) {
const separator = line.indexOf(':');
if (separator <= 0) {
throw new Error(`Invalid memory frontmatter line in ${sourcePath}.`);
}
const serializedKey = line.slice(0, separator).trim();
const objectKey = FRONTMATTER_KEYS.get(serializedKey);
if (!objectKey) {
throw new Error(`Unknown memory frontmatter field in ${sourcePath}.`);
}
if (seen.has(objectKey)) {
throw new Error(`Duplicate memory frontmatter field in ${sourcePath}.`);
}
const rawValue = line.slice(separator + 1).trim();
try {
return { objectKey, value: JSON.parse(rawValue) };
} catch {
throw new Error(`Memory frontmatter field in ${sourcePath} must use a JSON value.`);
}
}
function parseMemoryDocument(source, sourcePath = '<memory>') {
const openingMarker = typeof source === 'string'
? /^---\r?\n/.exec(source)
: null;
if (!openingMarker) {
throw new Error(`Memory document ${sourcePath} must start with --- frontmatter.`);
}
if (Buffer.byteLength(source, 'utf8') > MAX_DOCUMENT_BYTES) {
throw new Error(`Memory document ${sourcePath} is too large.`);
}
const frontmatterStart = openingMarker[0].length;
const remainder = source.slice(frontmatterStart);
const closingMarker = /\r?\n---(?=\r?\n|$)/.exec(remainder);
if (!closingMarker) {
throw new Error(`Memory document ${sourcePath} has no closing frontmatter marker.`);
}
const frontmatterSource = remainder.slice(0, closingMarker.index);
const parsed = frontmatterSource.split(/\r?\n/).reduce((state, line) => {
const next = parseFrontmatterLine(line, sourcePath, state.seen);
return {
values: { ...state.values, [next.objectKey]: next.value },
seen: new Set([...state.seen, next.objectKey]),
};
}, { values: {}, seen: new Set() });
const missing = FRONTMATTER_FIELDS
.map(([, objectKey]) => objectKey)
.filter(objectKey => !parsed.seen.has(objectKey));
if (missing.length > 0) {
throw new Error(`Memory document ${sourcePath} is missing fields: ${missing.join(', ')}.`);
}
const afterMarker = remainder.slice(closingMarker.index + closingMarker[0].length);
const body = afterMarker.replace(/^\r?\n/, '').replace(/\r?\n$/, '');
return normalizeMemory({ ...parsed.values, body });
}
function findPotentialSecrets(value) {
const text = typeof value === 'string' ? value : '';
return SECRET_PATTERNS
.filter(item => item.pattern.test(text))
.map(item => item.label)
.filter((label, index, labels) => labels.indexOf(label) === index);
}
module.exports = {
MAX_BODY_BYTES,
MAX_DOCUMENT_BYTES,
MEMORY_KINDS,
MEMORY_SCHEMA_VERSION,
MEMORY_SCOPES,
MEMORY_STATUSES,
MEMORY_TRUST_STATES,
asNonEmptyString,
decodeUtf8,
findPotentialSecrets,
hasUnsafeControlCharacters,
normalizeMemory,
parseMemoryDocument,
serializeMemoryDocument,
uniqueStrings,
validateEnum,
validateMemoryId,
validateSlug,
};