@tpsdev-ai/flair 0.20.1 → 0.22.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 (104) hide show
  1. package/README.md +39 -11
  2. package/SECURITY.md +52 -20
  3. package/config.yaml +32 -5
  4. package/dist/cli.js +2235 -260
  5. package/dist/deploy.js +96 -15
  6. package/dist/doctor-client.js +312 -0
  7. package/dist/fleet-presence.js +98 -0
  8. package/dist/fleet-verify.js +291 -0
  9. package/dist/install/clients.js +18 -0
  10. package/dist/mcp-client-assertion.js +364 -0
  11. package/dist/probe.js +97 -0
  12. package/dist/rem/restore.js +1 -1
  13. package/dist/rem/runner.js +82 -12
  14. package/dist/resources/Admin.js +1 -1
  15. package/dist/resources/AdminConnectors.js +1 -1
  16. package/dist/resources/AdminDashboard.js +1 -1
  17. package/dist/resources/AdminIdp.js +1 -1
  18. package/dist/resources/AdminInstance.js +1 -1
  19. package/dist/resources/AdminMemory.js +2 -2
  20. package/dist/resources/AdminPrincipals.js +1 -1
  21. package/dist/resources/Agent.js +14 -0
  22. package/dist/resources/AgentCard.js +1 -1
  23. package/dist/resources/AgentSeed.js +1 -1
  24. package/dist/resources/AttentionQuery.js +356 -0
  25. package/dist/resources/Credential.js +11 -1
  26. package/dist/resources/Federation.js +121 -2
  27. package/dist/resources/Integration.js +1 -1
  28. package/dist/resources/MCPClientMetadata.js +88 -0
  29. package/dist/resources/Memory.js +127 -22
  30. package/dist/resources/MemoryBootstrap.js +441 -85
  31. package/dist/resources/MemoryGrant.js +1 -1
  32. package/dist/resources/MemoryReflect.js +105 -49
  33. package/dist/resources/MemoryUsage.js +104 -0
  34. package/dist/resources/OAuth.js +69 -5
  35. package/dist/resources/OrgEvent.js +11 -0
  36. package/dist/resources/OrgEventCatchup.js +1 -1
  37. package/dist/resources/Presence.js +268 -17
  38. package/dist/resources/RecordUsage.js +230 -0
  39. package/dist/resources/Relationship.js +108 -26
  40. package/dist/resources/SemanticSearch.js +88 -320
  41. package/dist/resources/SkillScan.js +15 -0
  42. package/dist/resources/Soul.js +14 -0
  43. package/dist/resources/WorkspaceLatest.js +1 -1
  44. package/dist/resources/WorkspaceState.js +12 -1
  45. package/dist/resources/agent-auth.js +61 -3
  46. package/dist/resources/agentcard-fields.js +2 -2
  47. package/dist/resources/auth-middleware.js +42 -14
  48. package/dist/resources/bm25-filter.js +1 -1
  49. package/dist/resources/bm25.js +13 -7
  50. package/dist/resources/collision-lib.js +157 -0
  51. package/dist/resources/dedup.js +2 -2
  52. package/dist/resources/ed25519-auth.js +2 -2
  53. package/dist/resources/embeddings-boot.js +149 -0
  54. package/dist/resources/embeddings-provider.js +364 -106
  55. package/dist/resources/entity-vocab.js +139 -0
  56. package/dist/resources/federation-nonce-store.js +195 -0
  57. package/dist/resources/health.js +97 -6
  58. package/dist/resources/instance-identity.js +53 -0
  59. package/dist/resources/mcp-client-metadata-fields.js +112 -0
  60. package/dist/resources/mcp-handler.js +1 -1
  61. package/dist/resources/mcp-tools.js +88 -10
  62. package/dist/resources/memory-bootstrap-lib.js +1 -1
  63. package/dist/resources/memory-read-scope.js +58 -71
  64. package/dist/resources/memory-reflect-lib.js +289 -0
  65. package/dist/resources/memory-visibility.js +37 -0
  66. package/dist/resources/migration-boot.js +143 -0
  67. package/dist/resources/migrations/dir-safety.js +71 -0
  68. package/dist/resources/migrations/embedding-stamp.js +178 -0
  69. package/dist/resources/migrations/envelope.js +43 -0
  70. package/dist/resources/migrations/export.js +38 -0
  71. package/dist/resources/migrations/ledger.js +55 -0
  72. package/dist/resources/migrations/lock.js +154 -0
  73. package/dist/resources/migrations/progress.js +31 -0
  74. package/dist/resources/migrations/registry.js +36 -0
  75. package/dist/resources/migrations/risk-policy.js +23 -0
  76. package/dist/resources/migrations/runner.js +456 -0
  77. package/dist/resources/migrations/snapshot.js +127 -0
  78. package/dist/resources/migrations/source-fields.js +93 -0
  79. package/dist/resources/migrations/space.js +73 -0
  80. package/dist/resources/migrations/state.js +64 -0
  81. package/dist/resources/migrations/status.js +39 -0
  82. package/dist/resources/migrations/synthetic-test-migration.js +93 -0
  83. package/dist/resources/migrations/types.js +9 -0
  84. package/dist/resources/presence-internal.js +29 -0
  85. package/dist/resources/provenance.js +50 -0
  86. package/dist/resources/rate-limiter.js +8 -1
  87. package/dist/resources/scoring.js +124 -5
  88. package/dist/resources/semantic-retrieval-core.js +317 -0
  89. package/dist/version-check.js +167 -0
  90. package/dist/version-handshake.js +122 -0
  91. package/package.json +4 -5
  92. package/schemas/agent.graphql +7 -0
  93. package/schemas/event.graphql +5 -0
  94. package/schemas/federation.graphql +12 -0
  95. package/schemas/memory.graphql +82 -0
  96. package/schemas/schema.graphql +5 -43
  97. package/schemas/workspace.graphql +3 -0
  98. package/dist/resources/IngestEvents.js +0 -162
  99. package/dist/resources/IssueTokens.js +0 -19
  100. package/dist/resources/ObsAgentSnapshot.js +0 -13
  101. package/dist/resources/ObsEventFeed.js +0 -13
  102. package/dist/resources/ObsOffice.js +0 -19
  103. package/dist/resources/ObservationCenter.js +0 -23
  104. package/ui/observation-center.html +0 -385
