@tpsdev-ai/flair 0.29.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.
Files changed (117) hide show
  1. package/README.md +194 -377
  2. package/dist/cli-shim.cjs +1 -1
  3. package/dist/cli.js +1567 -366
  4. package/dist/deploy.js +241 -43
  5. package/dist/fabric-upgrade.js +77 -24
  6. package/dist/federation/scheduler.js +500 -0
  7. package/dist/fleet-verify.js +3 -3
  8. package/dist/install/clients.js +111 -53
  9. package/dist/lib/mcp-enable.js +2 -2
  10. package/dist/lib/mcp-spec.js +128 -0
  11. package/dist/lib/safe-snapshot-extract.js +231 -0
  12. package/dist/lib/scheduler-platform.js +128 -0
  13. package/dist/lib/xml-escape.js +54 -0
  14. package/dist/rem/scheduler.js +212 -55
  15. package/dist/rem/snapshot.js +13 -0
  16. package/dist/replication-convergence.js +505 -0
  17. package/dist/resources/A2AAdapter.js +1 -1
  18. package/dist/resources/Admin.js +1 -1
  19. package/dist/resources/AdminConnectors.js +1 -1
  20. package/dist/resources/AdminDashboard.js +1 -1
  21. package/dist/resources/AdminIdp.js +1 -1
  22. package/dist/resources/AdminInstance.js +1 -1
  23. package/dist/resources/AdminMemory.js +1 -1
  24. package/dist/resources/AdminPrincipals.js +1 -1
  25. package/dist/resources/Agent.js +1 -1
  26. package/dist/resources/AgentCard.js +1 -1
  27. package/dist/resources/AgentSeed.js +1 -1
  28. package/dist/resources/AttentionQuery.js +1 -1
  29. package/dist/resources/Credential.js +1 -1
  30. package/dist/resources/Federation.js +1 -1
  31. package/dist/resources/Instance.js +1 -1
  32. package/dist/resources/Integration.js +1 -1
  33. package/dist/resources/MCPClientMetadata.js +1 -1
  34. package/dist/resources/Memory.js +1 -1
  35. package/dist/resources/MemoryBootstrap.js +8 -9
  36. package/dist/resources/MemoryCandidate.js +168 -0
  37. package/dist/resources/MemoryConsolidate.js +1 -1
  38. package/dist/resources/MemoryDedupStats.js +1 -1
  39. package/dist/resources/MemoryFeed.js +1 -1
  40. package/dist/resources/MemoryGrant.js +1 -1
  41. package/dist/resources/MemoryMaintenance.js +1 -1
  42. package/dist/resources/MemoryReflect.js +2 -2
  43. package/dist/resources/MemoryReindex.js +1 -1
  44. package/dist/resources/MemoryUsage.js +1 -1
  45. package/dist/resources/OAuth.js +1 -1
  46. package/dist/resources/OAuthClient.js +1 -1
  47. package/dist/resources/OrgEvent.js +1 -1
  48. package/dist/resources/OrgEventCatchup.js +1 -1
  49. package/dist/resources/OrgEventMaintenance.js +1 -1
  50. package/dist/resources/PairingToken.js +1 -1
  51. package/dist/resources/Peer.js +1 -1
  52. package/dist/resources/Presence.js +3 -3
  53. package/dist/resources/RecordUsage.js +1 -1
  54. package/dist/resources/Relationship.js +1 -1
  55. package/dist/resources/SemanticSearch.js +18 -46
  56. package/dist/resources/SkillScan.js +1 -1
  57. package/dist/resources/Soul.js +1 -1
  58. package/dist/resources/SoulFeed.js +1 -1
  59. package/dist/resources/WorkspaceLatest.js +1 -1
  60. package/dist/resources/WorkspaceState.js +1 -1
  61. package/dist/resources/XAA.js +1 -1
  62. package/dist/resources/a2a-url.js +1 -1
  63. package/dist/resources/abstention.js +1 -1
  64. package/dist/resources/agent-auth.js +1 -1
  65. package/dist/resources/agentcard-fields.js +1 -1
  66. package/dist/resources/auth-middleware.js +1 -1
  67. package/dist/resources/embeddings-boot.js +14 -16
  68. package/dist/resources/embeddings-provider.js +15 -12
  69. package/dist/resources/federation-cleanup.js +5 -5
  70. package/dist/resources/federation-nonce-store.js +5 -5
  71. package/dist/resources/health.js +53 -25
  72. package/dist/resources/in-process.js +225 -0
  73. package/dist/resources/instance-identity.js +1 -1
  74. package/dist/resources/mcp-client-metadata-fields.js +1 -1
  75. package/dist/resources/mcp-handler.js +1 -1
  76. package/dist/resources/mcp-oauth.js +2 -2
  77. package/dist/resources/mcp-tools.js +23 -17
  78. package/dist/resources/memory-read-scope.js +1 -1
  79. package/dist/resources/memory-reflect-lib.js +3 -3
  80. package/dist/resources/memory-visibility.js +3 -3
  81. package/dist/resources/migration-boot.js +81 -11
  82. package/dist/resources/migrations/data-dir.js +205 -0
  83. package/dist/resources/migrations/embedding-stamp.js +3 -3
  84. package/dist/resources/migrations/graph-heal.js +2 -2
  85. package/dist/resources/migrations/ledger.js +1 -1
  86. package/dist/resources/migrations/progress.js +33 -0
  87. package/dist/resources/migrations/registry.js +14 -5
  88. package/dist/resources/migrations/runner.js +93 -13
  89. package/dist/resources/migrations/state.js +13 -2
  90. package/dist/resources/migrations/synthetic-test-migration.js +1 -1
  91. package/dist/resources/migrations/visibility-backfill.js +232 -0
  92. package/dist/resources/models-dir.js +18 -9
  93. package/dist/resources/record-types.js +28 -0
  94. package/dist/resources/semantic-retrieval-core.js +6 -5
  95. package/dist/resources/usage-recording.js +1 -1
  96. package/dist/src/lib/scheduler-platform.js +128 -0
  97. package/dist/src/lib/xml-escape.js +54 -0
  98. package/dist/src/rem/scheduler.js +397 -0
  99. package/docs/deploying-on-fabric.md +267 -0
  100. package/docs/deployment.md +6 -0
  101. package/docs/embedding-in-a-harper-app.md +299 -0
  102. package/docs/federation.md +61 -4
  103. package/docs/integrations.md +3 -0
  104. package/docs/mcp-clients.md +16 -7
  105. package/docs/quickstart.md +80 -54
  106. package/docs/releasing.md +72 -38
  107. package/docs/supply-chain-policy.md +40 -4
  108. package/docs/troubleshooting.md +24 -0
  109. package/docs/upgrade.md +100 -5
  110. package/package.json +4 -14
  111. package/schemas/memory.graphql +9 -1
  112. package/templates/bin/flair-federation-sync.sh.tmpl +28 -0
  113. package/templates/launchd/dev.flair.federation.sync.plist.tmpl +47 -0
  114. package/templates/systemd/flair-federation-sync.service.tmpl +21 -0
  115. package/templates/systemd/flair-federation-sync.timer.tmpl +16 -0
  116. package/dist/resources/rerank-provider.js +0 -569
  117. package/docs/rerank-provisioning.md +0 -101
