The scattered rule is the leak, so there is one rule

Flair

An identity and memory substrate whose read-scoping lives in a single helper every path must import — written that way because the same rule, scattered, once leaked office memories to any authenticated agent — and whose promoted memories default to private unless a recorded justification says otherwise.

Carries 3 of 7 rubric mechanisms. Most systems here carry none or one (41%), and a dash means the mechanism was not found at this commit — not that the system needed it. Each mark is one LLM reviewer's reading of the code at this commit rather than a run of it — known limits.

  • Tombstone
  • Trust state
  • Bi-temporal
  • Scope enforced
  • Mutation audit
  • Human review
  • Negative evals

1. Executive Summary

Flair is "[t]he identity and memory substrate for AI agents. Crypto-pinned. Federated. Self-hosted." Apache-2.0, TypeScript, version 0.54.2, 250,467 lines with 474 test files, distributed as an npm CLI over a Harper instance it installs and supervises, with thirteen adapter packages and a stdio MCP server written into whichever clients it detects.

Its pitch is three things that survive a restart: an identity proved with an Ed25519 keypair, memory searched by meaning, and a "soul" — the personality, values and procedures that make an agent that agent.

What makes it worth reading is none of those. It is that the comments record the bugs.

The read-scoping module exists because the rule was once scattered. Its header says so:

"Before this module existed, SemanticSearch had its OWN inline grant-resolution + a visibility === "office" global OR-clause that leaked ANY authenticated agent's read of ANY other agent's memories once that memory happened to carry visibility: "office" … Scattering the scoping rule per path is exactly how that leak happened — this module exists so it can't happen again: one rule, one place, every path imports it."

Four paths import it — search, get, semantic search, bootstrap, and the by-id guard in the auth middleware — the resolver is composed from a record-type registry rather than hand-typed per site, and a test introspects the composed resolver's mode and owner field against that registry so the two cannot drift apart again. That is the correct response to a scoping leak, carried through to the test that keeps it fixed.

The migration invariant is argued, not asserted. The private exclusion is visibility != 'private' and the module explains why it is not visibility == 'shared':

"not_equal, which INCLUDES records missing the field entirely … never visibility == 'shared' (equals, which would EXCLUDE them and silently retroactively privatize every legacy row)."

Choosing the predicate by what it does to rows written before the field existed is a habit almost nothing else here displays.

Promotion defaults to private unless something justified sharing. A continuity candidate promoted into memory re-verifies a scope tag, a visibilityRuling and a visibilityRationale, and defaults to private otherwise, under a rule written into the schema: "a shared promoted row must always trace to a recorded justification, never to a default."

The dedup gate never suppresses a write, in capitals, with the bug that earned it: an earlier client-side gate returned the existing record instead of writing, and "two topically-close but DISTINCT findings — one about replication route-directionality, one about DDL/schema replication — and the SECOND was silently dropped". Now the server computes a match signal, attaches it to the response, and writes anyway.

Three marks follow from the above: a stored visibility that withholds, an owner key bound from the authenticated agent and applied by one resolver, and an end-to-end test — "(a) search as the connector: A's org-non-private row IS visible; A's private row NEVER is" — that asserts both halves through the actual MCP search tool, checking the private row's absence by id and by content marker.

Now read the scope model before deploying it, because it is not what "identity substrate" usually implies. Within one instance, every verified agent reads every other agent's non-private memory. That is deliberate — the module calls it knowledge refinement rather than access control, private is "the ONLY owner-only exception", and per-owner grants still exist as an inspectable relationship but no longer gate any read. For a personal instance that is a defensible model and the "zero knobs" argument behind it is coherent. For a shared one it means one careless agent registration reads everything not marked private, and the only hard boundary left is the federation push filter, which "already excludes private rows from ever leaving this instance".

The identity is also narrower than the tagline. The Ed25519 key proves an agent to the HTTP surface; memories are not encrypted with it, so losing it "costs the identity, not the data", and nothing signs a memory's content. The attribution field on a usage row is "OPAQUE — never parsed, never fed to an LLM, never rendered" and the schema says to trust it as "nothing more than a label" — the right disclosure, and a reminder that provenance here is a hint rather than a proof.

That candour runs through the documentation too, which volunteers what most projects bury: the match percentage "is not a probability that the memory answers your question correctly"; a root-owned install makes semantic search "silently degrade to keyword-only"; the built-in MCP surface is off by default and "[n]o documented client setup uses it today"; and the install pulls roughly 130 MB of React Native tooling a Node install never uses, with the upstream issue linked and a warning not to fix it with --omit=optional because that breaks the database binding.

2. Mental Model

A memory belongs to an agent and is open to the instance unless it says private.

Private is the only word that withholds anything.

A promotion must cite its reason to be shared.

A key proves who is calling, not what was said.