@@ -0,0 +1,291 @@
1
+ /**
2
+ * fleet-verify.ts — `flair fleet verify` (flair#636)
3
+ *
4
+ * Fabric deploys tolerate replication errors by design (origin-first, see
5
+ * src/deploy.ts's REPLICATION_FAILURE_RE / --ignore-replication-errors) —
6
+ * but nothing upstream of this module confirms a replicated node actually
7
+ * converged. Harper's own "Successfully deployed" means "origin took it."
8
+ * This sweeps the origin + every known peer and reports a per-node table
9
+ * instead of a single boolean, reusing src/probe.ts's `probeInstance`
10
+ * verbatim (flair#635/#641 — same shape, Fabric admin Basic auth swapped in
11
+ * for the injected `authedGet`, exactly per that module's own docstring).
12
+ *
13
+ * ── What "peer" means here — read before trusting a green sweep ──────────
14
+ * Harper Fabric's OWN cluster-replication peers (the physical nodes a
15
+ * component gets replicated to by `harper deploy`) are NOT enumerable
16
+ * through any operation in the OSS @harperfast/harper build this repo
17
+ * depends on. `cluster_status` — the operation that would answer "what
18
+ * nodes are in this cluster and are they in sync" — is explicitly called
19
+ * out as harper-pro-only in Harper's own source (node_modules/@harperfast/
20
+ * harper/components/mcp/tools/schemas/operationDescriptions.ts: "Out-of-core
21
+ * operations (e.g. harper-pro's cluster_status) cannot have entries here").
22
+ * There is no "list this cluster's replication nodes" call available to this
23
+ * CLI, on the origin or anywhere else — direct AND via-origin cluster
24
+ * topology discovery are both unavailable in this Harper tier.
25
+ *
26
+ * The only node registry this module CAN reach is Flair's OWN federation
27
+ * peer table (resources/Federation.ts's Peer records, via GET
28
+ * /FederationPeers) — a distinct, higher-level concept (cross-instance
29
+ * memory-sync pairing, hub-and-spoke) that happens to double as a fleet
30
+ * registry ONLY where an operator has ALSO paired each Fabric replica as a
31
+ * federation peer of the origin. A replica that was never paired is
32
+ * invisible to this sweep — `0 peers known` means "0 peers ON FILE", never
33
+ * "0 peers exist." renderFleetSweepTable() prints this caveat inline
34
+ * whenever the peer list comes back empty, so it can't be missed by reading
35
+ * a green summary line.
36
+ *
37
+ * Every peer that DOES carry a direct `endpoint` gets a REAL probe (health +
38
+ * authenticated version check, same Fabric admin Basic-auth credentials as
39
+ * the origin — a Fabric cluster shares one admin realm). A peer with no
40
+ * usable endpoint is reported "unverifiable" — never silently dropped, never
41
+ * printed green. See classifyNode()'s "method" field: "direct" (we actually
42
+ * hit it) vs "none" (we could not attempt a check at all, and say why).
43
+ */
44
+ import { probeInstance } from "./probe.js";
45
+ import * as render from "./render.js";
46
+ // ─── Exit codes (documented in `flair fleet verify --help`) ─────────────────
47
+ /** All nodes verified: healthy, authenticated, and version-matched. */
48
+ export const FLEET_EXIT_OK = 0;
49
+ /** Origin failed — unreachable, unauthenticated, or running the wrong version. Worst case: the cluster's entrypoint itself is broken. */
50
+ export const FLEET_EXIT_ORIGIN_FAILED = 1;
51
+ /** Origin is fine, but at least one reachable peer is running a DIFFERENT version — a mixed-version fleet (the #638 scenario). */
52
+ export const FLEET_EXIT_PEER_SKEW = 2;
53
+ /** Origin is fine, no version skew among reachable peers, but at least one peer could not be verified at all (unreachable, auth rejected, or no endpoint on file). */
54
+ export const FLEET_EXIT_PEER_UNREACHABLE = 3;
55
+ export const FLEET_EXIT_DESCRIPTIONS = {
56
+ [FLEET_EXIT_OK]: "all nodes verified: healthy, authenticated, version-matched",
57
+ [FLEET_EXIT_ORIGIN_FAILED]: "origin failed (unreachable, unauthenticated, or wrong version)",
58
+ [FLEET_EXIT_PEER_SKEW]: "origin OK, but a reachable peer is running a DIFFERENT version (skew)",
59
+ [FLEET_EXIT_PEER_UNREACHABLE]: "origin OK, no skew among reachable peers, but a peer could not be verified at all (unreachable, auth rejected, or no endpoint on file)",
60
+ };
61
+ /**
62
+ * Priority order when multiple problems exist at once: an origin failure
63
+ * always wins (nothing downstream matters if the entrypoint is broken), then
64
+ * skew (a real mixed-version fleet), then unreachable/unverifiable (we
65
+ * genuinely don't know that node's state).
66
+ */
67
+ export function decideFleetExitCode(origin, peers) {
68
+ if (origin.status !== "ok")
69
+ return FLEET_EXIT_ORIGIN_FAILED;
70
+ if (peers.some((p) => p.status === "skew"))
71
+ return FLEET_EXIT_PEER_SKEW;
72
+ if (peers.some((p) => p.status !== "ok"))
73
+ return FLEET_EXIT_PEER_UNREACHABLE;
74
+ return FLEET_EXIT_OK;
75
+ }
76
+ // ─── Pure decision logic: ProbeResult → FleetNodeResult ─────────────────────
77
+ /**
78
+ * Turn a probe attempt (or the absence of one) into a table row. Pure —
79
+ * no I/O. `expectVersion` is the baseline this node should be running;
80
+ * pass null when there is nothing to compare against (skew can't be
81
+ * evaluated, but health/auth still can).
82
+ */
83
+ export function classifyNode(role, id, url, method, probe, expectVersion, unverifiableReason) {
84
+ if (method === "none" || probe === null) {
85
+ return {
86
+ id, role, url, method: "none",
87
+ healthy: null, authenticated: null, version: null, versionMatch: null,
88
+ status: "unverifiable",
89
+ detail: unverifiableReason ?? "no way to verify this node",
90
+ };
91
+ }
92
+ if (!probe.healthy) {
93
+ return {
94
+ id, role, url, method,
95
+ healthy: false, authenticated: null, version: null, versionMatch: null,
96
+ status: "unreachable",
97
+ detail: probe.error ?? `${url ?? id} did not answer /Health`,
98
+ };
99
+ }
100
+ if (probe.authenticated === false) {
101
+ return {
102
+ id, role, url, method,
103
+ healthy: true, authenticated: false, version: null, versionMatch: null,
104
+ status: "auth-failed",
105
+ detail: probe.error ?? "authenticated request rejected",
106
+ };
107
+ }
108
+ if (expectVersion !== null && probe.versionMatch === false) {
109
+ return {
110
+ id, role, url, method,
111
+ healthy: true, authenticated: probe.authenticated, version: probe.version, versionMatch: false,
112
+ status: "skew",
113
+ detail: probe.error ?? `version mismatch: expected ${expectVersion}, reports ${probe.version ?? "unknown"}`,
114
+ };
115
+ }
116
+ return {
117
+ id, role, url, method,
118
+ healthy: true, authenticated: probe.authenticated, version: probe.version, versionMatch: probe.versionMatch,
119
+ status: "ok",
120
+ detail: probe.version ? `healthy, running ${probe.version}` : "healthy",
121
+ };
122
+ }
123
+ // ─── Peer endpoint validation ────────────────────────────────────────────────
124
+ /**
125
+ * A federation Peer record may have no `endpoint` at all (tunnel-paired —
126
+ * see src/cli.ts's existing `flair federation ping` handling of the same
127
+ * shape) or an endpoint that isn't a plain http(s) URL. Reject non-http(s)
128
+ * schemes rather than attempting to fetch them (same protocol allowlist as
129
+ * the existing federation-ping probe — Sherlock review on #314).
130
+ */
131
+ export function validatePeerEndpoint(endpoint) {
132
+ if (!endpoint) {
133
+ return { error: "no endpoint on file (never federation-paired with a direct endpoint, or tunnel-paired)" };
134
+ }
135
+ let parsed;
136
+ try {
137
+ parsed = new URL(endpoint);
138
+ }
139
+ catch {
140
+ return { error: `invalid endpoint URL on file: ${endpoint}` };
141
+ }
142
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
143
+ return { error: `unsupported endpoint protocol on file: ${parsed.protocol}` };
144
+ }
145
+ return { url: endpoint };
146
+ }
147
+ // ─── Fabric admin Basic-auth authedGet (injected into probeInstance) ────────
148
+ /**
149
+ * Builds the authedGet probeInstance needs, using Fabric admin Basic auth —
150
+ * the exact credential shape src/deploy.ts and src/fabric-upgrade.ts already
151
+ * use for the whole cluster (FABRIC_USER/FABRIC_PASSWORD). Assumes a single
152
+ * Fabric cluster shares one admin realm across its nodes; a peer that
153
+ * rejects these credentials (a genuinely separate Harper instance with its
154
+ * own admin) surfaces as "auth-failed" — itself a real, informative finding,
155
+ * not a bug in this probe.
156
+ *
157
+ * NEVER logs the credential — only the Authorization header carries it, and
158
+ * probeInstance/callers only ever see/report the HTTP outcome.
159
+ */
160
+ export function buildFabricAuthedGet(baseUrl, fabricUser, fabricPassword) {
161
+ const base = baseUrl.replace(/\/+$/, "");
162
+ const auth = Buffer.from(`${fabricUser}:${fabricPassword}`).toString("base64");
163
+ return async (path) => {
164
+ const res = await fetch(`${base}${path}`, {
165
+ headers: { Authorization: `Basic ${auth}` },
166
+ signal: AbortSignal.timeout(15_000),
167
+ });
168
+ const text = await res.text();
169
+ if (!res.ok)
170
+ throw new Error(text || `HTTP ${res.status}`);
171
+ return text ? JSON.parse(text) : {};
172
+ };
173
+ }
174
+ async function defaultFetchPeers(authedGet) {
175
+ const body = await authedGet("/FederationPeers");
176
+ return Array.isArray(body?.peers) ? body.peers : [];
177
+ }
178
+ function defaultDeps() {
179
+ return {
180
+ probe: probeInstance,
181
+ fetchPeers: defaultFetchPeers,
182
+ buildAuthedGet: buildFabricAuthedGet,
183
+ };
184
+ }
185
+ // ─── Orchestrator ───────────────────────────────────────────────────────────
186
+ export async function sweepFleet(opts, injected) {
187
+ const deps = { ...defaultDeps(), ...injected };
188
+ const log = deps.log ?? (() => { });
189
+ const originAuthedGet = deps.buildAuthedGet(opts.target, opts.fabricUser, opts.fabricPassword);
190
+ log(`Probing origin ${opts.target} ...`);
191
+ const originProbe = await deps.probe(opts.target, {
192
+ expectVersion: opts.expectVersion,
193
+ timeoutMs: opts.timeoutMs,
194
+ authedGet: originAuthedGet,
195
+ });
196
+ const explicitExpect = opts.expectVersion ?? null;
197
+ const expectVersion = explicitExpect ?? originProbe.version ?? null;
198
+ const expectVersionSource = explicitExpect !== null ? "explicit" : expectVersion !== null ? "origin" : "none";
199
+ // Origin is classified against the EXPLICIT expectation only — when none was
200
+ // given, the origin itself defines the baseline and can't be "skewed"
201
+ // relative to its own reading.
202
+ const origin = classifyNode("origin", "origin", opts.target, "direct", originProbe, explicitExpect);
203
+ let peerRecords = [];
204
+ let peerEnumerationError = null;
205
+ try {
206
+ log("Enumerating federation peers ...");
207
+ peerRecords = await deps.fetchPeers(originAuthedGet);
208
+ }
209
+ catch (err) {
210
+ peerEnumerationError = err?.message ?? String(err);
211
+ }
212
+ // Revoked peers are intentionally decommissioned — reporting them
213
+ // unreachable would be noise, not signal.
214
+ const knownPeers = peerRecords.filter((p) => p.status !== "revoked");
215
+ const peers = [];
216
+ for (const p of knownPeers) {
217
+ const check = validatePeerEndpoint(p.endpoint);
218
+ if ("error" in check) {
219
+ peers.push(classifyNode("peer", p.id, p.endpoint ?? null, "none", null, expectVersion, check.error));
220
+ continue;
221
+ }
222
+ log(`Probing peer ${p.id} (${check.url}) ...`);
223
+ const peerAuthedGet = deps.buildAuthedGet(check.url, opts.fabricUser, opts.fabricPassword);
224
+ const peerProbe = await deps.probe(check.url, {
225
+ expectVersion: expectVersion ?? undefined,
226
+ timeoutMs: opts.timeoutMs,
227
+ authedGet: peerAuthedGet,
228
+ });
229
+ peers.push(classifyNode("peer", p.id, check.url, "direct", peerProbe, expectVersion));
230
+ }
231
+ if (peerEnumerationError) {
232
+ // A failure to even LIST peers means fleet coverage is unknown — never
233
+ // let that read as "0 peers, all clear."
234
+ peers.push({
235
+ id: "(peer enumeration)",
236
+ role: "peer",
237
+ url: null,
238
+ method: "none",
239
+ healthy: null, authenticated: null, version: null, versionMatch: null,
240
+ status: "unverifiable",
241
+ detail: `GET /FederationPeers on the origin failed: ${peerEnumerationError} — fleet coverage unknown, cannot confirm every peer was even considered`,
242
+ });
243
+ }
244
+ const exitCode = decideFleetExitCode(origin, peers);
245
+ return { target: opts.target, expectVersion, expectVersionSource, origin, peers, peerEnumerationError, exitCode };
246
+ }
247
+ // ─── Rendering ───────────────────────────────────────────────────────────────
248
+ const STATUS_COLOR = {
249
+ ok: render.c.green,
250
+ skew: render.c.red,
251
+ unreachable: render.c.red,
252
+ "auth-failed": render.c.yellow,
253
+ unverifiable: render.c.yellow,
254
+ };
255
+ /**
256
+ * Per-node table + the caveats an operator needs to trust it. An "ok" row
257
+ * always means a REAL probe answered; anything else names exactly how it's
258
+ * wrong (see FleetNodeResult.detail) instead of a bare pass/fail.
259
+ */
260
+ export function renderFleetSweepTable(result) {
261
+ const rows = [result.origin, ...result.peers];
262
+ const cols = [
263
+ { label: "node", key: "id" },
264
+ { label: "role", key: "role" },
265
+ { label: "method", key: "method", format: (v) => (v === "direct" ? "direct" : render.wrap(render.c.dim, "none")) },
266
+ {
267
+ label: "status",
268
+ key: "status",
269
+ format: (v) => render.wrap(STATUS_COLOR[v] ?? render.c.dim, String(v)),
270
+ },
271
+ { label: "version", key: "version", format: (v) => (v ? String(v) : render.wrap(render.c.dim, "—")) },
272
+ { label: "detail", key: "detail" },
273
+ ];
274
+ const lines = [render.table(cols, rows)];
275
+ if (result.peers.length === 0 || (result.peers.length === 1 && result.peerEnumerationError)) {
276
+ lines.push("");
277
+ lines.push(render.wrap(render.c.dim, "0 federation peers known to the origin. This does NOT mean the Fabric cluster has no other " +
278
+ "replication nodes — Harper's own cluster peers aren't visible to this tool (cluster_status is a " +
279
+ "harper-pro-only operation, unavailable in the OSS build this CLI ships). It means no other node is " +
280
+ "paired as a Flair federation peer of this origin. If this cluster has replicas, pair them " +
281
+ "(`flair federation pair`) so fleet verify can actually see them."));
282
+ }
283
+ const expectLine = result.expectVersionSource === "explicit"
284
+ ? `compared against --expect-version ${result.expectVersion}`
285
+ : result.expectVersionSource === "origin"
286
+ ? `compared against the origin's own reported version (${result.expectVersion}) — no --expect-version given`
287
+ : "no version baseline available (origin unreachable/unauthenticated and no --expect-version given) — skew could not be checked";
288
+ lines.push("");
289
+ lines.push(render.wrap(render.c.dim, expectLine));
290
+ return lines.join("\n");
291
+ }
@@ -179,6 +179,24 @@ function geminiConfigPath() {
179
179
  function codexConfigPath() {
180
180
  return join(resolveHome(), ".codex", "config.toml");
181
181
  }
182
+ /**
183
+ * Single dispatcher for "where does this client's MCP config live" — used by
184
+ * `flair doctor`'s client-integration checks (flair#588) to read the config
185
+ * without duplicating the per-client path logic that already lives here.
186
+ * Additive only: does not change existing wire/detect behavior.
187
+ */
188
+ export function clientConfigPath(id) {
189
+ switch (id) {
190
+ case "claude-code":
191
+ return join(resolveHome(), ".claude.json");
192
+ case "codex":
193
+ return codexConfigPath();
194
+ case "gemini":
195
+ return geminiConfigPath();
196
+ case "cursor":
197
+ return cursorConfigPath();
198
+ }
199
+ }
182
200
  // ---- Internal wiring functions --------------------------------------------------
183
201
  //
184
202
  // Claude Code wiring lives inline in src/cli.ts (it writes ~/.claude.json, the