@@ -0,0 +1,267 @@
1
+ # Deploying on Harper Fabric
2
+
3
+ Run Flair as a component on [Harper Fabric](https://www.harperdb.io/) — managed, hosted,
4
+ multi-region — instead of a local process you own. The hosted counterpart to
5
+ [deployment.md](deployment.md); two things define it: you *deploy* rather than install,
6
+ and **you get no shell on the node.**
7
+
8
+ ## When to use it
9
+
10
+ | | Standalone local | Harper Fabric |
11
+ |---|---|---|
12
+ | You run | `flair init` | `flair deploy` |
13
+ | Shell on the node | yes | **no** |
14
+ | Multi-region / failover | no | yes |
15
+ | Upgrade | `flair upgrade` | `flair upgrade --target <url>` |
16
+
17
+ Choose Fabric for an always-on hub in more than one region. For a single instance on a VPS
18
+ you already have, [Remote Server](../README.md#remote-server) is simpler and keeps your shell.
19
+
20
+ ---
21
+
22
+ ## Quickstart
23
+
24
+ ### 1. Deploy the component
25
+
26
+ ```bash
27
+ export FABRIC_USER=<admin> FABRIC_PASSWORD=<pass>
28
+
29
+ # Validate args and package layout without deploying
30
+ flair deploy --fabric-org <org> --fabric-cluster <cluster> --dry-run
31
+
32
+ flair deploy --fabric-org <org> --fabric-cluster <cluster>
33
+ ```
34
+
35
+ Target defaults to `https://<cluster>.<org>.harperfabric.com`; override with `--target`.
36
+
37
+ Credentials go via the environment, not argv, so they stay out of `ps`. Use
38
+ `--fabric-password-file <path>` (mode `0600`) when scripting; inline flags leak to history.
39
+
40
+ Deploy verifies the served API (Harper reports success for a component serving
41
+ nothing; `--no-verify` opts out), waits 600s for replication (Harper's 120s default
42
+ aborts mid-replicate on Fabric), and polls for convergence before reporting one.
43
+
44
+ > `--fabric-token` is accepted but **fails** — `deploy_component` is Basic-auth only.
45
+
46
+ ### 2. Provision
47
+
48
+ ```bash
49
+ flair init --target https://<cluster>.<org>.harperfabric.com \
50
+ --ops-target <ops-url> \
51
+ --cluster-admin-user <user> --cluster-admin-pass <pass> \
52
+ --remote --force
53
+ ```
54
+
55
+ - `--force` is required — this writes to a live instance.
56
+ - `--remote` marks it a federation **hub** and creates the `flair_pair_initiator` role;
57
+ without it, pairing later fails role-not-found.
58
+ - Generated admin password lands in `~/.tps/secrets/flair-fabric-hdb` (mode `0600`);
59
+ `--flair-admin-pass` to choose your own.
60
+
61
+ Run **once**, before serving multi-region traffic — see
62
+ [Multi-region](#multi-region-replication-vs-federation).
63
+
64
+ ### 3. Verify
65
+
66
+ ```bash
67
+ curl -sf https://<cluster>.<org>.harperfabric.com/Health
68
+ flair fleet verify --target https://<cluster>.<org>.harperfabric.com
69
+ flair status --target https://<cluster>.<org>.harperfabric.com
70
+ ```
71
+
72
+ ### 4. Connect a client
73
+
74
+ ```bash
75
+ export FLAIR_URL=https://<cluster>.<org>.harperfabric.com
76
+
77
+ # Register an agent — --ops-target is required, see Ports below
78
+ flair agent add mybot --target "$FLAIR_URL" --ops-target <ops-url>
79
+ ```
80
+
81
+ Also set `FLAIR_PUBLIC_URL` to that URL in the component's environment — OAuth metadata
82
+ and A2A discovery advertise it, or clients see a loopback address.
83
+
84
+ ---
85
+
86
+ ## Ports: the derivation trap
87
+
88
+ Locally, Flair serves data on `19926` and the ops API on `19925`. The CLI derives
89
+ **ops = data − 1** everywhere.
90
+
91
+ A managed endpoint is HTTPS on 443 with no port, so derivation produces **`:442`** —
92
+ where nothing answers:
93
+
94
+ ```bash
95
+ # --target https://<cluster>.<org>.harperfabric.com → ops derived as :442 ✗
96
+ # --target https://<fabric-node>:19926/<instance> → ops derived as :19925 ✓
97
+ ```
98
+
99
+ **Pass `--ops-target <url>` explicitly** (or set `FLAIR_OPS_TARGET`) on any command that
100
+ touches the ops API: `init --target`, `agent add --target`, `federation token --target`.
101
+
102
+ > **Gap — needs a Fabric account to verify.** This guide does not state the correct
103
+ > ops-API URL for a managed `*.harperfabric.com` instance, or whether the ops API is
104
+ > reachable remotely there at all. The derivation above is certain (read from source);
105
+ > the right value to pass is not.
106
+
107
+ Precedence: `--target` > `--url` > `FLAIR_TARGET` > `FLAIR_URL` > localhost. For ops:
108
+ `--ops-target` > `FLAIR_OPS_TARGET` > derived > localhost.
109
+
110
+ ## Auth
111
+
112
+ Flair ships `authorizeLocal: false` — it only governs loopback, so it changes nothing
113
+ here; remote callers always need real credentials.
114
+
115
+ - Admin ops use Basic auth; agents use Ed25519 ([auth.md](auth.md)).
116
+ - `/Health` is public — your liveness check.
117
+ - Don't debug with raw `curl` — on Fabric the auth gate fires before the resource
118
+ handler, so a hand-rolled request 401s in a way that looks like a Flair bug and isn't.
119
+
120
+ ---
121
+
122
+ ## Pairing a spoke to a hosted hub
123
+
124
+ No shell on the hub, so mint the token remotely rather than over `ssh`:
125
+
126
+ ```bash
127
+ # On any machine — note --ops-target, this hits the ops API
128
+ FLAIR_ADMIN_PASS=<hub-admin-password> flair federation token \
129
+ --target https://<cluster>.<org>.harperfabric.com \
130
+ --ops-target <ops-url> > ./pair-triple.json
131
+
132
+ # On the spoke
133
+ flair federation pair https://<cluster>.<org>.harperfabric.com \
134
+ --token-from ./pair-triple.json
135
+ ```
136
+
137
+ `federation token` takes `--admin-pass`, **not** `--admin-pass-file`. The triple holds a
138
+ one-time credential — delete it after pairing (60-min TTL, `--ttl`).
139
+
140
+ ### Federation is push-only
141
+
142
+ **A spoke pushes up. It cannot pull down.** `POST /FederationSync` is one-directional per
143
+ call and there is no pull endpoint anywhere.
144
+
145
+ For both directions, each side pairs **as a spoke of the other**: two pairings, two
146
+ tokens, two syncs. No setting makes sync bidirectional.
147
+
148
+ Syncs `Memory`, `Soul`, `Agent`, `Relationship`. **`Presence` is not federated** — no
149
+ cross-spoke roster.
150
+
151
+ ### Keeping a spoke synced
152
+
153
+ `flair federation sync` is one-shot; `watch` dies with its terminal. Use the scheduled
154
+ driver ([federation.md](federation.md#keeping-it-synced)):
155
+
156
+ ```bash
157
+ flair federation sync enable --interval 300
158
+ flair federation sync status
159
+ ```
160
+
161
+ > **The driver is local-only.** It writes a launchd job or systemd timer **on the machine
162
+ > running the CLI**; `--target` just points that local job at a remote instance. You
163
+ > cannot install a driver on a Fabric node.
164
+
165
+ A periodic one-shot, not a supervised watcher; `--interval` is the latency knob.
166
+ `flair federation status` omits its driver verdict for a remote `--target`.
167
+
168
+ ---
169
+
170
+ ## Multi-region: replication vs federation
171
+
172
+ Fabric gives you N regional nodes running one component — **not** N Flair instances.
173
+ Instance identity lives in the `Instance` table (`schemas/federation.graphql`); it is a
174
+ Harper table, so **it replicates** — every node shares one Flair identity.
175
+
176
+ | Layer | Handles | Scope |
177
+ |---|---|---|
178
+ | Harper replication | your app across its own regions | intra-app, automatic |
179
+ | Flair federation | this app ↔ another Flair instance | instance-to-instance, you pair it |
180
+
181
+ **You do not federate your own regions to each other** — Harper already did. Use
182
+ `flair federation pair` only to reach a *separate* Flair instance.
183
+
184
+ One asymmetry: identity replicates, but `flair-instance.yaml` (port config, per data
185
+ directory) does not. A node reporting a different **port** is expected; a different
186
+ **instance id** is not.
187
+
188
+ Provision once, from one place, before serving multi-region traffic: step 2's
189
+ `flair init … --remote` seeds the identity row so it exists before any node needs it.
190
+
191
+ ---
192
+
193
+ ## Operating it
194
+
195
+ ### What works remotely
196
+
197
+ | Command | Gives you |
198
+ |---|---|
199
+ | `GET /Health` | liveness, no auth |
200
+ | `flair status --target <url>` | subsystem rollups; the only byte counts available remotely |
201
+ | `flair quality --target <url>` | recall/coverage; halted-migration reasons |
202
+ | `flair fleet verify --target <url>` | health, auth, version across origin + Flair peers |
203
+ | `flair federation status\|verify\|reachability --target <url>` | peer table, sync recency, probes |
204
+
205
+ `fleet verify` exit codes: 1 origin failed, 2 peer version skew, 3 peer unverifiable.
206
+
207
+ > **A credential mismatch renders as an empty section, not an error.** `flair status`
208
+ > reads `/HealthDetail` with `FLAIR_ADMIN_PASS` / `HDB_ADMIN_PASSWORD` / a pinned agent
209
+ > key — **not** the `FABRIC_*` credentials `deploy` and `fleet verify` use. On failure it
210
+ > renders blank — a blank Disk section means "couldn't authenticate" as often as
211
+ > "nothing to report".
212
+
213
+ ### What doesn't
214
+
215
+ **`flair doctor` takes no `--target`** — it hardcodes localhost, reads a local PID file
216
+ and shells out to `lsof`. The command you'd reach for when something breaks is unavailable
217
+ here. Unavailable too: `start`, `stop`, `restart`, `snapshot`, `reembed`, `rem`, `bridge`.
218
+
219
+ **Fabric's own cluster topology is invisible.** `fleet verify` sweeps *Flair's* federation
220
+ peer table, not Harper's cluster nodes — `cluster_status` is harper-pro-only and absent
221
+ from the OSS `harper` build. **`0 peers known` means "0 on file", never "0 exist."**
222
+
223
+ **There is no disk or quota telemetry.** `flair status` reports usage for two directories:
224
+ no free space, no total, no quota, no warning threshold, walk capped at six levels, no
225
+ per-component size. `system_information` is never called — an instance can hit its quota
226
+ with nothing saying so. The one indirect signal is a migration halting for space.
227
+
228
+ > **On `get_components`.** A source comment claims Harper excludes `node_modules`
229
+ > server-side. That is uncited and unverified, so this guide doesn't rely on it — and it
230
+ > wouldn't matter: Flair calls it only as a post-failure convergence oracle and discards
231
+ > the `size` field. Component disk usage is invisible structurally.
232
+
233
+ ### Known hazard: unbounded npm cache
234
+
235
+ **Open — [flair#886](https://github.com/tpsdev-ai/flair/issues/886).** Every deploy runs a
236
+ server-side `npm install` using the node's default cache. npm never evicts it, so it
237
+ grows until it fills the quota.
238
+
239
+ No in-product mitigation: no cache flag, no alternate location, no cleanup. One install
240
+ per deploy bounds the *rate*, not the total. Harper consults `install_command` only when
241
+ `node_modules` is absent and `deploy_component` has no force-reinstall option, so the
242
+ obvious fix isn't available — and clearing the cache needs node access this shape
243
+ doesn't give you.
244
+
245
+ ### Backup and rollback
246
+
247
+ `flair snapshot` is local-only. Back up before every upgrade — `flair backup` takes
248
+ **`--url`**, not `--target`:
249
+
250
+ ```bash
251
+ flair backup --url https://<cluster>.<org>.harperfabric.com \
252
+ --admin-pass-file <path> --output ./flair-backup.json
253
+
254
+ # --check shows the version diff and plan without deploying
255
+ flair upgrade --target https://<cluster>.<org>.harperfabric.com --check
256
+ ```
257
+
258
+ Full path: [upgrade.md](upgrade.md#upgrading-a-fabric-deployed-instance).
259
+
260
+ ---
261
+
262
+ ## See also
263
+
264
+ - [deployment.md](deployment.md) — the standalone local shape
265
+ - [upgrade.md](upgrade.md#upgrading-a-fabric-deployed-instance) — Fabric upgrades, fleet verify
266
+ - [federation.md](federation.md) — pairing, sync driver, conflict resolution
267
+ - [spoke-bringup.md](spoke-bringup.md) · [auth.md](auth.md) · [secrets-and-keys.md](secrets-and-keys.md)
@@ -116,6 +116,11 @@ Note: embeddings run on CPU in Docker (no Metal acceleration). Performance is ac
116
116
 
117
117
  Deploying to a Harper Fabric cluster is a different mechanism from the installs above — `flair deploy` pushes Flair as a cluster component instead of `npm install -g`. To upgrade an already-deployed Fabric instance in place, use `FABRIC_USER=<admin> FABRIC_PASSWORD=<pass> flair upgrade --target <fabric-url>` (or `--fabric-password-file <path>` in place of the env var), not the local upgrade path. Inline `--fabric-user`/`--fabric-password` flags also work but are discouraged — both leak to shell history and `ps`. See [`docs/upgrade.md` — Upgrading a Fabric-deployed instance](upgrade.md#upgrading-a-fabric-deployed-instance) for the full walkthrough, including the automatic post-deploy fleet-convergence sweep.
118
118
 
119
+ For the hosted shape end to end — when to choose it, ports and auth against a managed
120
+ Fabric endpoint, pairing local spokes to a hosted hub, and what you can and cannot
121
+ observe without a shell on the node — see
122
+ [`docs/deploying-on-fabric.md`](deploying-on-fabric.md).
123
+
119
124
  ---
120
125
 
121
126
  ## Remote Access
@@ -179,6 +184,7 @@ Set these in the Flair process environment (`~/Library/LaunchAgents/ai.tpsdev.fl
179
184
  | `HDB_ADMIN_PASSWORD` | Bootstrap password for the embedded Harper. After first start, the persisted user record is the source of truth; rotate via the Harper ops API, not by changing this env var. | Set at install time. See [secrets-and-keys.md](secrets-and-keys.md) for rotation. |
180
185
  | `FLAIR_KEY_PASSPHRASE` | Passphrase used to derive the AES-256-GCM key that wraps federation private-key seeds at rest. Auto-generated to `~/.flair/keys/.passphrase` if unset. | Set explicitly for production federation deployments so the passphrase isn't auto-generated and lost on disk wipe. |
181
186
  | `HTTP_PORT` | Override the Harper HTTP port. Useful for sandboxes; production deployments should configure the port in `config.yaml` instead. | Rare. |
187
+ | `FLAIR_OPS_BIND` | Bind address for the Harper **ops API**. Resolution order: `flair init --ops-bind` > this variable > the `opsBind` key `flair init` persists in `~/.flair/config.yaml` > `127.0.0.1`. Every Flair-managed Harper start re-asserts the resolved value, so the persisted key is what makes a choice survive `flair restart` / `flair upgrade`. | Only for deployments that genuinely need remote ops admin (multi-host / Fabric) — set it to `0.0.0.0`, or record it once with `flair init --ops-bind 0.0.0.0`. Single-host installs want the loopback default. |
182
188
 
183
189
  ---
184
190
 
@@ -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)