@tpsdev-ai/flair 0.31.1 → 0.33.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 +80 -54
- package/SECURITY.md +7 -0
- package/config.yaml +34 -0
- package/dist/cli.js +349 -117
- package/dist/resources/Memory.js +24 -2
- package/dist/resources/in-process-api.js +386 -0
- package/dist/resources/mcp-tools.js +40 -0
- package/docs/deployment-shapes.md +35 -0
- package/docs/deployment.md +2 -2
- package/docs/embedding-in-a-harper-app.md +175 -75
- package/docs/hosted-on-fabric.md +203 -0
- package/docs/mcp-clients.md +4 -2
- package/docs/quickstart.md +29 -4
- package/docs/secrets-and-keys.md +4 -4
- package/docs/standalone-local.md +243 -0
- package/docs/the-team.md +8 -4
- package/docs/troubleshooting.md +1 -1
- package/docs/upgrade.md +6 -2
- package/package.json +7 -1
|
@@ -18,88 +18,91 @@ Embedding *adds* the in-process path; `rest: true` keeps serving MCP clients and
|
|
|
18
18
|
|
|
19
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
20
|
|
|
21
|
-
**2.
|
|
21
|
+
**2. Import the facade and write a memory.**
|
|
22
22
|
|
|
23
23
|
```javascript
|
|
24
24
|
import { server } from "harper";
|
|
25
|
-
|
|
26
|
-
// by hand. They are the whole in-process contract.
|
|
27
|
-
import { agentContext, internalContext, collectionResource } from "@tpsdev-ai/flair/dist/resources/in-process.js";
|
|
25
|
+
import { Flair } from "@tpsdev-ai/flair";
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// Keys carry NO leading slash: get("Memory"), never get("/Memory").
|
|
32
|
-
const flair = (path) => server.resources.get(path).Resource;
|
|
27
|
+
const flair = new Flair(server);
|
|
28
|
+
const planner = flair.as("planner");
|
|
33
29
|
|
|
34
|
-
|
|
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
|
-
}
|
|
30
|
+
await planner.memory.write("deploy runs at 0200 UTC", { durability: "standard" });
|
|
44
31
|
```
|
|
45
32
|
|
|
46
|
-
|
|
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
|
-
> They do still need the **context**. Only the collection binding is unnecessary for a read, never the identity — `Cls.search(query)` with the second argument left off resolves to the trusted `internal` verdict when it runs outside a request scope (a boot hook, a timer, a queue worker, a detached promise) and returns every agent's private records. On that path the resource's `allow*` gate is not consulted at all. Pass the context to every call, read and write.
|
|
59
|
-
|
|
60
|
-
> ### ⚠️ A resource with no context is an administrator
|
|
61
|
-
>
|
|
62
|
-
> 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.
|
|
63
|
-
>
|
|
64
|
-
> Measured, not inferred: a context-less `Memory.search()` returns every agent's `private` records, and so does a context-less `SemanticSearch`.
|
|
65
|
-
>
|
|
66
|
-
> Correct for Flair's own maintenance passes. In your app it is a data leak you find months later.
|
|
67
|
-
>
|
|
68
|
-
> **Make `agentId` a required argument, as above.** Never export a version that defaults it.
|
|
33
|
+
That is the whole API. No deep import, no `server.resources`, no `.Resource`, no `collectionResource()`, no double-passing `agentId`. The facade stamps `agentId` from the handle's context onto the body internally — you pass it once, at `flair.as(id)`.
|
|
69
34
|
|
|
70
35
|
**3. Read it back, scoped to that agent.**
|
|
71
36
|
|
|
72
37
|
```javascript
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return h.post({ q: query, limit });
|
|
76
|
-
}
|
|
38
|
+
const hits = await planner.recall("deploy schedule", { limit: 5 });
|
|
39
|
+
const record = await planner.memory.get(hits[0].id);
|
|
77
40
|
```
|
|
78
41
|
|
|
79
|
-
**4.
|
|
42
|
+
**4. Register an agent, no CLI.**
|
|
80
43
|
|
|
81
44
|
```javascript
|
|
82
|
-
await
|
|
83
|
-
|
|
45
|
+
await flair.admin.registerAgent("planner", { publicKey: "pending" });
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
> **`flair.admin` is a root shell.** Every call site is greppable via `git grep "flair.admin"`. Use for provisioning and maintenance only, never as a request handler's default.
|
|
49
|
+
|
|
50
|
+
**5. Verify it worked.**
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
console.log(await planner.memory.search({ limit: 10 }));
|
|
84
54
|
console.log([...server.resources.keys()].sort()); // what Flair registered
|
|
85
55
|
```
|
|
86
56
|
|
|
87
|
-
|
|
88
|
-
>
|
|
89
|
-
> 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.
|
|
90
|
-
>
|
|
91
|
-
> | Claim | Result |
|
|
92
|
-
> |---|---|
|
|
93
|
-
> | `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 |
|
|
94
|
-
> | `.Resource` is Flair's resource, not the raw table | Confirmed: prototype chain `Memory → Memory → Resource`, and it is **not** `databases.flair.Memory` |
|
|
95
|
-
> | Key format | **No leading slash.** `get("Memory")` hits; `get("/Memory")` returns `undefined` |
|
|
96
|
-
> | `getMatch` | `getMatch("Memory")` hits. **`getMatch("/Memory")` misses** — do not use the slashed form |
|
|
97
|
-
> | 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 |
|
|
98
|
-
> | 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 |
|
|
99
|
-
> | Cross-agent by-id read | `Memory.get(<beta's private id>)` as alpha returns **404**, never 403 — a denied caller cannot enumerate ids |
|
|
100
|
-
> | Context-less call | Unfiltered across all agents, via both `search` and `SemanticSearch` (see the warning above) |
|
|
57
|
+
---
|
|
101
58
|
|
|
102
|
-
|
|
59
|
+
## The facade
|
|
60
|
+
|
|
61
|
+
### `new Flair(server)`
|
|
62
|
+
|
|
63
|
+
One handle per Harper instance. Resolves resources lazily on first use — no lookup at construction time.
|
|
64
|
+
|
|
65
|
+
**The handle owns nothing.** It holds a reference to the Harper server the caller already owns and acquires no timers, connections, or file handles. There is no `close()` or `dispose()` method. If a future version acquires something releasable, that is a breaking change and will be versioned as one.
|
|
66
|
+
|
|
67
|
+
### `flair.as(agentId)`
|
|
68
|
+
|
|
69
|
+
Returns an `AgentHandle` scoped to that agent. The `agentId` is runtime-validated: missing, empty, blank, or non-string throws `InProcessContextError`.
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
const planner = flair.as("planner");
|
|
73
|
+
planner.agentId; // "planner"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Security:** In-process identity is asserted, not verified — co-location IS the grant. Build the `agentId` from your own server-side state, never from request data. If an agent id can reach `flair.as()` 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**.
|
|
77
|
+
|
|
78
|
+
### `AgentHandle`
|
|
79
|
+
|
|
80
|
+
| Method | Description |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `handle.memory.write(content, opts?)` | Write a memory as this agent. `agentId` is stamped from the handle — the caller never passes it. |
|
|
83
|
+
| `handle.memory.get(id)` | Read a memory by id, scoped to this agent. |
|
|
84
|
+
| `handle.memory.search(opts?)` | Search memories scoped to this agent. |
|
|
85
|
+
| `handle.recall(query, opts?)` | Semantic search scoped to this agent. |
|
|
86
|
+
|
|
87
|
+
### `flair.admin`
|
|
88
|
+
|
|
89
|
+
Admin operations — unfiltered reads, cross-agent writes. Every call site is greppable via `git grep "flair.admin"`.
|
|
90
|
+
|
|
91
|
+
**The handle is cached** — `flair.admin === flair.admin` is `true`. Access it once and reuse the reference, or access it inline; either is fine.
|
|
92
|
+
|
|
93
|
+
| Method | Description |
|
|
94
|
+
|---|---|
|
|
95
|
+
| `flair.admin.registerAgent(id, opts?)` | Register an agent through the Agent resource (full Principal shape). |
|
|
96
|
+
| `flair.admin.memory.get(id)` | Read any memory by id, unfiltered. |
|
|
97
|
+
| `flair.admin.memory.write(asAgentId, content, opts?)` | Write a memory attributed to another agent. |
|
|
98
|
+
|
|
99
|
+
### `flair.internal`
|
|
100
|
+
|
|
101
|
+
Trusted, unattributed, unfiltered operations — Flair's `internal` verdict. Every call site is greppable via `git grep "flair.internal"`.
|
|
102
|
+
|
|
103
|
+
| Method | Description |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `flair.internal.agentTable.put(record)` | Write directly to the Agent resource (bypasses admin gate). |
|
|
103
106
|
|
|
104
107
|
---
|
|
105
108
|
|
|
@@ -109,7 +112,8 @@ Handlers return a `Response` for `401`/`403`/`400` rather than throwing — chec
|
|
|
109
112
|
|
|
110
113
|
```javascript
|
|
111
114
|
for (const id of ["planner", "researcher", "reviewer"]) {
|
|
112
|
-
|
|
115
|
+
const agent = flair.as(id);
|
|
116
|
+
await agent.memory.write(`${id} came online`);
|
|
113
117
|
}
|
|
114
118
|
```
|
|
115
119
|
|
|
@@ -117,18 +121,14 @@ for (const id of ["planner", "researcher", "reviewer"]) {
|
|
|
117
121
|
|
|
118
122
|
### Registering agents, no CLI
|
|
119
123
|
|
|
120
|
-
Go through
|
|
124
|
+
Go through `flair.admin.registerAgent()` — it goes through the `Agent` **resource**, which fills in the whole Principal shape for you:
|
|
121
125
|
|
|
122
126
|
```javascript
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
runtime: "headless",
|
|
129
|
-
...(admin ? { admin: true } : {}), // sets role:"admin" too — see below
|
|
130
|
-
});
|
|
131
|
-
}
|
|
127
|
+
await flair.admin.registerAgent("researcher", {
|
|
128
|
+
publicKey: "pending",
|
|
129
|
+
displayName: "Research Agent",
|
|
130
|
+
admin: false,
|
|
131
|
+
});
|
|
132
132
|
```
|
|
133
133
|
|
|
134
134
|
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.
|
|
@@ -146,7 +146,7 @@ import { generateKeyPairSync } from "node:crypto";
|
|
|
146
146
|
|
|
147
147
|
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
148
148
|
const raw = publicKey.export({ format: "der", type: "spki" }).subarray(-32);
|
|
149
|
-
await registerAgent("remote-worker", { publicKey: raw.toString("hex") });
|
|
149
|
+
await flair.admin.registerAgent("remote-worker", { publicKey: raw.toString("hex") });
|
|
150
150
|
// keep `privateKey` in your own secret store — Flair never sees it
|
|
151
151
|
```
|
|
152
152
|
|
|
@@ -298,6 +298,106 @@ A `SyncLog` row reads `direction: "pull"` — the receiver's label for a push it
|
|
|
298
298
|
|
|
299
299
|
---
|
|
300
300
|
|
|
301
|
+
## Appendix: the primitives layer
|
|
302
|
+
|
|
303
|
+
The facade wraps a lower-level API that is still available for callers who need direct access. Import it from `@tpsdev-ai/flair/server`:
|
|
304
|
+
|
|
305
|
+
```javascript
|
|
306
|
+
import { agentContext, adminContext, internalContext, collectionResource } from "@tpsdev-ai/flair/server";
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
This is the same seam Flair's own MCP handler and internal tooling use. **You should not need it for ordinary agent operations** — the facade covers those. Reach for the primitives when you are building your own abstraction on top of Flair's resources, or when you need the context helpers (`agentContext`, `adminContext`, `internalContext`) to pass into a resource call directly.
|
|
310
|
+
|
|
311
|
+
### Resolving a resource
|
|
312
|
+
|
|
313
|
+
```javascript
|
|
314
|
+
// The RESOURCE — carries auth, scoping, visibility, embedding.
|
|
315
|
+
// NOT databases.flair.Memory: that is the raw table, and enforces none of it.
|
|
316
|
+
// Keys carry NO leading slash: get("Memory"), never get("/Memory").
|
|
317
|
+
const flair = (path) => server.resources.get(path).Resource;
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Writing a memory (primitives)
|
|
321
|
+
|
|
322
|
+
```javascript
|
|
323
|
+
export async function remember(agentId, content, opts = {}) {
|
|
324
|
+
// A create needs a COLLECTION-bound instance. `new Cls(...)` does not give
|
|
325
|
+
// you one, and cannot be made to — see the note below.
|
|
326
|
+
const h = await collectionResource(flair("Memory"), agentContext(agentId));
|
|
327
|
+
return h.post({
|
|
328
|
+
agentId, // required — an absent one is never filled in
|
|
329
|
+
content,
|
|
330
|
+
durability: opts.durability ?? "standard",
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
> ### Why `collectionResource`, and not `new Memory()`
|
|
336
|
+
>
|
|
337
|
+
> 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:
|
|
338
|
+
>
|
|
339
|
+
> ```javascript
|
|
340
|
+
> const h = new (flair("Memory"))(undefined, agentContext(agentId));
|
|
341
|
+
> h.isCollection = true; // TypeError: Cannot set property isCollection ... which has only a getter
|
|
342
|
+
> h.post({ ... }); // without the line above: 405 "The Memory does not have a post method implemented"
|
|
343
|
+
> ```
|
|
344
|
+
>
|
|
345
|
+
> `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.
|
|
346
|
+
>
|
|
347
|
+
> They do still need the **context**. Only the collection binding is unnecessary for a read, never the identity — `Cls.search(query)` with the second argument left off resolves to the trusted `internal` verdict when it runs outside a request scope (a boot hook, a timer, a queue worker, a detached promise) and returns every agent's private records. On that path the resource's `allow*` gate is not consulted at all. Pass the context to every call, read and write.
|
|
348
|
+
|
|
349
|
+
> ### ⚠️ A resource with no context is an administrator
|
|
350
|
+
>
|
|
351
|
+
> 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.
|
|
352
|
+
>
|
|
353
|
+
> Measured, not inferred: a context-less `Memory.search()` returns every agent's `private` records, and so does a context-less `SemanticSearch`.
|
|
354
|
+
>
|
|
355
|
+
> Correct for Flair's own maintenance passes. In your app it is a data leak you find months later.
|
|
356
|
+
>
|
|
357
|
+
> **Make `agentId` a required argument, as above.** Never export a version that defaults it.
|
|
358
|
+
|
|
359
|
+
### Reading back (primitives)
|
|
360
|
+
|
|
361
|
+
```javascript
|
|
362
|
+
export async function recall(agentId, query, limit = 5) {
|
|
363
|
+
const h = await collectionResource(flair("SemanticSearch"), agentContext(agentId));
|
|
364
|
+
return h.post({ q: query, limit });
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### Registering agents (primitives)
|
|
369
|
+
|
|
370
|
+
```javascript
|
|
371
|
+
export async function registerAgent(id, { publicKey = "pending", admin = false } = {}) {
|
|
372
|
+
const h = await collectionResource(flair("Agent"), internalContext()); // provisioning is infrastructure, not an agent's write
|
|
373
|
+
return h.post({
|
|
374
|
+
id, name: id, displayName: id,
|
|
375
|
+
publicKey, // a placeholder is fine — see below
|
|
376
|
+
runtime: "headless",
|
|
377
|
+
...(admin ? { admin: true } : {}), // sets role:"admin" too — see below
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### What we measured, so you do not have to
|
|
383
|
+
|
|
384
|
+
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.
|
|
385
|
+
|
|
386
|
+
| Claim | Result |
|
|
387
|
+
|---|---|
|
|
388
|
+
| `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 |
|
|
389
|
+
| `.Resource` is Flair's resource, not the raw table | Confirmed: prototype chain `Memory → Memory → Resource`, and it is **not** `databases.flair.Memory` |
|
|
390
|
+
| Key format | **No leading slash.** `get("Memory")` hits; `get("/Memory")` returns `undefined` |
|
|
391
|
+
| `getMatch` | `getMatch("Memory")` hits. **`getMatch("/Memory")` misses** — do not use the slashed form |
|
|
392
|
+
| 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 |
|
|
393
|
+
| 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 |
|
|
394
|
+
| Cross-agent by-id read | `Memory.get(<beta's private id>)` as alpha returns **404**, never 403 — a denied caller cannot enumerate ids |
|
|
395
|
+
| Context-less call | Unfiltered across all agents, via both `search` and `SemanticSearch` (see the warning above) |
|
|
396
|
+
|
|
397
|
+
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`.
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
301
401
|
## See also
|
|
302
402
|
|
|
303
403
|
[Integrations](integrations.md) · [Deployment](deployment.md) · [Federation](federation.md) · [Auth](auth.md) · [Architecture](../DESIGN.md)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Hosted on Harper Fabric
|
|
2
|
+
|
|
3
|
+
Deploy Flair as a component to a [Harper Fabric](https://www.harperdb.io/) instance. You do not run the Harper process yourself: managed hosting, multi-region replication, no shell on the node.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Deploy
|
|
8
|
+
|
|
9
|
+
You **deploy** rather than install. `flair deploy` pushes Flair as a Fabric component:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
export FABRIC_USER=<admin> FABRIC_PASSWORD=<pass>
|
|
13
|
+
|
|
14
|
+
# Validate args and package layout without deploying
|
|
15
|
+
flair deploy --fabric-org <org> --fabric-cluster <cluster> --dry-run
|
|
16
|
+
|
|
17
|
+
# Deploy
|
|
18
|
+
flair deploy --fabric-org <org> --fabric-cluster <cluster>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Credentials go via the environment, not argv, so they stay out of `ps`. Use `--fabric-password-file <path>` (mode `0600`) when scripting; inline `--fabric-user`/`--fabric-password` flags leak to shell history and are discouraged.
|
|
22
|
+
|
|
23
|
+
Target defaults to `https://<cluster>.<org>.harperfabric.com`; override with `--target`. Deploy verifies the served API, waits for replication, and polls for convergence before reporting success.
|
|
24
|
+
|
|
25
|
+
> `--fabric-token` is accepted but **fails** — `deploy_component` is Basic-auth only.
|
|
26
|
+
|
|
27
|
+
### Provision the instance
|
|
28
|
+
|
|
29
|
+
Run **once**, before serving traffic:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
flair init --target https://<cluster>.<org>.harperfabric.com \
|
|
33
|
+
--ops-target <ops-url> \
|
|
34
|
+
--cluster-admin-user <user> --cluster-admin-pass <pass> \
|
|
35
|
+
--remote --force
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- `--force` is required — this writes to a live instance.
|
|
39
|
+
- `--remote` marks it a federation **hub** and creates the `flair_pair_initiator` role; without it, pairing later fails role-not-found.
|
|
40
|
+
- Generated admin password lands in `~/.tps/secrets/flair-fabric-hdb` (mode `0600`); `--flair-admin-pass` to choose your own.
|
|
41
|
+
|
|
42
|
+
### Port derivation trap
|
|
43
|
+
|
|
44
|
+
Locally, Flair serves data on `19926` and the ops API on `19925`. The CLI derives **ops = data − 1** everywhere. A managed endpoint is HTTPS on 443 with no port, so derivation produces `:442` — where nothing answers.
|
|
45
|
+
|
|
46
|
+
**Pass `--ops-target <url>` explicitly** (or set `FLAIR_OPS_TARGET`) on any command that touches the ops API: `init --target`, `agent add --target`, `federation token --target`.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
On Fabric, configuration goes through the component's environment, not a local `config.yaml`. Set these in the Fabric component env:
|
|
53
|
+
|
|
54
|
+
| Variable | What it does | When to set it |
|
|
55
|
+
|----------|--------------|----------------|
|
|
56
|
+
| `FLAIR_PUBLIC_URL` | The URL operators reach this Flair on. Surfaced in OAuth metadata and A2A discovery. | **Always set** — or clients see a loopback address. |
|
|
57
|
+
| `HDB_ADMIN_PASSWORD` | Bootstrap password for the embedded Harper. | Set at install time. |
|
|
58
|
+
| `FLAIR_KEY_PASSPHRASE` | Passphrase for federation key encryption. | Set for production federation deployments. |
|
|
59
|
+
|
|
60
|
+
On Fabric / managed deploys, environment variables are provisioned through Harper's Fabric secrets mechanism (encrypted at rest with `enc:v1:` storage format).
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Agent authentication
|
|
65
|
+
|
|
66
|
+
Agents authenticate with **Ed25519 per-agent keys** — the same model as standalone local. Each agent holds a private key and signs every request.
|
|
67
|
+
|
|
68
|
+
### Register an agent
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Register an agent — --ops-target is required (see Port derivation trap above)
|
|
72
|
+
flair agent add mybot --target "$FLAIR_URL" --ops-target <ops-url>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The private key is stored on the **client machine** at `~/.flair/keys/<agent>.key`, not on the Fabric node. The Fabric node stores only the public key in the `Agent` table.
|
|
76
|
+
|
|
77
|
+
### Connect a client
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
export FLAIR_URL=https://<cluster>.<org>.harperfabric.com
|
|
81
|
+
|
|
82
|
+
# Register an agent
|
|
83
|
+
flair agent add mybot --target "$FLAIR_URL" --ops-target <ops-url>
|
|
84
|
+
|
|
85
|
+
# Use with any MCP client — set FLAIR_AGENT_ID and FLAIR_URL in the client env
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Auth is the same protocol as standalone: Ed25519 signature of `agentId:timestamp:nonce:METHOD:/path`, 30-second replay window, nonce deduplication. The difference is purely the transport — HTTPS instead of localhost HTTP.
|
|
89
|
+
|
|
90
|
+
See [secrets-and-keys.md](secrets-and-keys.md) for the full threat model.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Verify it works
|
|
95
|
+
|
|
96
|
+
### Health and status
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
curl -sf https://<cluster>.<org>.harperfabric.com/Health
|
|
100
|
+
|
|
101
|
+
flair status --target https://<cluster>.<org>.harperfabric.com
|
|
102
|
+
flair fleet verify --target https://<cluster>.<org>.harperfabric.com
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`fleet verify` checks health, auth, and version across the origin node plus every Flair federation peer on file. Exit codes: 0 = all verified, 1 = origin failed, 2 = peer version skew, 3 = peer unverifiable.
|
|
106
|
+
|
|
107
|
+
> **A credential mismatch renders as an empty section.** `flair status` reads `/HealthDetail` with `FLAIR_ADMIN_PASS` / `HDB_ADMIN_PASSWORD` / a pinned agent key — **not** the `FABRIC_*` credentials. On failure it renders blank.
|
|
108
|
+
|
|
109
|
+
### What is available remotely
|
|
110
|
+
|
|
111
|
+
| Command | Works remotely |
|
|
112
|
+
|---|---|
|
|
113
|
+
| `GET /Health` | Yes — public, no auth |
|
|
114
|
+
| `flair status --target <url>` | Yes — subsystem rollups |
|
|
115
|
+
| `flair quality --target <url>` | Yes — recall/coverage metrics |
|
|
116
|
+
| `flair fleet verify --target <url>` | Yes — origin + Flair peers |
|
|
117
|
+
| `flair federation status\|verify\|reachability --target <url>` | Yes — peer table, sync recency |
|
|
118
|
+
|
|
119
|
+
### What does **not** work remotely
|
|
120
|
+
|
|
121
|
+
**`flair doctor`** takes no `--target` — it hardcodes localhost, reads a local PID file, and shells out to `lsof`. Unavailable too: `start`, `stop`, `restart`, `snapshot`, `reembed`, `rem`, `bridge`.
|
|
122
|
+
|
|
123
|
+
**Fabric's own cluster topology is invisible.** `fleet verify` sweeps *Flair's* federation peer table, not Harper's cluster nodes. `cluster_status` is harper-pro-only. `0 peers known` means "0 on file", never "0 exist."
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Upgrade
|
|
128
|
+
|
|
129
|
+
A Fabric-deployed Flair is a component, not an npm package. Upgrade in place:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
FABRIC_USER=<admin> FABRIC_PASSWORD=<pass> \
|
|
133
|
+
flair upgrade --target https://<cluster>.<org>.harperfabric.com
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
This resolves the target version, stages a clean deployable with the required `@harperfast/harper` version pin, confirms the staged build before deploying, pushes it via `flair deploy`, and verifies the result. After a successful deploy, it runs a fleet convergence sweep across the origin plus every Flair federation peer.
|
|
137
|
+
|
|
138
|
+
- `--check` shows the version diff and plan without deploying.
|
|
139
|
+
- `--yes` skips the confirmation prompt for scripted use.
|
|
140
|
+
- `--fabric-password-file <path>` reads the password from a file instead of an env var.
|
|
141
|
+
- `--no-fleet-verify` skips the post-deploy fleet sweep.
|
|
142
|
+
|
|
143
|
+
Inline `--fabric-user`/`--fabric-password` flags also work but are **discouraged** — both leak to shell history and `ps`.
|
|
144
|
+
|
|
145
|
+
### Backup before upgrading
|
|
146
|
+
|
|
147
|
+
`flair snapshot` is local-only. Back up before every upgrade:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
flair backup --url https://<cluster>.<org>.harperfabric.com \
|
|
151
|
+
--admin-pass-file <path> --output ./flair-backup.json
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
See [upgrade.md](upgrade.md#upgrading-a-fabric-deployed-instance) for the full walkthrough.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Federation
|
|
159
|
+
|
|
160
|
+
Available. Pair a local spoke to a Fabric-hosted hub:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
# On any machine (no shell on the hub) — generate a pairing token triple
|
|
164
|
+
FLAIR_ADMIN_PASS=<hub-admin-password> flair federation token \
|
|
165
|
+
--target https://<cluster>.<org>.harperfabric.com \
|
|
166
|
+
--ops-target <ops-url> > ./pair-triple.json
|
|
167
|
+
|
|
168
|
+
# On the spoke — pair to the Fabric hub
|
|
169
|
+
flair federation pair https://<cluster>.<org>.harperfabric.com \
|
|
170
|
+
--token-from ./pair-triple.json
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Pairing limitation
|
|
174
|
+
|
|
175
|
+
The scheduled sync driver (`flair federation sync enable`) writes a launchd job or systemd timer **on the machine running the CLI** — it cannot be installed on a Fabric node. A periodic one-shot from the spoke machine is the workaround.
|
|
176
|
+
|
|
177
|
+
Full walkthrough: [federation.md](federation.md).
|
|
178
|
+
|
|
179
|
+
### Multi-region replication
|
|
180
|
+
|
|
181
|
+
Fabric gives you N regional nodes running one component — **not** N Flair instances. Every node shares one Flair identity (the `Instance` table replicates). You do **not** federate your own regions to each other — Harper replication handles that. Use `flair federation pair` only to reach a **separate** Flair instance.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Known operational limitations
|
|
186
|
+
|
|
187
|
+
### No disk or quota telemetry
|
|
188
|
+
|
|
189
|
+
`flair status` reports usage for two directories: no free space, no total, no quota. An instance can hit its quota with nothing saying so. The one indirect signal is a migration halting for space.
|
|
190
|
+
|
|
191
|
+
### Unbounded npm cache
|
|
192
|
+
|
|
193
|
+
Every deploy runs a server-side `npm install` using the node's default cache. npm never evicts it, so it grows until it fills the quota. There is no cache flag, alternate location, or cleanup option. [flair#886](https://github.com/tpsdev-ai/flair/issues/886).
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## See also
|
|
198
|
+
|
|
199
|
+
- [deployment-shapes.md](deployment-shapes.md) — choose your shape
|
|
200
|
+
- [upgrade.md](upgrade.md#upgrading-a-fabric-deployed-instance) — full Fabric upgrade walkthrough
|
|
201
|
+
- [federation.md](federation.md) — pairing, sync driver, conflict resolution
|
|
202
|
+
- [standalone-local.md](standalone-local.md) — the standalone shape (different upgrade, shell available)
|
|
203
|
+
- [secrets-and-keys.md](secrets-and-keys.md) — admin password, key lifecycle
|
package/docs/mcp-clients.md
CHANGED
|
@@ -203,7 +203,7 @@ Eleven tools, kept deliberately small:
|
|
|
203
203
|
| Tool | What it does |
|
|
204
204
|
|---|---|
|
|
205
205
|
| `memory_search` | Semantic search across your agent's memories |
|
|
206
|
-
| `memory_store` | Save a memory with type, durability, tags. Auto-dedups near-duplicates |
|
|
206
|
+
| `memory_store` | Save a memory with type, durability, tags, visibility. Auto-dedups near-duplicates |
|
|
207
207
|
| `memory_update` | Update an existing memory by ID — overwrite in place, or version it with `preserveHistory` |
|
|
208
208
|
| `memory_get` | Fetch a specific memory by ID |
|
|
209
209
|
| `memory_delete` | Remove a memory |
|
|
@@ -214,7 +214,9 @@ Eleven tools, kept deliberately small:
|
|
|
214
214
|
| `flair_workspace_set` | Set your agent's current workspace state (ref/branch, phase, task) in the Office Space |
|
|
215
215
|
| `flair_orgevent` | Publish an org-wide coordination event (claim/release/status) to the Office Space |
|
|
216
216
|
|
|
217
|
-
Writes are scoped per-agent (your `FLAIR_AGENT_ID`) and enforced by Flair's server, not by client convention — you can't write as another agent. Reads are more open by design: any agent on the same Flair instance can read any other agent's non-private memories (open-within-org read; see [SECURITY.md](../SECURITY.md)).
|
|
217
|
+
Writes are scoped per-agent (your `FLAIR_AGENT_ID`) and enforced by Flair's server, not by client convention — you can't write as another agent. Reads are more open by design: any agent on the same Flair instance can read any other agent's **non-private** memories, with no grant to set up (open-within-org read; see [SECURITY.md](../SECURITY.md)).
|
|
218
|
+
|
|
219
|
+
Which memories are non-private is decided at write time, and the default is not "shared". `memory_store` defaults `durability` to `standard`, and the server derives visibility from durability — `permanent`/`persistent` → `shared`, `standard`/`ephemeral` → `private` — so **a bare `memory_store` call writes an owner-only memory that no other agent can read.** Pass `visibility: "shared"` (or `"private"`, to be explicit) to say what you mean; the tool reports the visibility the write actually landed on so an agent can confirm it rather than assume.
|
|
218
220
|
|
|
219
221
|
---
|
|
220
222
|
|
package/docs/quickstart.md
CHANGED
|
@@ -26,7 +26,9 @@ export PATH="$HOME/.npm-global/bin:$PATH" # add this to ~/.zshrc or ~/.bashrc
|
|
|
26
26
|
npm install -g @tpsdev-ai/flair
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
One install gives you
|
|
29
|
+
One install gives you one command: `flair`.
|
|
30
|
+
|
|
31
|
+
The stdio adapter your MCP client talks to is a separate package, `@tpsdev-ai/flair-mcp`, and it is deliberately not installed globally — `flair init` wires each client to fetch it on demand with `npx -y @tpsdev-ai/flair-mcp@<version>`, so there is no second global package to keep in step. (The server also has its own `/mcp` endpoint built in, but it is off by default — it registers no route unless `FLAIR_MCP_OAUTH` and a public issuer are set — and no client setup in this guide uses it.) `@tpsdev-ai/flair-client` is a separate package you add to your own project when you want to call Flair from code.
|
|
30
32
|
|
|
31
33
|
## 2. Bootstrap Flair and register an agent
|
|
32
34
|
|
|
@@ -110,12 +112,35 @@ flair memory add --agent local "Harper v5 sandbox blocks node:module but process
|
|
|
110
112
|
{
|
|
111
113
|
"id": "local-1785277247486",
|
|
112
114
|
"written": true,
|
|
113
|
-
"deduplicated": false
|
|
115
|
+
"deduplicated": false,
|
|
116
|
+
"visibility": "private"
|
|
114
117
|
}
|
|
115
118
|
```
|
|
116
119
|
|
|
117
120
|
Flair embedded the text locally on write. No network calls.
|
|
118
121
|
|
|
122
|
+
### Who can read it
|
|
123
|
+
|
|
124
|
+
`visibility: private` means **only `local` can read this memory** — no other agent on the instance can search it, fetch it by id, or receive it in a bootstrap.
|
|
125
|
+
|
|
126
|
+
You didn't ask for that, and it isn't a setting you have to remember. Flair derives the default from the memory's **durability**, because how long a memory is meant to last is a good proxy for who it was meant for:
|
|
127
|
+
|
|
128
|
+
| Durability | Default visibility |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `permanent`, `persistent` | `shared` — a fact or decision worth keeping is worth the team being able to find |
|
|
131
|
+
| `standard`, `ephemeral` — including a bare write with no `--durability` | `private` — working context and scratch state belong to the agent that produced them |
|
|
132
|
+
|
|
133
|
+
So sharing is a deliberate act, and it takes one flag:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
flair memory add --agent local --visibility shared \
|
|
137
|
+
"Release tags are cut from main, never from a release branch"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`--visibility` takes exactly `private` or `shared` — a value it doesn't recognise is an error, not a guess — and overrides the durability rule in both directions. The `visibility` field in the response is the value the memory actually landed on, so read it rather than assuming.
|
|
141
|
+
|
|
142
|
+
Once a memory is `shared`, **every** agent on this instance can read it, with no grant to set up. That is the shipped model: reads open within one instance, closed at the federation edge. Full picture in [SECURITY.md](../SECURITY.md).
|
|
143
|
+
|
|
119
144
|
## 5. Find it back by meaning
|
|
120
145
|
|
|
121
146
|
```bash
|
|
@@ -131,7 +156,7 @@ You searched for a concept, not the keywords. The line under each hit is its cre
|
|
|
131
156
|
|
|
132
157
|
> The percentage is a **rank-fusion score, not a similarity**. It is normalized so the top result is always near 100%. Read it as ordering within these results, never as confidence that the match is good.
|
|
133
158
|
|
|
134
|
-
Add `--explain` to see the ranking inputs, or `--limit`, `--tag`, `--since 7d` to narrow the search. `flair memory search` runs the same query but always prints raw JSON — use it when piping to a script.
|
|
159
|
+
Add `--explain` to see the ranking inputs per hit — the raw score, the composite score under `--scoring composite`, and the record's durability, age and usage count. When output is JSON (`--json`, or any time stdout is not a terminal) the same breakdown arrives as an `_explain` object on each hit, so scripts get it too. Use `--limit`, `--tag`, `--since 7d` to narrow the search. `flair memory search` runs the same query but always prints raw JSON — use it when piping to a script.
|
|
135
160
|
|
|
136
161
|
## 6. Give your agent context on boot
|
|
137
162
|
|
|
@@ -160,7 +185,7 @@ With the MCP server wired up — `flair init` does this automatically for every
|
|
|
160
185
|
| You want to... | Go to |
|
|
161
186
|
|----------------|-------|
|
|
162
187
|
| Add more agents to the same instance | `flair agent add <id>` |
|
|
163
|
-
|
|
|
188
|
+
| Share a memory with your other agents | `flair memory add --visibility shared` — a bare write lands `private`, see [step 4](#who-can-read-it); a shared one is readable by every agent on the instance, no grant needed ([auth.md](auth.md)) |
|
|
164
189
|
| Import memories from agentic-stack / Mem0 / etc. | [bridges.md](bridges.md) |
|
|
165
190
|
| Sync memories across machines | [federation.md](federation.md) |
|
|
166
191
|
| Integrate with OpenClaw, Claude Code, Cursor | [README.md#integration](../README.md#integration) |
|
package/docs/secrets-and-keys.md
CHANGED
|
@@ -7,11 +7,11 @@ Flair owns **identity**. Flair does **not** own arbitrary secrets. This page dra
|
|
|
7
7
|
For each registered agent, Flair stores:
|
|
8
8
|
|
|
9
9
|
- A **public key** in the `Agent` table (server-side; used to verify signed requests).
|
|
10
|
-
- A **private key** at `~/.flair/keys/<agent>.key` on the host that owns that agent
|
|
10
|
+
- A **private key** at `~/.flair/keys/<agent>.key` on the host that owns that agent — the raw 32-byte Ed25519 seed. Created by `flair init --agent <id>` or `flair agent add <id>`. Mode `0600`.
|
|
11
11
|
|
|
12
12
|
Agents sign every request to Flair with this key. Flair refuses unsigned requests and refuses signatures that don't match the registered public key. The signed payload is `<agentId>:<timestamp>:<nonce>:<METHOD>:<path>` with a 30-second replay window and nonce dedup — replays inside that window are rejected.
|
|
13
13
|
|
|
14
|
-
**This is the only secret material Flair manages.** Lose the key file and the agent is locked out (`flair agent rotate <id>` to issue a new pair).
|
|
14
|
+
**This is the only secret material Flair manages.** Lose the key file and the agent is locked out (`flair agent rotate-key <id>` to issue a new pair).
|
|
15
15
|
|
|
16
16
|
## Flair admin password (Harper instance)
|
|
17
17
|
- If not provided via `--admin-pass`, `--admin-pass-file`, `FLAIR_ADMIN_PASS`, or `HDB_ADMIN_PASSWORD`, a random password is generated and written to `~/.flair/admin-pass` (mode `0o600`). The password is **not** printed to the console.
|
|
@@ -140,7 +140,7 @@ Hermes uses `~/.hermes/.env` for provider API keys (managed by `hermes auth`). T
|
|
|
140
140
|
|
|
141
141
|
- **Stays on the host that owns the agent.** If your agent runs on a given host, the key lives on that host. If you spin up the same agent on another machine, **don't copy the key** — register a new agent identity (`flair agent add <id>-on-<other-host>`) on that machine. Different identities, same Flair instance can store memories for both, you decide cross-agent visibility.
|
|
142
142
|
- **`chmod 600` enforced** by `flair agent add`. Don't relax it.
|
|
143
|
-
- **Don't check it into git.** `.gitignore` should already exclude `~/.flair/keys/`; if you're ever tempted to share keys for "convenience," rotate first (`flair agent rotate <id>`).
|
|
143
|
+
- **Don't check it into git.** `.gitignore` should already exclude `~/.flair/keys/`; if you're ever tempted to share keys for "convenience," rotate first (`flair agent rotate-key <id>`).
|
|
144
144
|
- **Backup separately**, encrypted. The `flair backup` command excludes private keys by default. Roll your own backup of `~/.flair/keys/` via age-encrypted archive if you want offsite recovery.
|
|
145
145
|
|
|
146
146
|
## What about a `flair secret` CLI?
|
|
@@ -153,7 +153,7 @@ If you find yourself wanting one anyway, your agent can call `security find-gene
|
|
|
153
153
|
|
|
154
154
|
| Asset | Owned by | If compromised → |
|
|
155
155
|
|---|---|---|
|
|
156
|
-
| Flair agent private key (`~/.flair/keys/<agent>.key`) | Flair (you, on the host) | Attacker can **write** memories under that agent's identity and read that agent's **`private`**-marked memories until you rotate. Use `flair agent rotate <id>`. Other agents' write identity is unaffected — they can't be impersonated with this key. |
|
|
156
|
+
| Flair agent private key (`~/.flair/keys/<agent>.key`) | Flair (you, on the host) | Attacker can **write** memories under that agent's identity and read that agent's **`private`**-marked memories until you rotate. Use `flair agent rotate-key <id>`. Other agents' write identity is unaffected — they can't be impersonated with this key. |
|
|
157
157
|
| LLM provider API keys (Anthropic, OpenAI, etc.) | OS keyring / 1Password | Standard provider revocation: rotate the key in the provider's console, update keyring entry. |
|
|
158
158
|
| Cross-host secrets (1Password vault, age-sops) | The secret manager itself | Trust falls back to that manager's MFA / key handling. Document recovery in your team's ops runbook. |
|
|
159
159
|
| Memory contents | Flair (server-side) | Write access requires the owning agent's key → see "Per-agent write isolation, org-wide non-private read" below. |
|