export * from "./audit.js"; export * from "./conversation-provenance.js"; /** * Write doctrine: a node without at least one adjacency is noise, * not knowledge. `writeNodeWithEdges` is the single primitive every graph * writer should call; it rejects zero-relationship writes, verifies every * relationship target resolves before the node is created, and commits the * node + all edges in one managed transaction. Provenance is stamped on the * node as flattened `createdBy*` properties (Neo4j does not persist nested * maps, and flat props are queryable — `MATCH (n) WHERE n.createdBySession * = $id RETURN n` is the forensic entry point). * * Process provenance doctrine (widened): writes * targeting any label in `ACTION_PROVENANCE_LABELS` (Person, UserProfile, * AdminUser, Organization, LocalBusiness, CloudflareTunnel, * CloudflareHostname) must include at least one inbound `:PRODUCED` edge * whose source carries one of `:Task`, `:Conversation`, or `:Message` (the * label set is checked against the actual `labels()` of the target node, so * subtype labels like `:AdminConversation`, `:UserMessage`, `:AssistantMessage`, * and `:AdminMessage` all qualify). This makes every durable LLM-tool-driven * entity creation traversable from either: * - the Task that produced it (autonomous workflows: onboarding skill, * cloudflare tunnel-login, etc.), or * - the Conversation/Message turn that triggered it (direct admin asks — * "add Anneke Roux as person" stamps a `:PRODUCED` edge from the active * `:AdminConversation` so the entity is traceable to its causal turn * without forcing the agent to mint a synthetic `:Task` per ask). * The Task-mediated path stays canonical for autonomous workflows. Direct * admin asks attach via the active Conversation/Message turn to avoid * redundant-Task bloat. Bootstrap writes (`createdBy.agent === 'system'`) * are exempt — installer and lazy-create paths run as system writers. * * Rejection paths (every one emits a stderr log the admin server pipes to * server.log, so orphan pressure is visible per-write not just in the * hourly [graph-health] signal): * - zero relationships → `[graph-write] reject reason=zero-relationships` * - unresolved target id → `[graph-write] reject reason=unresolved-target` * - missing provenance → `[graph-write] warn reason=missing-provenance` * (Task 580 — soft warn; write proceeds. Composer-spawned admin sessions * inherit a bare env that never carries SESSION_NODE_ID, so the prior * hard reject was failing every direct admin write to a gated label.) * - removed-feature write → `[graph-write] reject reason=removed-feature` * (`:ReviewAlert` and `:Event {actionTool:"review-digest-compose"}` are * deleted features; the gate catches doctrine-compliant writers * re-introducing them. The vitest tombstone grep is the primary fence * for raw `session.run("MERGE …")` callers that bypass this primitive.) * * The `createdBy.agent` field is advisory, not authoritative — it is sourced * from the MCP server's AGENT_SLUG env var, which is set by the trusted * spawning code (admin server / workflow runner). A misconfigured spawner * will write "unknown"; that's the signal something is wrong. Do not use * this field for access control — it is forensic, not a security boundary. */ /** * Labels that require an inbound `:PRODUCED` edge from a `:Task`, * `:Conversation`, or `:Message`. Bootstrap writes with * `createdBy.agent === 'system'` are exempt — that carve-out covers * PIN-setup `writeAdminUserAndPerson`, schema apply, and the lazy * `loadUserProfile` MERGE path that bypasses this primitive entirely * (raw Cypher in `platform/ui/app/lib/neo4j-store.ts`). * * The set is intentionally not exhaustive — labels added here become a * blocking gate for every LLM-tool write of that label. Add a label only * after every legitimate writer of it can either (a) carry a PRODUCED edge * from one of the accepted source families, or (b) declare itself as * `agent: 'system'`. */ export declare const ACTION_PROVENANCE_LABELS: ReadonlySet; /** * Source labels that satisfy the inbound-PRODUCED gate. The check is * performed against the full `labels()` array of the target node, so * subtype labels (`:AdminConversation`, `:UserMessage`, etc.) qualify * because they carry the parent `:Conversation` / `:Message` label too. */ export declare const PROVENANCE_SOURCE_LABELS: ReadonlySet; /** * Labels carrying a `CREATE VECTOR INDEX … ON (n.embedding)` in * platform/neo4j/schema.cypher. A node of one of these labels written with a * null embedding is invisible to the vector half of memory-search until a * manual reindex. This constant is the single runtime source of truth — * graph-write stays fs-free (it is bundled into the ESM payload and must not * read schema.cypher at runtime), so the memory-plugin drift test * (vector-indexed-labels-drift) pins it to the schema instead. */ export declare const VECTOR_INDEXED_LABELS: ReadonlySet; /** * Document/archive labels whose primary content is a long body. The safety-net * embed prefers a bounded `summary` field over the raw `body`/`text` blob for * these; the dedicated archive-ingest path adds the per-:Section embeddings. */ export declare const DOCUMENT_ARCHIVE_LABELS: ReadonlySet; import type { Session } from "neo4j-driver"; export interface GraphRelationship { type: string; direction: "outgoing" | "incoming"; targetNodeId: string; } export interface CreatedBy { /** Agent slug (e.g. "maxy-admin", "whatsapp-public") for LLM-tool writes. */ agent?: string; /** Session correlation id — same string across every node written in a conversation turn. */ session?: string; /** MCP tool name ("memory-write", "contact-create", ...) for LLM-tool writes. */ tool?: string; /** Subsystem name ("workflow-execute", "persist-tool-call", ...) for system writes. */ source?: string; } export interface WriteNodeWithEdgesParams { session: Session; labels: string[]; props: Record; /** At least one relationship is required — zero-rel writes are rejected. */ relationships: GraphRelationship[]; createdBy: CreatedBy; /** * Override for the accountId gate. Production callers leave * this unset — the gate compares `props.accountId` against * `process.env.ACCOUNT_ID`, which the MCP server / UI server validates at * boot time. Tests pass an explicit value to avoid env coupling. * Reading from env var (instead of an on-disk dir enumeration) keeps * graph-write out of the `node:fs` import chain — the lib is bundled * transitively into the ESM payload via cloudflare-task-tracker.ts, and * any CJS-side fs require shims to a runtime-fatal `__require("fs")` * after esbuild's CJS-to-ESM conversion strips the `node:` prefix. */ expectedAccountId?: string; /** * Optional embedder. When supplied and the node carries a label in * VECTOR_INDEXED_LABELS and `props.embedding` is absent/empty, the chokepoint * computes a bounded props embedding before CREATE. Injected (never imported) * so graph-write stays out of the node:fs / Ollama import chain. A failed * embed logs `[graph-write] warn reason=embed-failed` and the write proceeds — * a failed embed must never abort an otherwise-valid write. */ embed?: (text: string) => Promise; } export interface WriteNodeResult { nodeId: string; labels: string[]; edgesCreated: number; } /** Stamp flattened provenance into node properties. */ export declare function stampCreatedBy(props: Record, createdBy: CreatedBy): Record; /** * Enforce the write doctrine: node + ≥1 edge, transactional, provenance stamped. * The label and relationship type strings are interpolated into Cypher — the * caller must constrain them via Zod/schema before calling. Backticks are * stripped to defeat the only Cypher escape char. */ export declare function writeNodeWithEdges(params: WriteNodeWithEdgesParams): Promise; //# sourceMappingURL=index.d.ts.map