UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

337 lines (336 loc) • 17.8 kB
import { createHash } from "node:crypto"; import { canonicalJsonStringify } from "./file-backend.js"; import { computeEntryRev } from "./frontmatter.js"; import { MEMORY_ORIGIN_CAUSES } from "./types.js"; import { ambiguousOriginRepresentation, committedOriginOf } from "./frontmatter.js"; export const BUNDLE_SECTION_KEYS = Object.freeze(["meta", "entries", "governance", "residuals"]); function sha256Line(text) { return createHash("sha256").update(`${text}\n`, "utf8").digest("hex"); } export function computeBundleSectionHashes(bundle) { return { meta: sha256Line(canonicalJsonStringify({ bundle: bundle.bundle, at: bundle.at, scopes: bundle.scopes, storeId: bundle.storeId })), entries: sha256Line(canonicalJsonStringify(bundle.entries)), governance: sha256Line(canonicalJsonStringify(bundle.governance)), residuals: sha256Line(canonicalJsonStringify(bundle.residuals)), }; } export function computeMemoryBundleHash(sectionHashes) { return sha256Line(canonicalJsonStringify(sectionHashes)); } const HEX64_RE = /^[0-9a-f]{64}$/; function isRecord(v) { return typeof v === "object" && v !== null && !Array.isArray(v); } function stringArrayInvalid(v, what) { if (!Array.isArray(v)) return `${what} is not an array`; for (const s of v) if (typeof s !== "string") return `${what} carries a non-string member`; return undefined; } function countInvalid(v, what) { return typeof v === "number" && Number.isInteger(v) && v >= 0 ? undefined : `${what} is not a non-negative integer`; } function entryInvalid(raw, scopes) { if (!isRecord(raw)) return "entry is not an object"; const e = raw; if (typeof e.id !== "string" || e.id.length === 0) return "entry has no id"; if (typeof e.slug !== "string" || e.slug.length === 0) return `entry ${JSON.stringify(e.id)} has no slug`; if (typeof e.scope !== "string" || e.scope.length === 0) return `entry ${JSON.stringify(e.id)} has no scope`; if (!scopes.has(e.scope)) return `entry ${JSON.stringify(e.id)} carries scope ${JSON.stringify(e.scope)} outside bundle.scopes (self-consistency)`; if (typeof e.body !== "string") return `entry ${JSON.stringify(e.id)} has no string body`; if (typeof e.rev !== "string" || e.rev.length === 0) return `entry ${JSON.stringify(e.id)} has no rev`; const fm = e.frontmatter; if (!isRecord(fm)) return `entry ${JSON.stringify(e.id)} has no frontmatter object`; for (const key of ["name", "description", "type"]) { if (fm[key] !== undefined && typeof fm[key] !== "string") return `entry ${JSON.stringify(e.id)} frontmatter.${key} is not a string`; } if (fm.deleted !== undefined && typeof fm.deleted !== "boolean") return `entry ${JSON.stringify(e.id)} frontmatter.deleted is not a boolean`; if (fm.extra !== undefined) { const bad = stringArrayInvalid(fm.extra, `entry ${JSON.stringify(e.id)} frontmatter.extra`); if (bad !== undefined) return bad; } if (fm.trust !== undefined && fm.trust !== "untrusted") return `entry ${JSON.stringify(e.id)} frontmatter.trust is not "untrusted"`; if (fm.provenance !== undefined) { const p = fm.provenance; if (!isRecord(p) || p.kind !== "repo_file" || typeof p.path !== "string" || typeof p.contentHash !== "string" || typeof p.ingestedAt !== "number") { return `entry ${JSON.stringify(e.id)} frontmatter.provenance is malformed`; } } if (fm.origin !== undefined) { const o = fm.origin; if (!isRecord(o) || o.taint !== "external" || typeof o.at !== "number" || !Number.isFinite(o.at) || (o.cause !== undefined && !(typeof o.cause === "string" && MEMORY_ORIGIN_CAUSES.includes(o.cause)))) { return `entry ${JSON.stringify(e.id)} frontmatter.origin is malformed`; } } if (fm.distilled !== undefined) { const d = fm.distilled; const inputsOk = (v) => Array.isArray(v) && v.length > 0 && v.every((row) => isRecord(row) && typeof row.id === "string" && row.id.length > 0 && typeof row.rev === "string" && row.rev.length > 0 && (row.superseded === undefined || row.superseded === true)); if (!isRecord(d) || typeof d.planId !== "string" || d.planId.length === 0 || typeof d.at !== "number" || !Number.isFinite(d.at) || typeof d.carrierRev !== "string" || d.carrierRev.length === 0 || !inputsOk(d.inputs)) { return `entry ${JSON.stringify(e.id)} frontmatter.distilled is malformed`; } } if (ambiguousOriginRepresentation(fm)) { return `entry ${JSON.stringify(e.id)} carries conflicting or duplicated origin representations (ambiguous marker representation)`; } const recomputed = computeEntryRev({ id: e.id, frontmatter: fm, body: e.body }); if (recomputed !== e.rev) return `entry ${JSON.stringify(e.id)} rev ${JSON.stringify(e.rev)} does not equal its recomputed content rev ${JSON.stringify(recomputed)} (forged or corrupted package)`; return undefined; } export function memoryBundleInvalid(raw, opts = {}) { if (!isRecord(raw)) return "bundle is not an object"; const b = raw; if (typeof b.bundle !== "number") return "bundle envelope version is missing"; if (b.bundle > 1) return `bundle envelope version ${String(b.bundle)} is newer than this engine supports — refusing to reinterpret it`; if (b.bundle !== 1) return `bundle envelope version ${String(b.bundle)} is not recognized`; if (typeof b.at !== "number" || !Number.isInteger(b.at)) return "bundle.at is not an integer"; const scopesBad = stringArrayInvalid(b.scopes, "bundle.scopes"); if (scopesBad !== undefined) return scopesBad; const scopes = b.scopes; if (scopes.length === 0) return "bundle.scopes is empty"; if (scopes.some((s) => s.length === 0)) return "bundle.scopes carries an empty scope"; if (new Set(scopes).size !== scopes.length) return "bundle.scopes carries a duplicate scope"; if (typeof b.storeId !== "string" || b.storeId.length === 0) return "bundle.storeId is missing"; if (opts.expectedScopes !== undefined) { const allowed = new Set(opts.expectedScopes); const outside = scopes.find((s) => !allowed.has(s)); if (outside !== undefined) return `bundle scope ${JSON.stringify(outside)} is outside the expected scope set — whole package refused`; } const entriesArr = Array.isArray(b.entries) && b.entries; if (entriesArr === false) return "bundle.entries is not an array"; const scopeSet = new Set(scopes); const entryIds = new Set(); for (const raw2 of entriesArr) { const bad = entryInvalid(raw2, scopeSet); if (bad !== undefined) return bad; const id = raw2.id; if (entryIds.has(id)) return `bundle.entries carries id ${JSON.stringify(id)} twice (bad package)`; entryIds.add(id); } const gov = b.governance; if (!isRecord(gov)) return "bundle.governance is missing (the governance section is a structural requirement, not an attachment)"; const challengesArr = Array.isArray(gov.challenges) && gov.challenges; if (challengesArr === false) return "bundle.governance.challenges is not an array"; const challengeEventIds = new Set(); for (const raw2 of challengesArr) { if (!isRecord(raw2)) return "challenge row is not an object"; const c = raw2; if (typeof c.eventId !== "string" || c.eventId.length === 0) return "challenge row has no eventId"; if (typeof c.entryId !== "string" || c.entryId.length === 0) return "challenge row has no entryId"; if (typeof c.reason !== "string") return "challenge row has no reason"; if (typeof c.at !== "number" || !Number.isInteger(c.at)) return "challenge row has no integer at"; if (c.challengedRev !== undefined && typeof c.challengedRev !== "string") return "challenge row challengedRev is not a string"; if (challengeEventIds.has(c.eventId)) return `bundle.governance.challenges carries eventId ${JSON.stringify(c.eventId)} twice (bad package)`; challengeEventIds.add(c.eventId); } const pollutedSessionsArr = Array.isArray(gov.pollutedSessions) && gov.pollutedSessions; if (pollutedSessionsArr === false) return "bundle.governance.pollutedSessions is not an array"; const pollutedIds = new Set(); for (const raw2 of pollutedSessionsArr) { if (!isRecord(raw2)) return "polluted-session row is not an object"; if (typeof raw2.sessionId !== "string" || raw2.sessionId.length === 0) return "polluted-session row has no sessionId"; if (typeof raw2.at !== "number") return "polluted-session row has no at"; if (typeof raw2.reason !== "string") return "polluted-session row has no reason"; if (raw2.cause !== undefined && !(typeof raw2.cause === "string" && MEMORY_ORIGIN_CAUSES.includes(raw2.cause))) return "polluted-session row cause is not a known spelling"; if (pollutedIds.has(raw2.sessionId)) return `bundle.governance.pollutedSessions carries sessionId ${JSON.stringify(raw2.sessionId)} twice (bad package)`; pollutedIds.add(raw2.sessionId); } const lineageArr = Array.isArray(gov.lineage) && gov.lineage; if (lineageArr === false) return "bundle.governance.lineage is not an array"; const lineagePairs = new Set(); for (const raw2 of lineageArr) { if (!isRecord(raw2)) return "lineage row is not an object"; if (typeof raw2.entryId !== "string" || raw2.entryId.length === 0) return "lineage row has no entryId"; if (typeof raw2.sessionId !== "string" || raw2.sessionId.length === 0) return "lineage row has no sessionId"; if (typeof raw2.lastRev !== "string" || raw2.lastRev.length === 0) return "lineage row has no lastRev"; if (typeof raw2.lastAt !== "number") return "lineage row has no lastAt"; if (raw2.marked !== undefined && raw2.marked !== true) return "lineage row marked is not the literal true"; if (raw2.marked === true) { const carried = entriesArr.find((e) => e.id === raw2.entryId); if (carried !== undefined && committedOriginOf(carried.frontmatter) === undefined) { return `bundle.governance.lineage row (${String(raw2.entryId)}, ${String(raw2.sessionId)}) claims marked:true but the carried entry has no origin marker (dishonest package)`; } } const key = `${raw2.entryId}\u0000${raw2.sessionId}`; if (lineagePairs.has(key)) return `bundle.governance.lineage carries (${raw2.entryId}, ${raw2.sessionId}) twice (bad package)`; lineagePairs.add(key); } const custodyArr = Array.isArray(gov.custody) && gov.custody; if (custodyArr === false) return "bundle.governance.custody is not an array"; const custodyByEv = new Map(); const scopeLiteralOutside = (row) => { const sideScope = (v) => { if (!isRecord(v)) return undefined; return typeof v.scope === "string" ? v.scope : undefined; }; const literals = [ sideScope(row.from), sideScope(row.to), typeof row.scope === "string" ? row.scope : undefined, sideScope(row.select), ]; return literals.find((s) => s !== undefined && !scopeSet.has(s)); }; for (const raw2 of custodyArr) { if (!isRecord(raw2)) return "custody row is not an object"; if (typeof raw2.ev !== "string" || raw2.ev.length === 0) return "custody row has no ev"; if (typeof raw2.channel !== "string" || raw2.channel.length === 0) return "custody row has no channel"; if (typeof raw2.at !== "number" || !Number.isInteger(raw2.at)) return "custody row has no integer at"; const outside = scopeLiteralOutside(raw2); if (outside !== undefined) return `custody row ${JSON.stringify(raw2.ev)} names scope ${JSON.stringify(outside)} outside bundle.scopes (self-consistency: every in-package scope literal ⊆ bundle.scopes)`; const canonical = canonicalJsonStringify(raw2); const prior = custodyByEv.get(raw2.ev); if (prior !== undefined && prior !== canonical) return `bundle.governance.custody carries ev ${JSON.stringify(raw2.ev)} twice with DIFFERENT payloads (bad package)`; custodyByEv.set(raw2.ev, canonical); } const res = b.residuals; if (!isRecord(res)) return "bundle.residuals is missing"; for (const key of ["quarantined", "unbound", "pendingLatch"]) { const bad = stringArrayInvalid(res[key], `bundle.residuals.${key}`); if (bad !== undefined) return bad; } const contentUnavailableArr = Array.isArray(res.contentUnavailable) && res.contentUnavailable; if (contentUnavailableArr === false) return "bundle.residuals.contentUnavailable is not an array"; for (const raw2 of contentUnavailableArr) { if (!isRecord(raw2) || typeof raw2.id !== "string" || typeof raw2.scope !== "string" || typeof raw2.slug !== "string" || typeof raw2.rev !== "string" || typeof raw2.reason !== "string") { return "bundle.residuals.contentUnavailable carries a malformed row"; } if (!scopeSet.has(raw2.scope)) return `bundle.residuals.contentUnavailable row ${JSON.stringify(raw2.id)} names scope ${JSON.stringify(raw2.scope)} outside bundle.scopes (self-consistency)`; } for (const key of ["quarantineOpaque", "unsliceableCustody", "unboundCount", "pendingLatchCount"]) { const bad = countInvalid(res[key], `bundle.residuals.${key}`); if (bad !== undefined) return bad; } const integrity = b.integrity; if (!isRecord(integrity)) return "bundle.integrity is missing"; const sh = integrity.sectionHashes; if (!isRecord(sh)) return "bundle.integrity.sectionHashes is missing"; const shKeys = Object.keys(sh).sort(); if (shKeys.length !== BUNDLE_SECTION_KEYS.length || BUNDLE_SECTION_KEYS.some((k) => !Object.prototype.hasOwnProperty.call(sh, k))) { return `bundle.integrity.sectionHashes must carry exactly the keys ${BUNDLE_SECTION_KEYS.join("/")} (a stripped or padded section set is a bad package)`; } for (const k of BUNDLE_SECTION_KEYS) { if (typeof sh[k] !== "string" || !HEX64_RE.test(sh[k])) return `bundle.integrity.sectionHashes.${k} is not a sha256 hex`; } const recomputed = computeBundleSectionHashes(b); for (const k of BUNDLE_SECTION_KEYS) { if (recomputed[k] !== sh[k]) return `bundle.integrity.sectionHashes.${k} does not match the section's canonical content (damaged or tampered package)`; } const entryCount = integrity.entryCount; if (typeof entryCount !== "number" || entryCount !== entriesArr.length) return "bundle.integrity.entryCount does not equal entries.length"; const revs = integrity.revs; if (!isRecord(revs)) return "bundle.integrity.revs is missing"; const revKeys = Object.keys(revs); if (revKeys.length !== entryIds.size) return "bundle.integrity.revs cardinality does not equal the entry id set"; for (const e of b.entries) { if (revs[e.id] !== e.rev) return `bundle.integrity.revs[${JSON.stringify(e.id)}] does not equal the entry's rev`; } return undefined; } export const BUNDLE_FREE_TEXT_DISCLOSURE = "Free-string fields in this bundle (pollution/challenge reasons, challenge event ids, custody request ids, session ids) are carried verbatim and may contain text the exporting deployment put there; subset-export tenant isolation does not extend to those literals."; export function assembleMemoryExportBundle(input) { const sections = { bundle: 1, at: input.at, scopes: [...input.scopes].sort(), storeId: input.storeId, entries: [...input.entries].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)), governance: { challenges: input.challenges, pollutedSessions: [...input.pollutedSessions].sort((a, b) => (a.sessionId < b.sessionId ? -1 : a.sessionId > b.sessionId ? 1 : 0)), lineage: [...input.lineage].sort((a, b) => (a.entryId < b.entryId ? -1 : a.entryId > b.entryId ? 1 : 0) || (a.sessionId < b.sessionId ? -1 : a.sessionId > b.sessionId ? 1 : 0)), custody: input.custody, }, residuals: input.residuals, }; const sectionHashes = computeBundleSectionHashes(sections); const doc = [BUNDLE_FREE_TEXT_DISCLOSURE]; if (input.residuals.unsliceableCustody > 0) { doc.push(`This subset export could not carry ${input.residuals.unsliceableCustody} custody row(s) that have no scope to slice by (unbound-deletion history among them): the anti-resurrection guarantee for those ids does not travel with this bundle — a full-store export carries them.`); } return { ...sections, integrity: { entryCount: sections.entries.length, revs: Object.fromEntries(sections.entries.map((e) => [e.id, e.rev])), sectionHashes, }, doc, }; }