@tpsdev-ai/flair 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +194 -377
- package/dist/cli.js +1355 -281
- package/dist/deploy.js +212 -24
- package/dist/fabric-upgrade.js +16 -1
- package/dist/federation/scheduler.js +500 -0
- package/dist/install/clients.js +111 -53
- package/dist/lib/mcp-spec.js +128 -0
- package/dist/lib/safe-snapshot-extract.js +231 -0
- package/dist/lib/scheduler-platform.js +128 -0
- package/dist/lib/xml-escape.js +54 -0
- package/dist/rem/scheduler.js +35 -87
- package/dist/rem/snapshot.js +13 -0
- package/dist/replication-convergence.js +505 -0
- package/dist/resources/MemoryBootstrap.js +7 -8
- package/dist/resources/SemanticSearch.js +17 -45
- package/dist/resources/abstention.js +1 -1
- package/dist/resources/embeddings-boot.js +10 -12
- package/dist/resources/embeddings-provider.js +10 -7
- package/dist/resources/health.js +24 -19
- package/dist/resources/in-process.js +225 -0
- package/dist/resources/mcp-tools.js +23 -17
- package/dist/resources/migration-boot.js +80 -10
- package/dist/resources/migrations/data-dir.js +205 -0
- package/dist/resources/migrations/progress.js +33 -0
- package/dist/resources/migrations/runner.js +29 -2
- package/dist/resources/migrations/state.js +13 -2
- package/dist/resources/models-dir.js +18 -9
- package/dist/resources/semantic-retrieval-core.js +5 -4
- package/dist/src/lib/scheduler-platform.js +128 -0
- package/dist/src/lib/xml-escape.js +54 -0
- package/dist/src/rem/scheduler.js +35 -87
- package/docs/deploying-on-fabric.md +267 -0
- package/docs/deployment.md +5 -0
- package/docs/embedding-in-a-harper-app.md +299 -0
- package/docs/federation.md +61 -4
- package/docs/integrations.md +3 -0
- package/docs/mcp-clients.md +16 -7
- package/docs/quickstart.md +80 -54
- package/docs/releasing.md +72 -38
- package/docs/supply-chain-policy.md +36 -0
- package/docs/troubleshooting.md +24 -0
- package/docs/upgrade.md +98 -3
- package/package.json +1 -11
- package/templates/bin/flair-federation-sync.sh.tmpl +28 -0
- package/templates/launchd/dev.flair.federation.sync.plist.tmpl +47 -0
- package/templates/systemd/flair-federation-sync.service.tmpl +21 -0
- package/templates/systemd/flair-federation-sync.timer.tmpl +16 -0
- package/dist/resources/rerank-provider.js +0 -569
- package/docs/rerank-provisioning.md +0 -101
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# Embedding Flair in a Harper app
|
|
2
|
+
|
|
3
|
+
Flair is a Harper component. If your app already runs on Harper, load Flair into the same instance and call it **in-process** — no HTTP, no second process, and **no shell on the node**.
|
|
4
|
+
|
|
5
|
+
Everything below is code you run inside your own component. The CLI is an alternative for local development, not a requirement — see [If you have shell access](#if-you-have-shell-access).
|
|
6
|
+
|
|
7
|
+
| | Embedded | Standalone ([deployment.md](deployment.md)) |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| Latency | A method call | HTTP round trip |
|
|
10
|
+
| Agent identity | You assert it per call | Ed25519 signature, verified server-side |
|
|
11
|
+
| Trust boundary | Callers are **inside** it | Callers are outside it, and must authenticate |
|
|
12
|
+
|
|
13
|
+
Embedding *adds* the in-process path; `rest: true` keeps serving MCP clients and remote agents as before.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Quickstart
|
|
18
|
+
|
|
19
|
+
**1. Add Flair to your instance.** Deploy `@tpsdev-ai/flair` as a component the way you deploy your own — Fabric's component deploy, Studio, or your pipeline. Its tables are declared `@table(database: "flair")`, so they never collide with yours.
|
|
20
|
+
|
|
21
|
+
**2. Resolve the resource and write a memory.**
|
|
22
|
+
|
|
23
|
+
```javascript
|
|
24
|
+
import { server } from "harper";
|
|
25
|
+
// Flair ships these two helpers so you do not have to get either of them right
|
|
26
|
+
// by hand. They are the whole in-process contract.
|
|
27
|
+
import { agentContext, internalContext, collectionResource } from "@tpsdev-ai/flair/dist/resources/in-process.js";
|
|
28
|
+
|
|
29
|
+
// The RESOURCE — carries auth, scoping, visibility, embedding.
|
|
30
|
+
// NOT databases.flair.Memory: that is the raw table, and enforces none of it.
|
|
31
|
+
// Keys carry NO leading slash: get("Memory"), never get("/Memory").
|
|
32
|
+
const flair = (path) => server.resources.get(path).Resource;
|
|
33
|
+
|
|
34
|
+
export async function remember(agentId, content, opts = {}) {
|
|
35
|
+
// A create needs a COLLECTION-bound instance. `new Cls(...)` does not give
|
|
36
|
+
// you one, and cannot be made to — see the note below.
|
|
37
|
+
const h = await collectionResource(flair("Memory"), agentContext(agentId));
|
|
38
|
+
return h.post({
|
|
39
|
+
agentId, // required — an absent one is never filled in
|
|
40
|
+
content,
|
|
41
|
+
durability: opts.durability ?? "standard",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
> ### Why `collectionResource`, and not `new Memory()`
|
|
47
|
+
>
|
|
48
|
+
> A resource's `post()` only works on an instance Harper has marked as a **collection**, and that mark is a *private* field only Harper's own `getResource()` can set. The public `isCollection` is a getter with no setter, so the obvious spelling fails two different ways, neither of which names the cause:
|
|
49
|
+
>
|
|
50
|
+
> ```javascript
|
|
51
|
+
> const h = new (flair("Memory"))(undefined, agentContext(agentId));
|
|
52
|
+
> h.isCollection = true; // TypeError: Cannot set property isCollection ... which has only a getter
|
|
53
|
+
> h.post({ ... }); // without the line above: 405 "The Memory does not have a post method implemented"
|
|
54
|
+
> ```
|
|
55
|
+
>
|
|
56
|
+
> `collectionResource(Cls, context)` is a two-line wrapper over the supported call — `Cls.getResource({}, context, { isCollection: true })` — and exists so this is written once. **Reads do not need it:** `Cls.get(id, context)` and `Cls.search(query, context)` thread the context themselves.
|
|
57
|
+
|
|
58
|
+
> ### ⚠️ A resource with no context is an administrator
|
|
59
|
+
>
|
|
60
|
+
> A resource built without a context resolves to Flair's trusted `internal` verdict and runs **unfiltered** — every read unscoped, every write unowned. Silently. No error, no warning, no trace.
|
|
61
|
+
>
|
|
62
|
+
> Measured, not inferred: a context-less `Memory.search()` returns every agent's `private` records, and so does a context-less `SemanticSearch`.
|
|
63
|
+
>
|
|
64
|
+
> Correct for Flair's own maintenance passes. In your app it is a data leak you find months later.
|
|
65
|
+
>
|
|
66
|
+
> **Make `agentId` a required argument, as above.** Never export a version that defaults it.
|
|
67
|
+
|
|
68
|
+
**3. Read it back, scoped to that agent.**
|
|
69
|
+
|
|
70
|
+
```javascript
|
|
71
|
+
export async function recall(agentId, query, limit = 5) {
|
|
72
|
+
const h = await collectionResource(flair("SemanticSearch"), agentContext(agentId));
|
|
73
|
+
return h.post({ q: query, limit });
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**4. Verify it worked, still in-process.**
|
|
78
|
+
|
|
79
|
+
```javascript
|
|
80
|
+
await remember("agent-alpha", "deploy runs at 0200 UTC");
|
|
81
|
+
console.log(await recall("agent-alpha", "deploy schedule"));
|
|
82
|
+
console.log([...server.resources.keys()].sort()); // what Flair registered
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
> ### What we measured, so you do not have to
|
|
86
|
+
>
|
|
87
|
+
> Run end to end on **Harper 5.1.22**, from a second component loaded into the same instance — the exact shape above. `test/integration/in-process-agents.test.ts` in the Flair repo is that run, and `test/fixtures/inproc-app` is the component it drives.
|
|
88
|
+
>
|
|
89
|
+
> | Claim | Result |
|
|
90
|
+
> |---|---|
|
|
91
|
+
> | `server.resources.get("Memory")` from another component | Returns an **entry object** `{ Resource, path, exportTypes, hasSubPaths, relativeURL }` — `.Resource` is required, it is not the class itself |
|
|
92
|
+
> | `.Resource` is Flair's resource, not the raw table | Confirmed: prototype chain `Memory → Memory → Resource`, and it is **not** `databases.flair.Memory` |
|
|
93
|
+
> | Key format | **No leading slash.** `get("Memory")` hits; `get("/Memory")` returns `undefined` |
|
|
94
|
+
> | `getMatch` | `getMatch("Memory")` hits. **`getMatch("/Memory")` misses** — do not use the slashed form |
|
|
95
|
+
> | When the lookup becomes valid | Flair's resources were already registered at the app component's **module top level** (55 entries, `Memory` and `Agent` present). The only entry missing at that moment was the app's *own*, still mid-registration. Resolving lazily, as above, is still the advice — it costs nothing and does not depend on component load order |
|
|
96
|
+
> | Per-agent scoping through `SemanticSearch` | Holds. Querying as `agent-beta` for a topic only `agent-alpha` has written returns **beta's own** memory, never alpha's private one — with real 768-dim embeddings attached, not a degraded path |
|
|
97
|
+
> | Cross-agent by-id read | `Memory.get(<beta's private id>)` as alpha returns **404**, never 403 — a denied caller cannot enumerate ids |
|
|
98
|
+
> | Context-less call | Unfiltered across all agents, via both `search` and `SemanticSearch` (see the warning above) |
|
|
99
|
+
|
|
100
|
+
Handlers return a `Response` for `401`/`403`/`400` rather than throwing — check for one. `Memory.post()` is in-process only; over HTTP the schema exposes `PUT`.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## N agents in one process
|
|
105
|
+
|
|
106
|
+
**Acting as an agent needs nothing but the context.** Identity resolves per call from `request.tpsAgent`, so one process serves any number of agents — no client to construct, no key to load, no per-agent setup:
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
for (const id of ["planner", "researcher", "reviewer"]) {
|
|
110
|
+
await remember(id, `${id} came online`);
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`tpsAgent` is **not** checked against the `Agent` table, so this works with no registration at all. Register agents anyway — the admin UI, federation, and the HTTP path all read those records.
|
|
115
|
+
|
|
116
|
+
### Registering agents, no CLI
|
|
117
|
+
|
|
118
|
+
Go through the `Agent` **resource**, with no context — provisioning is infrastructure work your app has already authorised, and `Agent.post()` fills in the whole Principal shape for you:
|
|
119
|
+
|
|
120
|
+
```javascript
|
|
121
|
+
export async function registerAgent(id, { publicKey = "pending", admin = false } = {}) {
|
|
122
|
+
const h = await collectionResource(flair("Agent"), internalContext()); // provisioning is infrastructure, not an agent's write
|
|
123
|
+
return h.post({
|
|
124
|
+
id, name: id, displayName: id,
|
|
125
|
+
publicKey, // a placeholder is fine — see below
|
|
126
|
+
runtime: "headless",
|
|
127
|
+
...(admin ? { role: "admin", admin: true } : {}), // role is what actually grants admin
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Verified against a real instance: that lands `kind: "agent"`, `status: "active"`, `displayName`, `admin: false`, `defaultTrustTier: "unverified"`, `type: "agent"`, `createdAt`/`updatedAt` and the federation `originatorInstanceId` stamp — without you naming any of them.
|
|
133
|
+
|
|
134
|
+
> **Prefer this to `databases.flair.Agent.put()`.** The raw table applies **no** defaults, so a hand-written literal has to reproduce every field above and then stay in step with Flair as the Principal model grows. Records written that way are missing `kind`/`status`/`defaultTrustTier` and read as under-specified Principals in the admin surfaces.
|
|
135
|
+
|
|
136
|
+
> **`isAdmin()` reads `role === "admin"`, not the `admin` boolean.** They are separate fields and only `role` grants admin rights; set both to keep the record self-consistent. Admin lookups are cached for 60 seconds, so a newly-created admin is not effective immediately.
|
|
137
|
+
|
|
138
|
+
`publicKey` is non-nullable in the schema, but it does not have to be a real key. An agent that only ever acts in-process never authenticates, and Flair's own paths write placeholders — `"pending"` when seeding, `mcp-oauth:<sub>` for token-authenticated agents. Give an agent a real key only if it must also authenticate **over HTTP**, which your app can do without any CLI:
|
|
139
|
+
|
|
140
|
+
```javascript
|
|
141
|
+
import { generateKeyPairSync } from "node:crypto";
|
|
142
|
+
|
|
143
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
144
|
+
const raw = publicKey.export({ format: "der", type: "spki" }).subarray(-32);
|
|
145
|
+
await registerAgent("remote-worker", { publicKey: raw.toString("hex") });
|
|
146
|
+
// keep `privateKey` in your own secret store — Flair never sees it
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Flair accepts the public key as 64-char hex, or base64 of the raw 32 bytes.
|
|
150
|
+
|
|
151
|
+
Both verified end to end: an agent registered this way, with a key its app minted, then authenticated over HTTP with a real `TPS-Ed25519` signature — and a request signed with the *wrong* key was rejected.
|
|
152
|
+
|
|
153
|
+
> **Two traps.** `Agent.put()` on the *resource* silently strips `publicKey`, so set the key when you **create** the record; to rotate one later, write `databases.flair.Agent.put({ ...existing, publicKey })` on the raw table (which is what `flair agent rotate-key` does through the admin ops API — there is no dedicated endpoint today). And do **not** call `AgentSeed` in-process: although its `allowCreate()` explicitly permits the trusted `internal` verdict, its `post()` then re-checks for a named admin and returns `403 forbidden: admin only` — confirmed by running it.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## The table is not the resource
|
|
158
|
+
|
|
159
|
+
Flair declares `Memory` as a table, then exports a **subclass** as the resource. Harper's loader registers exported classes in the routing map only, never back into `databases`, so the two stay distinct:
|
|
160
|
+
|
|
161
|
+
| Import | What you get |
|
|
162
|
+
|---|---|
|
|
163
|
+
| `server.resources.get("Memory").Resource` | **The resource.** Auth, read-scoping, private-memory exclusion, no-forge attribution, embedding generation, default visibility, rate limiting. |
|
|
164
|
+
| `databases.flair.Memory` | **The table.** Raw storage. None of the above. |
|
|
165
|
+
|
|
166
|
+
Flair uses the raw table where it *wants* to bypass its own rules — the federation merge path writes through it so a synced record's origin stamp survives. Use it only for work you have authorised yourself: provisioning, migrations, admin sweeps, reporting. Never for a memory written on an agent's behalf.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Identity
|
|
171
|
+
|
|
172
|
+
| Context | Verdict | Effect |
|
|
173
|
+
|---|---|---|
|
|
174
|
+
| `{ request: { tpsAnonymous: true } }` | `anonymous` | Denied everywhere |
|
|
175
|
+
| `{ request: { tpsAgent: "mybot" } }` | `agent` | Scoped to that agent — **use this** |
|
|
176
|
+
| `{ request: { tpsAgent: "mybot", tpsAgentIsAdmin: true } }` | admin | Unfiltered reads, cross-agent writes |
|
|
177
|
+
| **Nothing**, or an empty/missing `tpsAgent` | `internal` | **Trusted. Unfiltered.** See the warning above. |
|
|
178
|
+
|
|
179
|
+
Build these with `agentContext(id)`, `adminContext(id)` and `internalContext()` rather than by hand — see [below](#the-api-is-built-so-omission-cannot-happen-quietly) for why the hand-written form is a trap.
|
|
180
|
+
|
|
181
|
+
### The context object is a security boundary
|
|
182
|
+
|
|
183
|
+
**In-process identity is asserted, not verified.** Flair reads `request.tpsAgent` and acts as that agent. There is no signature check, no lookup against the `Agent` table, and no registration requirement — an id that has never been registered acts as an agent immediately. `tpsAgentIsAdmin: true` is asserted exactly the same way, and nothing checks that the named agent is really an admin.
|
|
184
|
+
|
|
185
|
+
That is deliberate. A co-located caller is already inside the trust boundary and could write `databases.flair.Memory` directly, so demanding a signature from same-process code would be theatre. Ed25519 is how agents *outside* the process prove identity.
|
|
186
|
+
|
|
187
|
+
The consequence is the single most important line in this guide:
|
|
188
|
+
|
|
189
|
+
> **Build the context from your own server-side state. Never from request data.**
|
|
190
|
+
>
|
|
191
|
+
> If an agent id can reach `agentContext()` from user input — a body field, a query param, a header you did not verify yourself — that is privilege escalation with **no error, no 403 and no trace**. Authenticate the caller with your own mechanism first, then map the identity *you* established onto `tpsAgent`.
|
|
192
|
+
|
|
193
|
+
There are exactly two ways to lose the model, from opposite ends. Both are pinned as tests in the Flair repo:
|
|
194
|
+
|
|
195
|
+
| | |
|
|
196
|
+
|---|---|
|
|
197
|
+
| **By omission** | No usable agent id ⇒ `internal` ⇒ admin-equivalent, unfiltered. |
|
|
198
|
+
| **By assertion** | An attacker-influenced `agentId` is honoured verbatim. |
|
|
199
|
+
|
|
200
|
+
### The API is built so omission cannot happen quietly
|
|
201
|
+
|
|
202
|
+
`resolveAgentAuth` tests `tpsAgent` for *truthiness*, so a missing or empty id is indistinguishable from "no identity supplied" — which is the trusted, unfiltered verdict. Measured:
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
resolveAgentAuth({ request: { tpsAgent: undefined } }) -> { kind: "internal" }
|
|
206
|
+
allowAdmin({ request: { tpsAgent: undefined } }) -> true
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
That would turn the most ordinary bug there is — `agentContext(session.agentId)` where the field came back undefined — into silent administrator access. So the helpers refuse rather than default:
|
|
210
|
+
|
|
211
|
+
| | |
|
|
212
|
+
|---|---|
|
|
213
|
+
| `agentContext(id)` | **Throws** `InProcessContextError` on a missing, empty or blank id. Takes **no options**, so no object spread into it can escalate. |
|
|
214
|
+
| `adminContext(id)` | The *only* way to get admin authority. Same id guard. |
|
|
215
|
+
| `internalContext()` | The *only* way to get the unfiltered verdict. |
|
|
216
|
+
| `collectionResource(Cls, context)` | Context is **required**; omitting it throws rather than granting `internal`. |
|
|
217
|
+
|
|
218
|
+
The privileged paths are now the longest ones to type, and `git grep "adminContext\|internalContext"` enumerates every deliberate escalation in your codebase. These are runtime guards, not type annotations — a plain-JavaScript embedder gets exactly the same protection.
|
|
219
|
+
|
|
220
|
+
### Individual identities, not one app identity
|
|
221
|
+
|
|
222
|
+
Give every agent its own. A per-agent context costs nothing — no client to construct, no key to load, no per-agent setup, not even a registration. Collapsing N agents onto one shared identity buys you nothing and loses the two things that make the memory model work: **per-agent attribution**, which is what trust grading and provenance are computed from, and **N separate blast radii**, which become one.
|
|
223
|
+
|
|
224
|
+
### In a cluster
|
|
225
|
+
|
|
226
|
+
Harper replicates every table in a replicated database unless the table opts out with `@table(replicate: false)`. None of Flair's do. So:
|
|
227
|
+
|
|
228
|
+
- **The registry replicates.** An agent registered on node A is visible on node B with no coordination. (Replication comes from the *database* being replicated — not from `@export`, which only controls REST exposure. `Memory` carries no `@export` and still replicates.)
|
|
229
|
+
- **Authority is local.** The context is constructed per call, in whichever process handles it. No node asks another who a caller is.
|
|
230
|
+
- **Attribution travels.** `agentId` is a field *on the record*, so a memory written on node A reads back correctly attributed — and correctly scoped — wherever it lands. Verified as far as this can be without a cluster: a fresh Harper process over the same storage resolves identical per-agent scope, so none of it lives in the process that did the writing.
|
|
231
|
+
|
|
232
|
+
The consequence, stated plainly because someone will ask: **every node running the app is equally trusted**, since each one can assert any identity. That is fine for one application spread across regions — it is a single trust domain by construction. It is **not** fine for running this component beside untrusted co-tenants on the same instance. Co-location *is* the grant.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Coexisting with your components
|
|
237
|
+
|
|
238
|
+
Flair's instance-wide middleware runs first but is **non-rejecting** — unrecognised requests pass through rather than 401'ing, so your component's auth keeps working. Two collisions to check:
|
|
239
|
+
|
|
240
|
+
- **`/Admin` is a prefix match.** Flair 401s any unauthenticated path *starting with* `/Admin` — including an app route like `/AdminPanel`. Rename yours.
|
|
241
|
+
- **Top-level paths.** Flair claims ~53, including `Memory`, `Agent`, `Instance`, `Integration`, `Credential`, `Health`, `Presence`, `Relationship`, `Soul`, `SemanticSearch`. `server.resources.keys()` lists them.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Federation
|
|
246
|
+
|
|
247
|
+
Pairing an embedded instance to an external hub as a spoke **requires shell access today** — the pairing and sync commands are CLI-only. If your deployment has no shell, federation is not available to it yet.
|
|
248
|
+
|
|
249
|
+
> **Sync is push-only — one direction per call.** A spoke pushes its records up and **receives nothing back**. There is no pull endpoint, and nothing initiates a hub-to-spoke push. Do not plan on reading another instance's memories through the hub.
|
|
250
|
+
>
|
|
251
|
+
> For records both ways, each instance must pair **as a spoke of the other** — two pairings, each side running its own sync.
|
|
252
|
+
|
|
253
|
+
Private memories are never pushed. Full walkthrough: [federation.md](federation.md).
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## If you have shell access
|
|
258
|
+
|
|
259
|
+
Local development and self-hosted installs can drive the same things from the CLI:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
flair doctor
|
|
263
|
+
flair agent add mybot # registers + generates and stores a keypair
|
|
264
|
+
flair agent list
|
|
265
|
+
flair memory search "deploy schedule" --agent mybot
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Federation, which has no in-process equivalent:
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
flair federation token --admin-pass <hub-admin-pass> > triple.json
|
|
272
|
+
flair federation pair <hub-url> --token-from ./triple.json
|
|
273
|
+
flair federation sync enable --interval 300
|
|
274
|
+
flair federation status
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
`sync enable` landed **after 0.30.0**; on an older install, schedule the one-shot yourself.
|
|
278
|
+
|
|
279
|
+
A `SyncLog` row reads `direction: "pull"` — the receiver's label for a push it accepted. `"push"` is never written. Not a pull path.
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Choosing a surface
|
|
284
|
+
|
|
285
|
+
| Surface | Use when |
|
|
286
|
+
|---|---|
|
|
287
|
+
| **In-process resource** | Your app's agents, same instance. No round trip — and no auth safety net. |
|
|
288
|
+
| **[`@tpsdev-ai/flair-client`](https://www.npmjs.com/package/@tpsdev-ai/flair-client)** | Out of process: another host, runtime or sidecar. One client per `agentId`. |
|
|
289
|
+
| **Native `/mcp`** | MCP clients against this instance. Identity from the OAuth token. |
|
|
290
|
+
| **`@tpsdev-ai/flair-mcp`** (stdio) | Local MCP clients. Talks HTTP via `FlairClient`. |
|
|
291
|
+
| **`flair` CLI** | Pairing and key generation. Needs a shell. |
|
|
292
|
+
|
|
293
|
+
**Trust-graded recall (`includeTrust`)** — provenance, author, usage, freshness, supersession — works in-process (`SemanticSearch.post({ q, includeTrust: true })`, `Memory.get(id, { includeTrust: true })`), over REST, and on the native `/mcp` handler. It is **not** exposed by the CLI, the stdio MCP server, or `FlairClient`'s typed methods.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## See also
|
|
298
|
+
|
|
299
|
+
[Integrations](integrations.md) · [Deployment](deployment.md) · [Federation](federation.md) · [Auth](auth.md) · [Architecture](../DESIGN.md)
|
package/docs/federation.md
CHANGED
|
@@ -81,15 +81,69 @@ flair federation pair https://<fabric-node>:19926/<instance-name> --token-from .
|
|
|
81
81
|
|
|
82
82
|
Replace `<fabric-node>`, `<instance-name>`, and `<hub-admin-password>` with your actual values.
|
|
83
83
|
|
|
84
|
+
Running the hub on Fabric has its own considerations — port derivation against a managed
|
|
85
|
+
`443` endpoint, why the sync driver can only be installed on a machine you control, and
|
|
86
|
+
the observability limits of a node you have no shell on. See
|
|
87
|
+
[`docs/deploying-on-fabric.md`](deploying-on-fabric.md).
|
|
88
|
+
|
|
84
89
|
## Sync
|
|
85
90
|
|
|
86
|
-
Push local changes to the hub:
|
|
91
|
+
Push local changes to the hub, once:
|
|
87
92
|
|
|
88
93
|
```bash
|
|
89
94
|
flair federation sync --admin-pass <password>
|
|
90
95
|
# Output: ✅ Synced 12 records (0 skipped) in 145ms
|
|
91
96
|
```
|
|
92
97
|
|
|
98
|
+
### Keeping it synced
|
|
99
|
+
|
|
100
|
+
`flair federation sync` is one-shot and `flair federation watch` only runs
|
|
101
|
+
while its terminal is open. Neither survives a logout, so a spoke that is only
|
|
102
|
+
ever synced by hand looks paired but stops replicating — enable the scheduled
|
|
103
|
+
driver instead:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
flair federation sync enable # every 300s by default
|
|
107
|
+
flair federation sync enable --interval 900 # or pick your own cadence
|
|
108
|
+
flair federation sync status # is anything actually driving sync?
|
|
109
|
+
flair federation sync disable
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
This installs a **periodic one-shot**: a launchd job (`StartInterval`) on
|
|
113
|
+
macOS, a systemd user timer (`OnUnitActiveSec`) on Linux, each invoking
|
|
114
|
+
`flair federation sync` on the interval. It is deliberately not a supervised
|
|
115
|
+
long-lived watcher — the sync holds no state between runs, so a resident
|
|
116
|
+
process would buy nothing, and a supervisor cannot restart a process that
|
|
117
|
+
hangs rather than exits. The trade-off is latency, and `--interval` is the
|
|
118
|
+
knob. The first sync runs immediately on enable.
|
|
119
|
+
|
|
120
|
+
`flair federation watch` is unchanged and still the right tool for an
|
|
121
|
+
interactive "watch it sync while I debug" session.
|
|
122
|
+
|
|
123
|
+
**Credentials.** The scheduler never writes a password into a unit file. It
|
|
124
|
+
stores the *path* given to `--admin-pass-file` (defaulting to
|
|
125
|
+
`~/.flair/admin-pass` when that exists) and the CLI reads the file at run time,
|
|
126
|
+
refusing it unless it is owner-only (`chmod 600`). Pass `--no-credentials` to
|
|
127
|
+
wire none at all.
|
|
128
|
+
|
|
129
|
+
### Is anything driving sync?
|
|
130
|
+
|
|
131
|
+
`flair federation status` reports the driver alongside the peer table, because
|
|
132
|
+
"no peer has merged in 24h" has two completely different causes:
|
|
133
|
+
|
|
134
|
+
| What you see | What it means | What to do |
|
|
135
|
+
|---|---|---|
|
|
136
|
+
| `Sync driver: active` | A managed driver is loaded and syncs are landing | Nothing |
|
|
137
|
+
| `Sync driver: active … but no peer contact in <window>` | Sync **is** running; the runs are not reaching the peer | `flair federation reachability`, then the driver log |
|
|
138
|
+
| `Sync driver: NONE` | Nothing has run sync since you paired | `flair federation sync enable` |
|
|
139
|
+
| `Sync driver: INSTALLED BUT NOT LOADED` | Unit files exist, the service manager never loaded them | `flair federation sync enable` |
|
|
140
|
+
| `Sync driver: none managed by Flair — but syncs are landing` | A cron entry / hand-written unit is driving it | Nothing |
|
|
141
|
+
|
|
142
|
+
The check is local to the machine running the CLI, so it is omitted when
|
|
143
|
+
`--target` points at a remote instance.
|
|
144
|
+
|
|
145
|
+
Driver logs: `~/.flair/logs/federation-sync.{stdout,stderr}.log`.
|
|
146
|
+
|
|
93
147
|
## Security
|
|
94
148
|
|
|
95
149
|
### Signed requests
|
|
@@ -125,10 +179,13 @@ Records with `updatedAt` more than 5 minutes in the future are rejected. This pr
|
|
|
125
179
|
|
|
126
180
|
| Command | Description |
|
|
127
181
|
|---------|-------------|
|
|
128
|
-
| `flair federation status` | Show instance identity
|
|
182
|
+
| `flair federation status` | Show instance identity, peer connections, and whether anything is driving sync |
|
|
129
183
|
| `flair federation pair <hub-url> --token-from <file>` | Pair this spoke with a hub using a token triple file (or `-` for stdin) |
|
|
130
184
|
| `flair federation sync` | Push local changes to the hub (one-shot) |
|
|
131
|
-
| `flair federation
|
|
185
|
+
| `flair federation sync enable [--interval <s>] [--admin-pass-file <path>]` | Install the scheduled sync driver (launchd on macOS, systemd timer on Linux) |
|
|
186
|
+
| `flair federation sync disable [--remove-shim]` | Remove the scheduled sync driver |
|
|
187
|
+
| `flair federation sync status` | Show whether the driver is installed and genuinely active |
|
|
188
|
+
| `flair federation watch [--interval <s>]` | Run sync in a foreground loop for an interactive session (default 30s) |
|
|
132
189
|
| `flair federation reachability` | Probe local instance + each paired peer (read-only) |
|
|
133
190
|
| `flair federation token [--ttl <min>]` | Generate a one-time pairing token triple (hub only) |
|
|
134
191
|
|
|
@@ -174,6 +231,6 @@ flair federation unpin <instanceId>
|
|
|
174
231
|
## Limitations (1.0)
|
|
175
232
|
|
|
176
233
|
- **HTTP push only** — no persistent WebSocket connections or real-time sync
|
|
177
|
-
- **
|
|
234
|
+
- **Polled sync** — `flair federation sync enable` schedules a periodic one-shot (launchd / systemd, default 300s); there is no write-path trigger, so a new memory replicates on the next tick rather than immediately
|
|
178
235
|
- **Single hub** — spoke-to-spoke sync goes through the hub
|
|
179
236
|
- **Record-level LWW** — not field-level; concurrent edits to different fields of the same record may lose data
|
package/docs/integrations.md
CHANGED
|
@@ -24,6 +24,8 @@ Where Flair already runs. Each integration shown here is a working surface — t
|
|
|
24
24
|
|
|
25
25
|
Don't see your harness? If it speaks **MCP** — Flair already works with `flair-mcp`. If it has a **custom memory protocol** like LangGraph's `BaseStore` or CrewAI's `RAGStorage`, an adapter is a ~200-line package; [open an issue](https://github.com/tpsdev-ai/flair/issues) or [send a PR](https://github.com/tpsdev-ai/flair).
|
|
26
26
|
|
|
27
|
+
**Already running on Harper?** Every surface above reaches Flair over HTTP. If your application is itself a Harper app, you can skip the network entirely — load Flair as a component of the same instance and call its resources in-process. See [embedding-in-a-harper-app.md](embedding-in-a-harper-app.md), which also covers the table-vs-resource distinction that decides whether your memories are scoped.
|
|
28
|
+
|
|
27
29
|
**Adjacent: memory bridges** — for moving memories between Flair and another memory product. Five bridges ship today (Mem0, ChatGPT exports, claude-project files, agentic-stack, markdown); see [bridges.md](bridges.md). Bridges are import/export plumbing, not live orchestrator integrations.
|
|
28
30
|
|
|
29
31
|
---
|
|
@@ -191,6 +193,7 @@ If it has a custom memory protocol, the adapter pattern is small (~200 lines). L
|
|
|
191
193
|
## See also
|
|
192
194
|
|
|
193
195
|
- [Quickstart](quickstart.md) — `flair init` to working memory in 30 seconds
|
|
196
|
+
- [Embedding in a Harper app](embedding-in-a-harper-app.md) — run Flair as a component of your own Harper instance and call it in-process
|
|
194
197
|
- [Memory bridges](bridges.md) — import/export Flair ↔ Mem0, ChatGPT, claude-project, markdown, agentic-stack (five bridges shipped)
|
|
195
198
|
- [Federation](federation.md) — pair instances peer-to-peer for cross-machine sync
|
|
196
199
|
- [Supply-chain policy](supply-chain-policy.md) — what we do to keep this list of integrations safe
|
package/docs/mcp-clients.md
CHANGED
|
@@ -38,7 +38,9 @@ Flair runs as a local server at `http://127.0.0.1:19926` by default. The MCP ser
|
|
|
38
38
|
|
|
39
39
|
Pick whichever you use. The MCP server is the same package; only the config syntax differs.
|
|
40
40
|
|
|
41
|
-
> **Pin the version.**
|
|
41
|
+
> **Pin the version.** An unpinned `@tpsdev-ai/flair-mcp` re-resolves to whatever is currently published on *every* agent session, so any future publish reaches your machine silently, with no lockfile and no review step in the path. `flair init` wires clients to a **pinned** spec on purpose, and every MCP-server config snippet below is written the same way: `@tpsdev-ai/flair-mcp@<version>`.
|
|
42
|
+
>
|
|
43
|
+
> Replace `<version>` with the version you intend to run — the one you already have is `flair --version` — and bump it deliberately. Leaving the literal `<version>` in place fails loudly at `npx`, which is the intended failure: better than a config that looks pinned and isn't. `flair init` is the easier path and fills this in for you.
|
|
42
44
|
|
|
43
45
|
### Claude Code
|
|
44
46
|
|
|
@@ -47,14 +49,14 @@ The canonical approach is the `claude mcp add` CLI (writes to `~/.claude.json`):
|
|
|
47
49
|
```bash
|
|
48
50
|
claude mcp add flair --scope user \
|
|
49
51
|
-e FLAIR_AGENT_ID=my-project \
|
|
50
|
-
-- npx -y @tpsdev-ai/flair-mcp
|
|
52
|
+
-- npx -y @tpsdev-ai/flair-mcp@<version>
|
|
51
53
|
```
|
|
52
54
|
|
|
53
55
|
Verify:
|
|
54
56
|
|
|
55
57
|
```bash
|
|
56
58
|
claude mcp list
|
|
57
|
-
# → flair (stdio, npx -y @tpsdev-ai/flair-mcp)
|
|
59
|
+
# → flair (stdio, npx -y @tpsdev-ai/flair-mcp@<version>)
|
|
58
60
|
```
|
|
59
61
|
|
|
60
62
|
Or, if you prefer the project-scoped `.mcp.json` checked into your repo:
|
|
@@ -64,7 +66,7 @@ Or, if you prefer the project-scoped `.mcp.json` checked into your repo:
|
|
|
64
66
|
"mcpServers": {
|
|
65
67
|
"flair": {
|
|
66
68
|
"command": "npx",
|
|
67
|
-
"args": ["-y", "@tpsdev-ai/flair-mcp"],
|
|
69
|
+
"args": ["-y", "@tpsdev-ai/flair-mcp@<version>"],
|
|
68
70
|
"env": {
|
|
69
71
|
"FLAIR_AGENT_ID": "my-project"
|
|
70
72
|
}
|
|
@@ -116,7 +118,14 @@ Or wire it by hand — add a `SessionStart` hook to `~/.claude/settings.json`:
|
|
|
116
118
|
}
|
|
117
119
|
```
|
|
118
120
|
|
|
119
|
-
Swap `me` for your `FLAIR_AGENT_ID`.
|
|
121
|
+
Swap `me` for your `FLAIR_AGENT_ID`. Unlike the MCP-server snippets above, this
|
|
122
|
+
one is shown **unpinned**, because that is what `flair hook install` writes and
|
|
123
|
+
what the tooling recognises: `flair hook status` matches the exact unpinned
|
|
124
|
+
command, so a hand-pinned hook reports `wired: false` there (while `flair
|
|
125
|
+
doctor` still sees it). Pinning this line is therefore not yet supported —
|
|
126
|
+
prefer `flair hook install`.
|
|
127
|
+
|
|
128
|
+
The hook reads Claude Code's SessionStart
|
|
120
129
|
payload on stdin, calls Flair's `bootstrap` (soul + relevant memories +
|
|
121
130
|
predicted context, scoped to your project by the session's working directory),
|
|
122
131
|
and emits it as `hookSpecificOutput.additionalContext` — which Claude Code
|
|
@@ -143,7 +152,7 @@ Edit `~/.gemini/settings.json` (create it if absent):
|
|
|
143
152
|
"mcpServers": {
|
|
144
153
|
"flair": {
|
|
145
154
|
"command": "npx",
|
|
146
|
-
"args": ["-y", "@tpsdev-ai/flair-mcp"],
|
|
155
|
+
"args": ["-y", "@tpsdev-ai/flair-mcp@<version>"],
|
|
147
156
|
"env": {
|
|
148
157
|
"FLAIR_AGENT_ID": "my-project"
|
|
149
158
|
}
|
|
@@ -165,7 +174,7 @@ Edit `~/.codex/config.toml` (create it if absent):
|
|
|
165
174
|
```toml
|
|
166
175
|
[mcp_servers.flair]
|
|
167
176
|
command = "npx"
|
|
168
|
-
args = ["-y", "@tpsdev-ai/flair-mcp"]
|
|
177
|
+
args = ["-y", "@tpsdev-ai/flair-mcp@<version>"]
|
|
169
178
|
|
|
170
179
|
[mcp_servers.flair.env]
|
|
171
180
|
FLAIR_AGENT_ID = "my-project"
|