@tpsdev-ai/flair 0.20.0 → 0.21.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 (49) hide show
  1. package/README.md +29 -4
  2. package/SECURITY.md +28 -18
  3. package/dist/cli.js +482 -32
  4. package/dist/deploy.js +280 -18
  5. package/dist/doctor-client.js +312 -0
  6. package/dist/install/clients.js +18 -0
  7. package/dist/rem/restore.js +1 -1
  8. package/dist/resources/Admin.js +1 -1
  9. package/dist/resources/AdminConnectors.js +1 -1
  10. package/dist/resources/AdminDashboard.js +1 -1
  11. package/dist/resources/AdminIdp.js +1 -1
  12. package/dist/resources/AdminInstance.js +1 -1
  13. package/dist/resources/AdminMemory.js +2 -2
  14. package/dist/resources/AdminPrincipals.js +1 -1
  15. package/dist/resources/Agent.js +14 -0
  16. package/dist/resources/AgentCard.js +1 -1
  17. package/dist/resources/AgentSeed.js +1 -1
  18. package/dist/resources/Federation.js +98 -2
  19. package/dist/resources/IngestEvents.js +1 -1
  20. package/dist/resources/Integration.js +1 -1
  21. package/dist/resources/Memory.js +123 -17
  22. package/dist/resources/MemoryBootstrap.js +46 -36
  23. package/dist/resources/MemoryGrant.js +1 -1
  24. package/dist/resources/OAuth.js +61 -4
  25. package/dist/resources/OrgEventCatchup.js +1 -1
  26. package/dist/resources/Presence.js +55 -3
  27. package/dist/resources/Relationship.js +10 -1
  28. package/dist/resources/SemanticSearch.js +14 -14
  29. package/dist/resources/Soul.js +14 -0
  30. package/dist/resources/WorkspaceLatest.js +1 -1
  31. package/dist/resources/WorkspaceState.js +1 -1
  32. package/dist/resources/agent-auth.js +1 -1
  33. package/dist/resources/agentcard-fields.js +2 -2
  34. package/dist/resources/auth-middleware.js +24 -5
  35. package/dist/resources/bm25-filter.js +1 -1
  36. package/dist/resources/bm25.js +1 -1
  37. package/dist/resources/dedup.js +2 -2
  38. package/dist/resources/ed25519-auth.js +2 -2
  39. package/dist/resources/embeddings-provider.js +1 -1
  40. package/dist/resources/federation-nonce-store.js +195 -0
  41. package/dist/resources/instance-identity.js +53 -0
  42. package/dist/resources/memory-bootstrap-lib.js +1 -1
  43. package/dist/resources/memory-read-scope.js +58 -71
  44. package/dist/resources/memory-visibility.js +37 -0
  45. package/dist/version-check.js +167 -0
  46. package/package.json +2 -2
  47. package/schemas/agent.graphql +7 -0
  48. package/schemas/federation.graphql +12 -0
  49. package/schemas/memory.graphql +16 -0
