@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.
- package/README.md +39 -11
- package/SECURITY.md +52 -20
- package/config.yaml +32 -5
- package/dist/cli.js +2235 -260
- package/dist/deploy.js +96 -15
- package/dist/doctor-client.js +312 -0
- package/dist/fleet-presence.js +98 -0
- package/dist/fleet-verify.js +291 -0
- package/dist/install/clients.js +18 -0
- package/dist/mcp-client-assertion.js +364 -0
- package/dist/probe.js +97 -0
- package/dist/rem/restore.js +1 -1
- package/dist/rem/runner.js +82 -12
- package/dist/resources/Admin.js +1 -1
- package/dist/resources/AdminConnectors.js +1 -1
- package/dist/resources/AdminDashboard.js +1 -1
- package/dist/resources/AdminIdp.js +1 -1
- package/dist/resources/AdminInstance.js +1 -1
- package/dist/resources/AdminMemory.js +2 -2
- package/dist/resources/AdminPrincipals.js +1 -1
- package/dist/resources/Agent.js +14 -0
- package/dist/resources/AgentCard.js +1 -1
- package/dist/resources/AgentSeed.js +1 -1
- package/dist/resources/AttentionQuery.js +356 -0
- package/dist/resources/Credential.js +11 -1
- package/dist/resources/Federation.js +121 -2
- package/dist/resources/Integration.js +1 -1
- package/dist/resources/MCPClientMetadata.js +88 -0
- package/dist/resources/Memory.js +127 -22
- package/dist/resources/MemoryBootstrap.js +441 -85
- package/dist/resources/MemoryGrant.js +1 -1
- package/dist/resources/MemoryReflect.js +105 -49
- package/dist/resources/MemoryUsage.js +104 -0
- package/dist/resources/OAuth.js +69 -5
- package/dist/resources/OrgEvent.js +11 -0
- package/dist/resources/OrgEventCatchup.js +1 -1
- package/dist/resources/Presence.js +268 -17
- package/dist/resources/RecordUsage.js +230 -0
- package/dist/resources/Relationship.js +108 -26
- package/dist/resources/SemanticSearch.js +88 -320
- package/dist/resources/SkillScan.js +15 -0
- package/dist/resources/Soul.js +14 -0
- package/dist/resources/WorkspaceLatest.js +1 -1
- package/dist/resources/WorkspaceState.js +12 -1
- package/dist/resources/agent-auth.js +61 -3
- package/dist/resources/agentcard-fields.js +2 -2
- package/dist/resources/auth-middleware.js +42 -14
- package/dist/resources/bm25-filter.js +1 -1
- package/dist/resources/bm25.js +13 -7
- package/dist/resources/collision-lib.js +157 -0
- package/dist/resources/dedup.js +2 -2
- package/dist/resources/ed25519-auth.js +2 -2
- package/dist/resources/embeddings-boot.js +149 -0
- package/dist/resources/embeddings-provider.js +364 -106
- package/dist/resources/entity-vocab.js +139 -0
- package/dist/resources/federation-nonce-store.js +195 -0
- package/dist/resources/health.js +97 -6
- package/dist/resources/instance-identity.js +53 -0
- package/dist/resources/mcp-client-metadata-fields.js +112 -0
- package/dist/resources/mcp-handler.js +1 -1
- package/dist/resources/mcp-tools.js +88 -10
- package/dist/resources/memory-bootstrap-lib.js +1 -1
- package/dist/resources/memory-read-scope.js +58 -71
- package/dist/resources/memory-reflect-lib.js +289 -0
- package/dist/resources/memory-visibility.js +37 -0
- package/dist/resources/migration-boot.js +143 -0
- package/dist/resources/migrations/dir-safety.js +71 -0
- package/dist/resources/migrations/embedding-stamp.js +178 -0
- package/dist/resources/migrations/envelope.js +43 -0
- package/dist/resources/migrations/export.js +38 -0
- package/dist/resources/migrations/ledger.js +55 -0
- package/dist/resources/migrations/lock.js +154 -0
- package/dist/resources/migrations/progress.js +31 -0
- package/dist/resources/migrations/registry.js +36 -0
- package/dist/resources/migrations/risk-policy.js +23 -0
- package/dist/resources/migrations/runner.js +456 -0
- package/dist/resources/migrations/snapshot.js +127 -0
- package/dist/resources/migrations/source-fields.js +93 -0
- package/dist/resources/migrations/space.js +73 -0
- package/dist/resources/migrations/state.js +64 -0
- package/dist/resources/migrations/status.js +39 -0
- package/dist/resources/migrations/synthetic-test-migration.js +93 -0
- package/dist/resources/migrations/types.js +9 -0
- package/dist/resources/presence-internal.js +29 -0
- package/dist/resources/provenance.js +50 -0
- package/dist/resources/rate-limiter.js +8 -1
- package/dist/resources/scoring.js +124 -5
- package/dist/resources/semantic-retrieval-core.js +317 -0
- package/dist/version-check.js +167 -0
- package/dist/version-handshake.js +122 -0
- package/package.json +4 -5
- package/schemas/agent.graphql +7 -0
- package/schemas/event.graphql +5 -0
- package/schemas/federation.graphql +12 -0
- package/schemas/memory.graphql +82 -0
- package/schemas/schema.graphql +5 -43
- package/schemas/workspace.graphql +3 -0
- package/dist/resources/IngestEvents.js +0 -162
- package/dist/resources/IssueTokens.js +0 -19
- package/dist/resources/ObsAgentSnapshot.js +0 -13
- package/dist/resources/ObsEventFeed.js +0 -13
- package/dist/resources/ObsOffice.js +0 -19
- package/dist/resources/ObservationCenter.js +0 -23
- 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
|
+
}
|