@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,154 @@
1
+ /**
2
+ * lock.ts — single-flight guard (Kern verdict, 2026-07-12):
3
+ * "Single-flight v1 = in-process mutex + stale-tolerant file lock (flock on
4
+ * well-known path; dead-holder → break and proceed). Fabric-wide ordering =
5
+ * follow-on with its own review, NOT this slice."
6
+ *
7
+ * Two layers:
8
+ * 1. An in-process mutex (module-scope flag) — cheapest guard against two
9
+ * concurrent async callers inside the SAME Node process (e.g. a
10
+ * duplicate boot-trigger firing twice).
11
+ * 2. A file lock at a well-known path under the data dir. Node has no
12
+ * portable flock() binding, so this uses exclusive file CREATE
13
+ * (O_CREAT|O_EXCL — atomic at the OS level, the same primitive flock
14
+ * ultimately reduces to for a single-host guard) instead. The lock file
15
+ * holds `{pid, hostname, startedAt}`. A lock is considered STALE (and
16
+ * is broken/removed before the new attempt) when either: the recorded
17
+ * pid is no longer alive on this host, OR the file is older than
18
+ * `staleMs` (covers a cross-host holder this process can't liveness-
19
+ * check by pid — Fabric-wide coordination is explicitly out of scope
20
+ * for this slice, so age is the only signal available for that case).
21
+ *
22
+ * This is a SINGLE-HOST guard, not a Fabric-wide one — multiple Flair
23
+ * instances in a Fabric cluster each have their OWN data dir today, so this
24
+ * is sufficient for v1. Fabric-wide ordering (one lock across a cluster
25
+ * sharing storage) is explicitly deferred, per the verdict above.
26
+ */
27
+ import { existsSync, mkdirSync, openSync, closeSync, writeSync, readFileSync, unlinkSync, statSync, utimesSync, constants as fsConstants } from "node:fs";
28
+ import { dirname } from "node:path";
29
+ import { hostname as osHostname } from "node:os";
30
+ export const DEFAULT_STALE_MS = 5 * 60 * 1000; // 5 minutes with no heartbeat touch = stale
31
+ export function defaultIsProcessAlive(pid) {
32
+ try {
33
+ process.kill(pid, 0);
34
+ return true;
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ }
40
+ export function defaultLockDeps(lockPath) {
41
+ return {
42
+ lockPath,
43
+ staleMs: DEFAULT_STALE_MS,
44
+ isProcessAlive: defaultIsProcessAlive,
45
+ now: () => Date.now(),
46
+ };
47
+ }
48
+ // In-process mutex — module-scope, so it's shared by every caller within
49
+ // this one Node process regardless of which lockPath they pass (a single
50
+ // process only ever wants to run one migration cycle at a time).
51
+ let inProcessHeld = false;
52
+ /** Test-only reset for the in-process mutex (mirrors instance-identity.ts's `_reset*ForTests` idiom). */
53
+ export function _resetInProcessLockForTests() {
54
+ inProcessHeld = false;
55
+ }
56
+ function readHolder(lockPath) {
57
+ try {
58
+ const raw = JSON.parse(readFileSync(lockPath, "utf-8"));
59
+ if (typeof raw?.pid === "number" && typeof raw?.hostname === "string" && typeof raw?.startedAt === "string") {
60
+ return raw;
61
+ }
62
+ return null;
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ }
68
+ /**
69
+ * Attempts to acquire the migration lock. On success, returns `release()`
70
+ * (removes the lock file + clears the in-process flag) and `touch()`
71
+ * (updates the lock file's mtime — the runner calls this periodically
72
+ * during a long-running migration so ANOTHER process's staleness check
73
+ * doesn't mistake a genuinely-still-running holder for dead).
74
+ */
75
+ export function acquireMigrationLock(deps) {
76
+ const resolved = { ...defaultLockDeps(deps.lockPath), ...deps };
77
+ const { lockPath, staleMs, isProcessAlive, now } = resolved;
78
+ if (inProcessHeld) {
79
+ return { acquired: false, reason: "already held in-process (another async caller in this process holds it)" };
80
+ }
81
+ mkdirSync(dirname(lockPath), { recursive: true, mode: 0o700 });
82
+ if (existsSync(lockPath)) {
83
+ const holder = readHolder(lockPath);
84
+ let ageMs = Infinity;
85
+ try {
86
+ ageMs = now() - statSync(lockPath).mtimeMs;
87
+ }
88
+ catch {
89
+ /* vanished between existsSync and stat — fall through to acquire */
90
+ }
91
+ const dead = !holder || !isProcessAlive(holder.pid) || ageMs > staleMs;
92
+ if (dead) {
93
+ try {
94
+ unlinkSync(lockPath);
95
+ }
96
+ catch {
97
+ /* another racer already broke it — fine, continue to the exclusive create below */
98
+ }
99
+ }
100
+ else {
101
+ return {
102
+ acquired: false,
103
+ reason: `held by pid ${holder.pid} on ${holder.hostname} since ${holder.startedAt}`,
104
+ holder,
105
+ };
106
+ }
107
+ }
108
+ let fd;
109
+ try {
110
+ // O_EXCL: atomic exclusive create — fails with EEXIST if another
111
+ // process/racer won the create between our stale-check above and here.
112
+ fd = openSync(lockPath, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 0o600);
113
+ }
114
+ catch (err) {
115
+ const code = err?.code;
116
+ if (code === "EEXIST") {
117
+ return { acquired: false, reason: "lost the race to acquire the lock file" };
118
+ }
119
+ throw err;
120
+ }
121
+ try {
122
+ const info = { pid: process.pid, hostname: osHostname(), startedAt: new Date(now()).toISOString() };
123
+ writeSync(fd, JSON.stringify(info));
124
+ }
125
+ finally {
126
+ closeSync(fd);
127
+ }
128
+ inProcessHeld = true;
129
+ let released = false;
130
+ return {
131
+ acquired: true,
132
+ release: () => {
133
+ if (released)
134
+ return;
135
+ released = true;
136
+ inProcessHeld = false;
137
+ try {
138
+ unlinkSync(lockPath);
139
+ }
140
+ catch {
141
+ /* already gone — fine */
142
+ }
143
+ },
144
+ touch: () => {
145
+ const t = new Date(now());
146
+ try {
147
+ utimesSync(lockPath, t, t);
148
+ }
149
+ catch {
150
+ /* best-effort heartbeat — a failed touch must never break the migration */
151
+ }
152
+ },
153
+ };
154
+ }
@@ -0,0 +1,31 @@
1
+ let cycleStatus = { phase: "idle" };
2
+ const progressById = new Map();
3
+ export function setCyclePhase(phase, error) {
4
+ cycleStatus = { phase, lastCycleError: error, lastCycleAt: new Date().toISOString() };
5
+ }
6
+ export function getCycleStatus() {
7
+ return { ...cycleStatus };
8
+ }
9
+ export function setMigrationProgress(p) {
10
+ progressById.set(p.id, p);
11
+ }
12
+ export function getMigrationProgress(id) {
13
+ return progressById.get(id);
14
+ }
15
+ export function listMigrationProgress() {
16
+ return Array.from(progressById.values());
17
+ }
18
+ /** Seeds an "idle" entry for every registered migration id — called once at
19
+ * boot-trigger registration time, synchronously, so `health` always has a
20
+ * row per registered migration even before the async cycle has run at all. */
21
+ export function seedIdleProgress(ids) {
22
+ for (const id of ids) {
23
+ if (!progressById.has(id)) {
24
+ progressById.set(id, { id, rowsDone: 0, rowsRemaining: 0, state: "idle" });
25
+ }
26
+ }
27
+ }
28
+ export function _resetProgressForTests() {
29
+ progressById.clear();
30
+ cycleStatus = { phase: "idle" };
31
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * registry.ts — the MigrationRegistry: an ordered list of registered
3
+ * migrations. embedding-stamp always registers; the synthetic CI-only
4
+ * variant registers ONLY when shouldRegisterSyntheticMigration() is true
5
+ * (see synthetic-test-migration.ts's doc for the exact gating rule).
6
+ */
7
+ import { createEmbeddingStampMigration } from "./embedding-stamp.js";
8
+ import { createSyntheticTestMigration, shouldRegisterSyntheticMigration } from "./synthetic-test-migration.js";
9
+ export class MigrationRegistry {
10
+ migrations = [];
11
+ register(migration) {
12
+ if (this.migrations.some((m) => m.id === migration.id)) {
13
+ throw new Error(`migration id already registered: ${migration.id}`);
14
+ }
15
+ this.migrations.push(migration);
16
+ return this;
17
+ }
18
+ list() {
19
+ return this.migrations;
20
+ }
21
+ get(id) {
22
+ return this.migrations.find((m) => m.id === id);
23
+ }
24
+ }
25
+ /**
26
+ * Builds the production registry. Always includes embedding-stamp;
27
+ * conditionally includes the CI-only synthetic migration.
28
+ */
29
+ export function buildRegistry(env = process.env) {
30
+ const registry = new MigrationRegistry();
31
+ registry.register(createEmbeddingStampMigration());
32
+ if (shouldRegisterSyntheticMigration(env)) {
33
+ registry.register(createSyntheticTestMigration());
34
+ }
35
+ return registry;
36
+ }
@@ -0,0 +1,23 @@
1
+ const POSTURE = {
2
+ "derived-only": {
3
+ riskClass: "derived-only",
4
+ batchSize: 50,
5
+ snapshotScope: "metadata-only",
6
+ gate: "count+marker",
7
+ },
8
+ "schema-additive": {
9
+ riskClass: "schema-additive",
10
+ batchSize: 50,
11
+ snapshotScope: "schema+metadata",
12
+ gate: "count+full-envelope",
13
+ },
14
+ "content-transform": {
15
+ riskClass: "content-transform",
16
+ batchSize: 10,
17
+ snapshotScope: "pointers+metadata",
18
+ gate: "count+old-row-envelope+new-row-presence",
19
+ },
20
+ };
21
+ export function postureFor(riskClass) {
22
+ return POSTURE[riskClass];
23
+ }