@@ -0,0 +1,167 @@
1
+ /**
2
+ * version-check.ts — offline-tolerant, cached check of whether the installed
3
+ * @tpsdev-ai/flair is behind the latest published npm release.
4
+ *
5
+ * Motivation (flair#587): a laptop install sat on 0.16.1 through v0.17.0 and
6
+ * v0.19.0 (P0 security fixes) and v0.18.0 (memory-integrity fix) — `flair
7
+ * status` reported "✓ all checks passing" the whole time. Nothing anywhere
8
+ * in the CLI told the operator they were behind. `flair status` and `flair
9
+ * doctor` both wire this in.
10
+ *
11
+ * Non-negotiable design constraints — this must never make status/doctor
12
+ * WORSE:
13
+ * - Offline-tolerant: a failed/timed-out registry fetch falls back to a
14
+ * stale cache, or is skipped entirely. NEVER throws, NEVER hangs (short
15
+ * fetch timeout).
16
+ * - Cached with a TTL so a healthy network doesn't cost a registry round
17
+ * trip on every single `flair status`/`flair doctor` invocation.
18
+ * - No advisory data — we don't know which release fixed which CVE, so the
19
+ * severity heuristic is purely the version GAP (major/minor count), not
20
+ * "did this release carry a security fix". See classifyGap().
21
+ */
22
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
23
+ import { homedir } from "node:os";
24
+ import { dirname, join } from "node:path";
25
+ import { parseSemverCore } from "./fabric-upgrade.js";
26
+ export const FLAIR_PKG_NAME = "@tpsdev-ai/flair";
27
+ export const DEFAULT_CACHE_PATH = join(homedir(), ".flair", ".version-check-cache.json");
28
+ /** How long a cached "latest" answer is trusted before we re-hit the registry. */
29
+ export const DEFAULT_TTL_MS = 12 * 60 * 60 * 1000; // 12h
30
+ /** Registry fetch timeout — this runs on every status/doctor call, so it must stay short. */
31
+ export const DEFAULT_TIMEOUT_MS = 3000;
32
+ function readCacheFile(path) {
33
+ try {
34
+ if (!existsSync(path))
35
+ return null;
36
+ const raw = JSON.parse(readFileSync(path, "utf-8"));
37
+ if (typeof raw?.latest === "string" && typeof raw?.checkedAt === "number") {
38
+ return { latest: raw.latest, checkedAt: raw.checkedAt };
39
+ }
40
+ return null;
41
+ }
42
+ catch {
43
+ // Corrupt/unreadable cache — treat as absent, never throw.
44
+ return null;
45
+ }
46
+ }
47
+ function writeCacheFile(path, entry) {
48
+ try {
49
+ mkdirSync(dirname(path), { recursive: true });
50
+ writeFileSync(path, JSON.stringify(entry), "utf-8");
51
+ }
52
+ catch {
53
+ // Best-effort — a cache-write failure must never surface as a
54
+ // status/doctor error (e.g. read-only $HOME).
55
+ }
56
+ }
57
+ async function defaultFetchLatest(timeoutMs) {
58
+ try {
59
+ const res = await fetch(`https://registry.npmjs.org/${FLAIR_PKG_NAME}/latest`, {
60
+ signal: AbortSignal.timeout(timeoutMs),
61
+ });
62
+ if (!res.ok)
63
+ return null;
64
+ const data = (await res.json());
65
+ return typeof data?.version === "string" ? data.version : null;
66
+ }
67
+ catch {
68
+ // Offline, DNS failure, timeout, registry 5xx, bad JSON — all the same:
69
+ // we couldn't determine "latest" over the network this time.
70
+ return null;
71
+ }
72
+ }
73
+ export function defaultVersionCheckDeps() {
74
+ return {
75
+ fetchLatest: defaultFetchLatest,
76
+ cachePath: DEFAULT_CACHE_PATH,
77
+ ttlMs: DEFAULT_TTL_MS,
78
+ timeoutMs: DEFAULT_TIMEOUT_MS,
79
+ now: () => Date.now(),
80
+ readCache: readCacheFile,
81
+ writeCache: writeCacheFile,
82
+ };
83
+ }
84
+ /**
85
+ * Resolve the latest published @tpsdev-ai/flair version, preferring a fresh
86
+ * cache hit over a network round trip, and falling back to a stale cache (or
87
+ * giving up quietly) when the registry is unreachable. NEVER throws.
88
+ */
89
+ export async function checkVersion(installed, injected = {}) {
90
+ const deps = { ...defaultVersionCheckDeps(), ...injected };
91
+ const nowMs = deps.now();
92
+ const cached = deps.readCache(deps.cachePath);
93
+ if (cached && nowMs - cached.checkedAt < deps.ttlMs) {
94
+ return { installed, latest: cached.latest, source: "cache" };
95
+ }
96
+ // Defense-in-depth: the default fetchLatest already catches everything
97
+ // internally (network error, timeout, non-2xx, bad JSON) and resolves
98
+ // null rather than rejecting. This try/catch guards the contract even if
99
+ // a caller-injected fetchLatest misbehaves and throws/rejects instead —
100
+ // status/doctor must never crash or hang on a version check either way.
101
+ let fetched = null;
102
+ try {
103
+ fetched = await deps.fetchLatest(deps.timeoutMs);
104
+ }
105
+ catch {
106
+ fetched = null;
107
+ }
108
+ if (fetched) {
109
+ deps.writeCache(deps.cachePath, { latest: fetched, checkedAt: nowMs });
110
+ return { installed, latest: fetched, source: "network" };
111
+ }
112
+ // Registry unreachable/timed out — fall back to a stale cache rather than
113
+ // reporting nothing, but never block or throw trying to get a fresh one.
114
+ if (cached) {
115
+ return { installed, latest: cached.latest, source: "cache" };
116
+ }
117
+ return { installed, latest: null, source: "unavailable" };
118
+ }
119
+ const NO_GAP = { severity: "none", majorBehind: false, releasesBehind: 0 };
120
+ /**
121
+ * Classify how far `installed` is behind `latest` using major.minor.patch
122
+ * math only — we don't have advisory data, so:
123
+ * - any major version behind, or ≥2 minor versions behind → "red" (loud;
124
+ * heuristic for "you've likely missed a security fix")
125
+ * - a single minor version behind, or a patch-only gap → "yellow"
126
+ * - equal, ahead, or unparseable → "none"
127
+ */
128
+ export function classifyGap(installed, latest) {
129
+ const a = parseSemverCore(installed);
130
+ const b = parseSemverCore(latest);
131
+ if (!a || !b)
132
+ return NO_GAP;
133
+ const [aMaj, aMin, aPatch] = a;
134
+ const [bMaj, bMin, bPatch] = b;
135
+ if (bMaj > aMaj)
136
+ return { severity: "red", majorBehind: true, releasesBehind: 0 };
137
+ if (bMaj < aMaj)
138
+ return NO_GAP; // installed is ahead (e.g. local/pre-release build)
139
+ if (bMin > aMin) {
140
+ const releasesBehind = bMin - aMin;
141
+ return { severity: releasesBehind >= 2 ? "red" : "yellow", majorBehind: false, releasesBehind };
142
+ }
143
+ if (bMin < aMin)
144
+ return NO_GAP; // ahead on minor
145
+ if (bPatch > aPatch)
146
+ return { severity: "yellow", majorBehind: false, releasesBehind: bPatch - aPatch };
147
+ return NO_GAP; // equal, or ahead on patch
148
+ }
149
+ /**
150
+ * Build the human-readable nudge line for `flair status`/`flair doctor`, or
151
+ * null when there's nothing worth printing — current, ahead (local/dev
152
+ * build), or we couldn't determine latest at all (offline with no cache).
153
+ * Callers own icon/color; this returns plain text plus a severity to color by.
154
+ */
155
+ export function formatVersionNudge(result) {
156
+ if (!result.latest)
157
+ return null;
158
+ const gap = classifyGap(result.installed, result.latest);
159
+ if (gap.severity === "none")
160
+ return null;
161
+ const countHint = gap.majorBehind
162
+ ? "major version"
163
+ : `${gap.releasesBehind} release${gap.releasesBehind === 1 ? "" : "s"}`;
164
+ const message = `flair ${result.installed} is behind — latest is ${result.latest} (${countHint} behind). ` +
165
+ `Upgrade: npm i -g ${FLAIR_PKG_NAME}@latest`;
166
+ return { severity: gap.severity, message };
167
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpsdev-ai/flair",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "Identity, memory, and soul for AI agents. Cryptographic identity (Ed25519), semantic memory with local embeddings, and persistent personality — all in a single process.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -55,7 +55,7 @@
55
55
  "node": ">=22"
56
56
  },
