@tpsdev-ai/flair 0.31.1 → 0.32.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 +32 -6
- package/dist/cli.js +124 -40
- package/dist/resources/in-process-api.js +382 -0
- package/docs/deployment-shapes.md +35 -0
- package/docs/deployment.md +2 -2
- package/docs/embedding-in-a-harper-app.md +171 -76
- package/docs/hosted-on-fabric.md +203 -0
- package/docs/secrets-and-keys.md +4 -4
- package/docs/standalone-local.md +243 -0
- package/docs/upgrade.md +6 -2
- package/package.json +7 -1
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ─── Public in-process API (flair#956) ───────────────────────────────────────
|
|
3
|
+
*
|
|
4
|
+
* The facade that hides four internal implementation details a Harper engineer
|
|
5
|
+
* should never have to learn:
|
|
6
|
+
*
|
|
7
|
+
* 1. A deep import path into our dist/
|
|
8
|
+
* 2. That server.resources is keyed by REST path with no leading slash
|
|
9
|
+
* 3. That the registry entry wraps the class in .Resource
|
|
10
|
+
* 4. That creates need collectionResource() while reads do not
|
|
11
|
+
*
|
|
12
|
+
* And collapses the agentId double-pass (context + body) into one.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { Flair } from "@tpsdev-ai/flair";
|
|
16
|
+
* const flair = new Flair(server);
|
|
17
|
+
* const planner = flair.as("planner");
|
|
18
|
+
* await planner.memory.write("deploy runs at 0200 UTC");
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* The facade does NOT hide the security boundary. In-process identity is
|
|
22
|
+
* asserted, not verified — co-location IS the grant. flair.as(id) requires a
|
|
23
|
+
* non-empty id (runtime throw). flair.admin and flair.internal are separate,
|
|
24
|
+
* greppable properties. The docs say plainly: build the context from your own
|
|
25
|
+
* server-side state, never from request data.
|
|
26
|
+
*
|
|
27
|
+
* ── Internal implementation ─────────────────────────────────────────────────
|
|
28
|
+
* Every operation delegates to the existing primitives in ./in-process.js
|
|
29
|
+
* (agentContext, adminContext, internalContext, collectionResource). The facade
|
|
30
|
+
* is additive — existing code using the raw seam continues to work.
|
|
31
|
+
*/
|
|
32
|
+
import { agentContext, adminContext, internalContext, collectionResource, InProcessContextError, } from "./in-process.js";
|
|
33
|
+
// ─── Re-export for the "./server" entry point ────────────────────────────────
|
|
34
|
+
export { agentContext, adminContext, internalContext, collectionResource, InProcessContextError };
|
|
35
|
+
// ─── Resource resolution ─────────────────────────────────────────────────────
|
|
36
|
+
/**
|
|
37
|
+
* Resolve a Flair resource class from the Harper server registry.
|
|
38
|
+
* Throws with a helpful message listing available resources if not found.
|
|
39
|
+
*/
|
|
40
|
+
function resolveResource(server, name) {
|
|
41
|
+
const entry = server.resources.get?.(name) ?? server.resources.getMatch?.(name);
|
|
42
|
+
if (!entry?.Resource) {
|
|
43
|
+
const keys = [...server.resources.keys()].sort();
|
|
44
|
+
const available = keys.length > 0 ? keys.join(", ") : "(none)";
|
|
45
|
+
throw new Error(`Flair is not loaded in this Harper instance.\n` +
|
|
46
|
+
`The '${name}' resource was not found in the registry.\n` +
|
|
47
|
+
`Available: [${available}]\n` +
|
|
48
|
+
`Make sure @tpsdev-ai/flair is installed as a component of this instance.`);
|
|
49
|
+
}
|
|
50
|
+
return entry.Resource;
|
|
51
|
+
}
|
|
52
|
+
// ─── AgentHandle ─────────────────────────────────────────────────────────────
|
|
53
|
+
/**
|
|
54
|
+
* A handle that carries agent identity and scopes every operation to that agent.
|
|
55
|
+
*
|
|
56
|
+
* Returned by {@link Flair.as}. The agentId is validated at construction time
|
|
57
|
+
* (runtime, not types) — missing, empty, blank, or non-string throws
|
|
58
|
+
* {@link InProcessContextError}.
|
|
59
|
+
*
|
|
60
|
+
* **Security:** in-process identity is asserted, not verified. Build the
|
|
61
|
+
* agentId from your own server-side state, never from request data.
|
|
62
|
+
*/
|
|
63
|
+
export class AgentHandle {
|
|
64
|
+
agentId;
|
|
65
|
+
#server;
|
|
66
|
+
#ctx;
|
|
67
|
+
constructor(server, agentId) {
|
|
68
|
+
this.#server = server;
|
|
69
|
+
this.agentId = agentId;
|
|
70
|
+
// Throws InProcessContextError on missing/empty/blank id — see
|
|
71
|
+
// resources/in-process.ts's safety-design block for why this is
|
|
72
|
+
// not merely defensive.
|
|
73
|
+
this.#ctx = agentContext(agentId);
|
|
74
|
+
}
|
|
75
|
+
/** Memory operations scoped to this agent. */
|
|
76
|
+
get memory() {
|
|
77
|
+
return new AgentMemory(this.#server, this.#ctx, this.agentId);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Semantic search scoped to this agent.
|
|
81
|
+
*
|
|
82
|
+
* ```ts
|
|
83
|
+
* const hits = await planner.recall("deploy schedule", { limit: 5 });
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
async recall(query, opts) {
|
|
87
|
+
const Cls = resolveResource(this.#server, "SemanticSearch");
|
|
88
|
+
const h = new Cls(undefined, this.#ctx);
|
|
89
|
+
const body = { q: query, limit: opts?.limit ?? 5 };
|
|
90
|
+
if (opts?.includeTrust === true)
|
|
91
|
+
body.includeTrust = true;
|
|
92
|
+
if (opts?.abstain === true)
|
|
93
|
+
body.abstain = true;
|
|
94
|
+
if (opts?.scoring)
|
|
95
|
+
body.scoring = opts.scoring;
|
|
96
|
+
if (opts?.minScore !== undefined)
|
|
97
|
+
body.minScore = opts.minScore;
|
|
98
|
+
if (opts?.since)
|
|
99
|
+
body.since = opts.since;
|
|
100
|
+
if (opts?.asOf)
|
|
101
|
+
body.asOf = opts.asOf;
|
|
102
|
+
if (opts?.tag)
|
|
103
|
+
body.tag = opts.tag;
|
|
104
|
+
if (opts?.subject)
|
|
105
|
+
body.subject = opts.subject;
|
|
106
|
+
if (opts?.subjects)
|
|
107
|
+
body.subjects = opts.subjects;
|
|
108
|
+
return unwrap(await h.post(body));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// ─── AgentMemory (per-agent memory operations) ───────────────────────────────
|
|
112
|
+
class AgentMemory {
|
|
113
|
+
#server;
|
|
114
|
+
#ctx;
|
|
115
|
+
#agentId;
|
|
116
|
+
constructor(server, ctx, agentId) {
|
|
117
|
+
this.#server = server;
|
|
118
|
+
this.#ctx = ctx;
|
|
119
|
+
this.#agentId = agentId;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Write a memory as this agent.
|
|
123
|
+
*
|
|
124
|
+
* The agentId is stamped from the handle's context — the caller never
|
|
125
|
+
* passes it, and any agentId in opts is overwritten. This collapses the
|
|
126
|
+
* double-pass (context + body) into one.
|
|
127
|
+
*/
|
|
128
|
+
async write(content, opts) {
|
|
129
|
+
const Cls = resolveResource(this.#server, "Memory");
|
|
130
|
+
const h = await collectionResource(Cls, this.#ctx);
|
|
131
|
+
const body = {
|
|
132
|
+
agentId: this.#agentId,
|
|
133
|
+
content,
|
|
134
|
+
};
|
|
135
|
+
if (opts?.durability)
|
|
136
|
+
body.durability = opts.durability;
|
|
137
|
+
if (opts?.visibility)
|
|
138
|
+
body.visibility = opts.visibility;
|
|
139
|
+
if (opts?.tags)
|
|
140
|
+
body.tags = opts.tags;
|
|
141
|
+
if (opts?.type)
|
|
142
|
+
body.type = opts.type;
|
|
143
|
+
if (opts?.id)
|
|
144
|
+
body.id = opts.id;
|
|
145
|
+
return unwrap(await h.post(body));
|
|
146
|
+
}
|
|
147
|
+
/** Get a memory by id, scoped to this agent. */
|
|
148
|
+
async get(id) {
|
|
149
|
+
const Cls = resolveResource(this.#server, "Memory");
|
|
150
|
+
const h = new Cls(undefined, this.#ctx);
|
|
151
|
+
return unwrap(await h.get(id));
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Search memories scoped to this agent.
|
|
155
|
+
*
|
|
156
|
+
* Delegates to Memory.search() which applies the agent's read scope
|
|
157
|
+
* (own memories + granted owners' shared memories).
|
|
158
|
+
*/
|
|
159
|
+
async search(opts) {
|
|
160
|
+
const Cls = resolveResource(this.#server, "Memory");
|
|
161
|
+
const h = new Cls(undefined, this.#ctx);
|
|
162
|
+
const conditions = [];
|
|
163
|
+
if (opts?.tags) {
|
|
164
|
+
for (const tag of opts.tags) {
|
|
165
|
+
conditions.push({ search_attribute: "tags", search_type: "contains", search_value: tag });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (opts?.type) {
|
|
169
|
+
conditions.push({ search_attribute: "type", search_type: "equals", search_value: opts.type });
|
|
170
|
+
}
|
|
171
|
+
if (opts?.durability) {
|
|
172
|
+
conditions.push({ search_attribute: "durability", search_type: "equals", search_value: opts.durability });
|
|
173
|
+
}
|
|
174
|
+
if (opts?.visibility) {
|
|
175
|
+
conditions.push({ search_attribute: "visibility", search_type: "equals", search_value: opts.visibility });
|
|
176
|
+
}
|
|
177
|
+
const query = conditions.length > 0 ? { conditions, operator: "and" } : undefined;
|
|
178
|
+
return unwrap(await h.search(query));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// ─── AdminHandle ─────────────────────────────────────────────────────────────
|
|
182
|
+
/**
|
|
183
|
+
* Flair-admin operations — unfiltered reads, cross-agent writes.
|
|
184
|
+
*
|
|
185
|
+
* **This is a root shell.** Every call site is greppable via
|
|
186
|
+
* `git grep "flair.admin"`. Use for provisioning and maintenance only,
|
|
187
|
+
* never as a request handler's default.
|
|
188
|
+
*
|
|
189
|
+
* The admin agentId is validated at construction time (same guard as
|
|
190
|
+
* {@link AgentHandle}).
|
|
191
|
+
*/
|
|
192
|
+
export class AdminHandle {
|
|
193
|
+
agentId;
|
|
194
|
+
#server;
|
|
195
|
+
#ctx;
|
|
196
|
+
constructor(server, agentId) {
|
|
197
|
+
this.#server = server;
|
|
198
|
+
this.agentId = agentId;
|
|
199
|
+
this.#ctx = adminContext(agentId);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Register an agent through the Agent resource (full Principal shape).
|
|
203
|
+
*
|
|
204
|
+
* ```ts
|
|
205
|
+
* await flair.admin.registerAgent("planner", { publicKey: "pending" });
|
|
206
|
+
* ```
|
|
207
|
+
*/
|
|
208
|
+
async registerAgent(id, opts) {
|
|
209
|
+
const Cls = resolveResource(this.#server, "Agent");
|
|
210
|
+
const h = await collectionResource(Cls, this.#ctx);
|
|
211
|
+
const body = {
|
|
212
|
+
id,
|
|
213
|
+
name: id,
|
|
214
|
+
displayName: opts?.displayName ?? id,
|
|
215
|
+
publicKey: opts?.publicKey ?? "pending",
|
|
216
|
+
runtime: opts?.runtime ?? "headless",
|
|
217
|
+
};
|
|
218
|
+
if (opts?.admin === true)
|
|
219
|
+
body.admin = true;
|
|
220
|
+
return unwrap(await h.post(body));
|
|
221
|
+
}
|
|
222
|
+
/** Memory operations with admin authority (unfiltered reads, cross-agent writes). */
|
|
223
|
+
get memory() {
|
|
224
|
+
return new AdminMemory(this.#server, this.#ctx, this.agentId);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// ─── AdminMemory ─────────────────────────────────────────────────────────────
|
|
228
|
+
class AdminMemory {
|
|
229
|
+
#server;
|
|
230
|
+
#ctx;
|
|
231
|
+
#agentId;
|
|
232
|
+
constructor(server, ctx, agentId) {
|
|
233
|
+
this.#server = server;
|
|
234
|
+
this.#ctx = ctx;
|
|
235
|
+
this.#agentId = agentId;
|
|
236
|
+
}
|
|
237
|
+
/** Read any memory by id, unfiltered. */
|
|
238
|
+
async get(id) {
|
|
239
|
+
const Cls = resolveResource(this.#server, "Memory");
|
|
240
|
+
const h = new Cls(undefined, this.#ctx);
|
|
241
|
+
return unwrap(await h.get(id));
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Write a memory attributed to another agent.
|
|
245
|
+
*
|
|
246
|
+
* ```ts
|
|
247
|
+
* await flair.admin.memory.write("researcher", "provisioned memory", { visibility: "shared" });
|
|
248
|
+
* ```
|
|
249
|
+
*/
|
|
250
|
+
async write(asAgentId, content, opts) {
|
|
251
|
+
const Cls = resolveResource(this.#server, "Memory");
|
|
252
|
+
// Use adminContext for the acting admin, but stamp the target agentId
|
|
253
|
+
// on the body so the memory is owned by the target agent.
|
|
254
|
+
const h = await collectionResource(Cls, this.#ctx);
|
|
255
|
+
const body = {
|
|
256
|
+
agentId: asAgentId,
|
|
257
|
+
content,
|
|
258
|
+
};
|
|
259
|
+
if (opts?.durability)
|
|
260
|
+
body.durability = opts.durability;
|
|
261
|
+
if (opts?.visibility)
|
|
262
|
+
body.visibility = opts.visibility;
|
|
263
|
+
if (opts?.tags)
|
|
264
|
+
body.tags = opts.tags;
|
|
265
|
+
if (opts?.type)
|
|
266
|
+
body.type = opts.type;
|
|
267
|
+
if (opts?.id)
|
|
268
|
+
body.id = opts.id;
|
|
269
|
+
return unwrap(await h.post(body));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// ─── InternalHandle ──────────────────────────────────────────────────────────
|
|
273
|
+
/**
|
|
274
|
+
* Trusted, unattributed, unfiltered operations — Flair's `internal` verdict.
|
|
275
|
+
*
|
|
276
|
+
* Reads see every agent's private records; writes are owned by nobody.
|
|
277
|
+
* This exists for work that is genuinely infrastructure: provisioning a
|
|
278
|
+
* principal, a migration, a maintenance sweep.
|
|
279
|
+
*
|
|
280
|
+
* Every call site is greppable via `git grep "flair.internal"`.
|
|
281
|
+
*/
|
|
282
|
+
export class InternalHandle {
|
|
283
|
+
#server;
|
|
284
|
+
#ctx;
|
|
285
|
+
constructor(server) {
|
|
286
|
+
this.#server = server;
|
|
287
|
+
this.#ctx = internalContext();
|
|
288
|
+
}
|
|
289
|
+
/** Raw Agent table access for provisioning. */
|
|
290
|
+
get agentTable() {
|
|
291
|
+
return new InternalAgentTable(this.#server, this.#ctx);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// ─── InternalAgentTable ──────────────────────────────────────────────────────
|
|
295
|
+
class InternalAgentTable {
|
|
296
|
+
#server;
|
|
297
|
+
#ctx;
|
|
298
|
+
constructor(server, ctx) {
|
|
299
|
+
this.#server = server;
|
|
300
|
+
this.#ctx = ctx;
|
|
301
|
+
}
|
|
302
|
+
/** Write directly to the Agent resource (bypasses admin gate via internal context). */
|
|
303
|
+
async put(record) {
|
|
304
|
+
const Cls = resolveResource(this.#server, "Agent");
|
|
305
|
+
const h = await collectionResource(Cls, this.#ctx);
|
|
306
|
+
return unwrap(await h.post(record));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// ─── Flair (the facade) ──────────────────────────────────────────────────────
|
|
310
|
+
/**
|
|
311
|
+
* The public in-process API for Flair embedded in a Harper app.
|
|
312
|
+
*
|
|
313
|
+
* One handle per Harper instance. Resolves resources lazily on first use.
|
|
314
|
+
*
|
|
315
|
+
* ```ts
|
|
316
|
+
* import { Flair } from "@tpsdev-ai/flair";
|
|
317
|
+
* const flair = new Flair(server);
|
|
318
|
+
* const planner = flair.as("planner");
|
|
319
|
+
* await planner.memory.write("deploy runs at 0200 UTC");
|
|
320
|
+
* ```
|
|
321
|
+
*
|
|
322
|
+
* **Security:** In-process identity is asserted, not verified — co-location
|
|
323
|
+
* IS the grant. Build the agentId from your own server-side state, never
|
|
324
|
+
* from request data. `flair.as(id)` requires a non-empty id (runtime throw).
|
|
325
|
+
* `flair.admin` and `flair.internal` are separate, greppable properties for
|
|
326
|
+
* deliberate escalation.
|
|
327
|
+
*/
|
|
328
|
+
export class Flair {
|
|
329
|
+
#server;
|
|
330
|
+
constructor(server) {
|
|
331
|
+
this.#server = server;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Return a handle that acts as the given agent.
|
|
335
|
+
*
|
|
336
|
+
* The agentId is runtime-validated: missing, empty, blank, or non-string
|
|
337
|
+
* throws {@link InProcessContextError}. Build it from your own server-side
|
|
338
|
+
* state, never from request data.
|
|
339
|
+
*/
|
|
340
|
+
as(agentId) {
|
|
341
|
+
return new AgentHandle(this.#server, agentId);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Admin operations — unfiltered reads, cross-agent writes.
|
|
345
|
+
*
|
|
346
|
+
* **This is a root shell.** Every call site is greppable via
|
|
347
|
+
* `git grep "flair.admin"`. Use for provisioning and maintenance only.
|
|
348
|
+
*/
|
|
349
|
+
get admin() {
|
|
350
|
+
// AdminHandle requires an agentId for attribution. We use a sentinel
|
|
351
|
+
// that makes the admin identity visible in audit logs. The caller
|
|
352
|
+
// should use a real admin agent id when possible.
|
|
353
|
+
return new AdminHandle(this.#server, "_admin");
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Internal operations — trusted, unattributed, unfiltered.
|
|
357
|
+
*
|
|
358
|
+
* Every call site is greppable via `git grep "flair.internal"`.
|
|
359
|
+
* Use for infrastructure work only: provisioning, migrations, maintenance.
|
|
360
|
+
*/
|
|
361
|
+
get internal() {
|
|
362
|
+
return new InternalHandle(this.#server);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
366
|
+
/**
|
|
367
|
+
* Unwrap a handler return value into a plain object.
|
|
368
|
+
* Handlers may return a `Response` (the 401/403/400 guards) — surface its
|
|
369
|
+
* JSON body so the caller sees the structured error rather than an opaque object.
|
|
370
|
+
*/
|
|
371
|
+
async function unwrap(value) {
|
|
372
|
+
if (value && typeof value === "object" && typeof value.json === "function" && "status" in value) {
|
|
373
|
+
try {
|
|
374
|
+
const body = await value.json();
|
|
375
|
+
return { error: body?.error ?? "request failed", status: value.status, ...body };
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
return { error: "request failed", status: value.status };
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return value;
|
|
382
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Which Flair deployment shape are you in?
|
|
2
|
+
|
|
3
|
+
Flair runs in one of three shapes. Pick yours and follow only that path.
|
|
4
|
+
|
|
5
|
+
| You want to... | Shape | Start here |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| Run Flair on your own machine or VPS. `flair init` installs Harper, creates your agent identity, and you're running. | **Standalone local** | [standalone-local.md](standalone-local.md) |
|
|
8
|
+
| Run Flair on [Harper Fabric](https://www.harperdb.io/) — managed hosting, multi-region replication, no shell on the node. You deploy a component; agents connect over HTTPS. | **Hosted on Fabric** | [hosted-on-fabric.md](hosted-on-fabric.md) |
|
|
9
|
+
| Load Flair into a Harper instance you already run. In-process calls — no HTTP, no second process, no key to distribute. Over HTTP it is one memory API among many. | **Embedded in a Harper app** | [embedding-in-a-harper-app.md](embedding-in-a-harper-app.md) |
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## What changes across shapes
|
|
14
|
+
|
|
15
|
+
Not everything is the same. The critical differences:
|
|
16
|
+
|
|
17
|
+
### Identity
|
|
18
|
+
|
|
19
|
+
- **Standalone local & Fabric (over HTTP):** Agents authenticate with Ed25519-signed requests. Each agent holds a private key and signs `agentId:timestamp:nonce:METHOD:/path` on every request. The server verifies the signature against the agent's registered public key.
|
|
20
|
+
- **Embedded (in-process):** Identity is **asserted**, not verified. You pass the agent id via the call context — `agentContext("mybot")` — and Flair acts as that agent. No key, no signature, no `Agent`-table lookup. Co-location *is* the trust boundary: a caller inside the same Harper instance could write the storage tables directly anyway, so demanding a signature from same-process code would be theatre. **Never build the context from request data** — that is privilege escalation with no error and no trace.
|
|
21
|
+
|
|
22
|
+
### Upgrade
|
|
23
|
+
|
|
24
|
+
- **Standalone local:** `flair upgrade` — install, restart, verify, rollback-on-failure in one step.
|
|
25
|
+
- **Hosted on Fabric:** `flair upgrade --target <fabric-url>` — resolves the target version, stages a clean deployable, and pushes it. Fabric has no `flair upgrade` equivalent to the local path; redeploy the component.
|
|
26
|
+
- **Embedded:** Flair follows your host app's dependency lifecycle. When you update `@tpsdev-ai/flair` in your app's `package.json` and redeploy, Flair upgrades with the rest. There is no `flair upgrade` — it's a component, not a standalone process.
|
|
27
|
+
|
|
28
|
+
### Federation
|
|
29
|
+
|
|
30
|
+
- **Standalone local & Fabric:** Available. Hub-and-spoke sync with pairing tokens, signed requests, and originator enforcement. The CLI drives pairing (`flair federation pair`).
|
|
31
|
+
- **Embedded:** Pairing is **CLI-only** today — it requires shell access on the node ([flair#947](https://github.com/tpsdev-ai/flair/issues/947)). If your deployment has no shell, federation is not available.
|
|
32
|
+
|
|
33
|
+
### `@export`
|
|
34
|
+
|
|
35
|
+
`@export` on a GraphQL schema means **REST exposure only** — it does not mean replication. `Memory` has no `@export` and still replicates in a Harper Fabric cluster (replication is per-database, not per-export). This is a genuine and easily-inverted trap: do not assume `@export` controls sync or that the absence of `@export` means "not replicated."
|
package/docs/deployment.md
CHANGED
|
@@ -192,10 +192,10 @@ Set these in the Flair process environment (`~/Library/LaunchAgents/ai.tpsdev.fl
|
|
|
192
192
|
|
|
193
193
|
```bash
|
|
194
194
|
# Backup all data (agents, memories, souls)
|
|
195
|
-
flair backup
|
|
195
|
+
flair backup --output ~/flair-backup-$(date +%Y%m%d).json --admin-pass-file ~/.flair/admin-pass
|
|
196
196
|
|
|
197
197
|
# Restore to a fresh instance
|
|
198
|
-
flair restore
|
|
198
|
+
flair restore ~/flair-backup-20260405.json
|
|
199
199
|
```
|
|
200
200
|
|
|
201
201
|
Always backup before upgrades.
|