@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.
- package/README.md +29 -4
- package/SECURITY.md +28 -18
- package/dist/cli.js +482 -32
- package/dist/deploy.js +280 -18
- package/dist/doctor-client.js +312 -0
- package/dist/install/clients.js +18 -0
- package/dist/rem/restore.js +1 -1
- 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/Federation.js +98 -2
- package/dist/resources/IngestEvents.js +1 -1
- package/dist/resources/Integration.js +1 -1
- package/dist/resources/Memory.js +123 -17
- package/dist/resources/MemoryBootstrap.js +46 -36
- package/dist/resources/MemoryGrant.js +1 -1
- package/dist/resources/OAuth.js +61 -4
- package/dist/resources/OrgEventCatchup.js +1 -1
- package/dist/resources/Presence.js +55 -3
- package/dist/resources/Relationship.js +10 -1
- package/dist/resources/SemanticSearch.js +14 -14
- package/dist/resources/Soul.js +14 -0
- package/dist/resources/WorkspaceLatest.js +1 -1
- package/dist/resources/WorkspaceState.js +1 -1
- package/dist/resources/agent-auth.js +1 -1
- package/dist/resources/agentcard-fields.js +2 -2
- package/dist/resources/auth-middleware.js +24 -5
- package/dist/resources/bm25-filter.js +1 -1
- package/dist/resources/bm25.js +1 -1
- package/dist/resources/dedup.js +2 -2
- package/dist/resources/ed25519-auth.js +2 -2
- package/dist/resources/embeddings-provider.js +1 -1
- package/dist/resources/federation-nonce-store.js +195 -0
- package/dist/resources/instance-identity.js +53 -0
- package/dist/resources/memory-bootstrap-lib.js +1 -1
- package/dist/resources/memory-read-scope.js +58 -71
- package/dist/resources/memory-visibility.js +37 -0
- package/dist/version-check.js +167 -0
- package/package.json +2 -2
- package/schemas/agent.graphql +7 -0
- package/schemas/federation.graphql +12 -0
- 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.
|
|
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.
|
|
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",
|
package/schemas/agent.graphql
CHANGED
|
@@ -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
|
package/schemas/memory.graphql
CHANGED
|
@@ -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 {
|