57
57
  "dependencies": {
58
- "@harperfast/harper": "5.1.14",
58
+ "@harperfast/harper": "5.1.15",
59
59
  "@harperfast/oauth": "2.1.0",
60
60
  "@types/js-yaml": "4.0.9",
61
61
  "commander": "14.0.3",
@@ -22,6 +22,13 @@ type Agent @table(database: "flair") @export {
22
22
 
23
23
  createdAt: String!
24
24
  updatedAt: String
25
+ originatorInstanceId: String @indexed # federation-edge-hardening slice 1 — write-time instance identity,
26
+ # stamped server-side in resources/Agent.ts's post()/put() from
27
+ # resources/instance-identity.ts's localInstanceId(). Nullable (existing rows
28
+ # read null = clean upgrade); preserved (never re-stamped) as the record flows
29
+ # through federation sync merges. See schemas/memory.graphql's Memory.
30
+ # originatorInstanceId for the full contract — same field, same idiom, this is
31
+ # one of the 4 synced tables (Memory/Soul/Agent/Relationship).
25
32
  }
26
33
 
27
34
  # Credential table — auth surfaces for Principals.
@@ -39,6 +39,18 @@ type Peer @table(database: "flair") @export {
39
39
  updatedAt: String
40
40
  }
41
41
 
42
+ # Anti-replay nonce store (federation-edge-hardening slice 4).
43
+ # Persists nonces recorded by verifyBodySignatureFresh (federation-crypto.ts)
44
+ # so an instance restart doesn't wipe recently-seen nonces — closes the
45
+ # ±30s freshness-window replay gap a restart would otherwise reopen. Nonces
46
+ # are already globally unique (128-bit random, generated per request by
47
+ # signBodyFresh), so the nonce string itself is the primary key — no scope
48
+ # prefix needed. Internal bookkeeping only: never served, never replicated.
49
+ type Nonce @table(database: "flair") {
50
+ id: ID @primaryKey # the nonce string (base64url, 128-bit random)
51
+ seenAt: Int! @indexed # ms-epoch when first recorded — eviction cutoff
52
+ }
53
+
42
54
  # Sync log — audit trail for federation operations.
43
55
  type SyncLog @table(database: "flair") {
44
56
  id: ID @primaryKey
@@ -31,6 +31,16 @@ type Memory @table(database: "flair") {
31
31
  validFrom: String @indexed # ISO timestamp — when this fact became true
32
32
  validTo: String @indexed # ISO timestamp — when this fact stopped being true (null = still valid)
33
33
  _safetyFlags: [String] # content safety flags from scanContent()
34
+ provenance: String # JSON blob (memory-provenance slice 1): { v, verified: { agentId, timestamp }, claimed?: { model } }
35
+ # Nullable by design — existing rows read back provenance = null, unchanged behavior (clean-upgrade-path gate).
36
+ # Same idiom as Soul.metadata below. Stamped server-side in resources/Memory.ts; never client-writable.
37
+ originatorInstanceId: String @indexed # federation-edge-hardening slice 1: WRITE-TIME instance identity, stamped server-side in
38
+ # resources/Memory.ts from resources/instance-identity.ts's localInstanceId() — never client-writable.
39
+ # Nullable (existing rows read null = clean upgrade). Distinct from the legacy _originatorInstanceId
40
+ # (Federation.ts's mergeRecord — stamped by the RECEIVER at merge time, forgeable/lossy); this field is
41
+ # stamped by the ORIGINATING instance itself and preserved (never re-stamped) as the record flows through
42
+ # sync merges. @indexed for the later sync push-query filter (per-record signature verification and the
43
+ # classifier org-gate are separate, later slices — not built here).
34
44
  }
35
45
 
36
46
  # Explicit entity-to-entity relationships with temporal validity.
@@ -48,6 +58,9 @@ type Relationship @table(database: "flair") {
48
58
  source: String # where this was learned (memory ID, conversation, etc.)
49
59
  createdAt: String! @indexed
50
60
  updatedAt: String
61
+ originatorInstanceId: String @indexed # federation-edge-hardening slice 1 — see Memory.originatorInstanceId's doc above
62
+ # for the full contract (write-time stamp, nullable, preserved across sync merges).
63
+ # Stamped server-side in resources/Relationship.ts's put().
51
64
  }
52
65
 
53
66
  type Soul @table(database: "flair") {
@@ -60,6 +73,9 @@ type Soul @table(database: "flair") {
60
73
  durability: String @indexed
61
74
  createdAt: String!
62
75
  updatedAt: String
76
+ originatorInstanceId: String @indexed # federation-edge-hardening slice 1 — see Memory.originatorInstanceId's doc above
77
+ # for the full contract (write-time stamp, nullable, preserved across sync merges).
78
+ # Stamped server-side in resources/Soul.ts's post()/put().
63
79
  }
64
80
 
65
81
  type MemoryGrant @table(database: "flair") @export {