Diagram — one resolver every cross-agent path imports, after a scattered copy of the same rule leaked office memories; private is the only value that withholds, and promotion must justify anything else
Diagram source
%% caption: one resolver every cross-agent path imports, after a scattered copy of the same rule leaked office memories; private is the only value that withholds, and promotion must justify anything else
flowchart TB
    RULE["resolveReadScope — 'The SINGLE source every<br/>cross-agent Memory read path resolves through'"]
    P1["Memory.search() / get()"] --> RULE
    P2["SemanticSearch"] --> RULE
    P3["MemoryBootstrap"] --> RULE
    P4["auth-middleware by-id guard"] --> RULE
    PAST["the leak this replaced: SemanticSearch's own<br/>inline grant resolution plus a global<br/>visibility === 'office' OR-clause"] -.->|"'Scattering the scoping rule per path<br/>is exactly how that leak happened'"| RULE
    RULE --> COND{"reader vs row"}
    COND -->|"the reader's own row"| ALL["readable at ANY visibility,<br/>including private"]
    COND -->|"another agent's row"| NP{"visibility != 'private'"}
    NP -->|"'shared', or the field absent"| OPEN["readable by any verified agent<br/>on this instance — open-within-org,<br/>no grant consulted"]
    NP -->|"'private'"| DENY["denied, with a message that does not<br/>name the other agent"]
    NOTE["not_equal, never equals 'shared' —<br/>equals 'would silently retroactively<br/>privatize every legacy row'"] -.-> NP
    PROM["continuity candidate promoted into memory"] --> CHK{"scopeTag AND visibilityRuling<br/>AND visibilityRationale re-verified?"}
    CHK -->|"yes"| SHARED["shared, traceable to a recorded justification"]
    CHK -->|"no"| PRIV["private — 'never to a default'"]
    FED["federation push filter"] -->|"the one remaining hard boundary:<br/>private rows never leave the instance"| OUT["another instance"]
    DEDUP["conservative-duplicate gate"] -->|"NEVER SUPPRESSES A WRITE —<br/>signal on the response only"| WROTE["the write happens anyway"]

3. Architecture

Area Role
resources/Memory.ts The memory resource, the dedup signal, the write response
resources/memory-read-scope.ts The one scoping rule
resources/memory-visibility.ts Validation and the durability-keyed default
resources/SemanticSearch.ts, bm25-index.ts Hybrid retrieval
resources/Federation.ts Cross-instance sync and its push filter
src/cli.ts, src/commands/ init, doctor, upgrade, federation, quality
packages/ The stdio MCP adapter, a client, and eleven runtime bridges

4. Essential Implementation Paths

resources/memory-read-scope.ts:4-46 — the leak, the rule, and the invariant, in one header.

resources/Memory.ts:41-95 — a gate that computes a signal and never drops a write, and the registry the scope is drawn from.

schemas/memory.graphql:268-276 — default private unless a justification survives re-verification.

test/integration/mcp-connector-principal-mapping.test.ts:256-283 — both halves of the claim, through the tool an agent actually calls.

5. Memory Data Model

Content, owning agent, durability class, visibility, embedding, usage. Durability sets the write-time visibility default and a guard refuses to flip an ephemeral row to shared on either POST or PUT — with the test file labelling its cases "POSITIVE CONTROL" and "no over-fire", which is the vocabulary this atlas uses for exactly the same reason.

6. Retrieval Mechanics

Hybrid semantic and lexical, with the honest note in the README that the displayed percentage is a similarity with a keyword boost, that hybrid ranking means the top result need not have the highest percentage, and that the number is not a probability of correctness. Three sentences that prevent a category of misreading.

7. Write Mechanics

Write, then signal. The gate scopes its single candidate comparison to the same agent as the write — "never cross-agent" — and requires both a cosine threshold and a Jaccard token overlap against the one top candidate, with no fallback to the second. Conservative in both directions: it rarely claims a duplicate, and it never acts on the claim.

8. Agent Integration

flair init --agent name writes a keypair, wires every detected MCP client to a version-pinned npx adapter, and runs a smoke test. The distinction between the built-in /mcp surface (off unless two environment variables are set) and the separate stdio adapter package is documented rather than left to be discovered, which is more care than the average MCP story gets.

9. Reliability, Safety, and Trust

The scoping history above is the trust story, and the surrounding disclosures are the rest of it. What is absent: nothing signs memory content, there is no provenance class distinguishing what a person said from what a model inferred, no supersession, and no record of what a memory said before an edit.

10. Tests, Evals, and Benchmarks

474 test files across unit, isolated-unit, integration and end-to-end suites, with a benchmark harness and a corpus profiler. Test names carry issue numbers and the vocabulary of controls. Nothing was built or run for this reading.

11. For Your Own Build

Put the scoping rule in one module and make every path import it. Then add the test that trips when a second copy appears. This project's own header is the best argument for it in the corpus, because it is written from the other side of the leak.

Choose the predicate by what it does to old rows. != private and == shared differ only on records written before the field existed, and one of them privatises your whole history silently.

Never let a de-duplication gate drop a write. Compute the signal, return it, and store the record; the two-findings bug is what the other design costs.

Default to private and require a reason to share. A promoted row that can only be shared by citing a recorded justification is a memory system that can answer why something is visible.

And say what your score is not. "This percentage is not a probability that the memory answers your question" is one sentence that stops a whole class of downstream misuse.

12. Open Questions

Whether the open-within-org model is intended for multi-tenant instances. The module argues it as knowledge refinement for one org, and nothing in the code distinguishes an org from an instance.

Whether federation verification covers content. The push filter excludes private rows and the tree carries federation-verify and fleet-verify; what they attest to was not traced.

What the "soul" costs at read time. It is central to the pitch and sits outside the memory paths read here.

Appendix: File Index

Path What to read it for
resources/memory-read-scope.ts:4-46 A leak, a rule, and an invariant argued from old rows
resources/Memory.ts:41-95 A gate that signals and never suppresses
schemas/memory.graphql:186-199, :268-276 A label not to trust, and a default that demands a reason
test/data-scoping.test.ts:96-118 The predicate, with its controls and a no-oracle assertion
test/integration/mcp-connector-principal-mapping.test.ts:256-283 Both halves, through the real tool

History

2026-09-1602512aae… — first reading, at a commit dated 15 September 2026. Screened before opening, from a shallow clone: thirty-five files scanned, no auto-run surfaces, twelve build-time execution points, five unpinned surfaces and sixteen dependency files inside the seven-day cooldown. Nothing was installed, built or run.