@tpsdev-ai/flair 0.9.0 → 0.10.1
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 +17 -19
- package/dist/cli.js +1480 -437
- package/dist/rem/restore.js +62 -2
- package/dist/render.js +168 -0
- package/dist/resources/A2AAdapter.js +33 -3
- package/dist/resources/AdminInstance.js +44 -8
- package/dist/resources/AgentCard.js +5 -14
- package/dist/resources/Federation.js +64 -33
- package/dist/resources/agentcard-fields.js +40 -0
- package/dist/resources/auth-middleware.js +37 -29
- package/dist/resources/federation-classify.js +29 -0
- package/dist/resources/health.js +12 -7
- package/package.json +11 -2
- package/schemas/federation.graphql +6 -3
- package/schemas/schema.graphql +1 -1
|
@@ -110,12 +110,18 @@ async function backfillEmbedding(memoryId) {
|
|
|
110
110
|
// ─── HTTP middleware ──────────────────────────────────────────────────────────
|
|
111
111
|
server.http(async (request, nextLayer) => {
|
|
112
112
|
const url = new URL(request.url, "http://" + (request.headers.get("host") || "localhost"));
|
|
113
|
+
// A2A discovery endpoints: GET returns public agent-card metadata (per
|
|
114
|
+
// A2A spec, cards are intentionally public). POST invokes JSON-RPC
|
|
115
|
+
// actions (message/send writes OrgEvents on behalf of agents,
|
|
116
|
+
// tasks/list reads Beads issues, message/stream subscribes to
|
|
117
|
+
// OrgEvents) — those must be authenticated. Narrowing to GET-only
|
|
118
|
+
// closes the P0 where any caller could forge OrgEvents as any agent
|
|
119
|
+
// and read all internal Beads issues unauthenticated.
|
|
120
|
+
const isA2APath = url.pathname === "/a2a" || url.pathname === "/A2AAdapter" || url.pathname.startsWith("/A2AAdapter/");
|
|
113
121
|
if (url.pathname === "/health" ||
|
|
114
122
|
url.pathname === "/Health" ||
|
|
115
|
-
|
|
116
|
-
url.pathname === "/A2AAdapter" ||
|
|
123
|
+
(request.method === "GET" && isA2APath) ||
|
|
117
124
|
url.pathname === "/AgentCard" ||
|
|
118
|
-
url.pathname.startsWith("/A2AAdapter/") ||
|
|
119
125
|
url.pathname.startsWith("/AgentCard/") ||
|
|
120
126
|
// FederationSync uses Ed25519 body-signature auth with anti-replay, validated
|
|
121
127
|
// by the resource handler (allowCreate=true, same pattern as FederationPair).
|
|
@@ -136,7 +142,7 @@ server.http(async (request, nextLayer) => {
|
|
|
136
142
|
// and inline JS, with no embedded data. The JS prompts for admin-pass and
|
|
137
143
|
// auths every API call (/Agent, /SemanticSearch, /FederationPeers, etc).
|
|
138
144
|
// Without this allow-list entry, the HTML is 401-blocked on hosted Flair
|
|
139
|
-
// instances (
|
|
145
|
+
// instances (a localhost caller works only because authorizeLocal=true).
|
|
140
146
|
url.pathname === "/ObservationCenter")
|
|
141
147
|
return nextLayer(request);
|
|
142
148
|
// If Harper has already authorized this request (e.g. authorizeLocal=true on localhost),
|
|
@@ -266,32 +272,34 @@ server.http(async (request, nextLayer) => {
|
|
|
266
272
|
request.tpsAgent = agentId;
|
|
267
273
|
request._tpsAuthVerified = true;
|
|
268
274
|
request.tpsAgentIsAdmin = await isAdmin(agentId);
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
275
|
+
// Grant Harper-level permissions for the cryptographically-verified agent by
|
|
276
|
+
// setting request.user directly to the admin super_user.
|
|
277
|
+
//
|
|
278
|
+
// Prior approach swapped the Authorization header to "Basic admin:<pass>" so
|
|
279
|
+
// Harper's auth pipeline would re-authenticate with full (HNSW-capable)
|
|
280
|
+
// permissions. That silently broke under Harper 5.0.9: its authentication
|
|
281
|
+
// layer (security/auth.ts `authentication()`) reads the Authorization header
|
|
282
|
+
// and resolves request.user BEFORE invoking this middleware via nextHandler.
|
|
283
|
+
// A TPS-Ed25519 header matches no Basic/Bearer strategy, so Harper sets
|
|
284
|
+
// request.user = null and never re-reads the header — our post-hoc swap was
|
|
285
|
+
// ignored, the request ran unauthenticated, and Harper surfaced it downstream
|
|
286
|
+
// as a generic "Login failed" 401. (Broke at the 2026-05-27 daemon restart,
|
|
287
|
+
// which loaded 5.0.9 fresh; the prior long-running process held an older
|
|
288
|
+
// in-memory Harper where the late header read still worked.)
|
|
289
|
+
//
|
|
290
|
+
// Setting request.user directly is the supported extension path and is honored
|
|
291
|
+
// by all downstream resources, including HNSW vector search. getUser(admin,
|
|
292
|
+
// null) looks up the admin record WITHOUT password validation — safe here
|
|
293
|
+
// because the Ed25519 signature verified above already proves agent identity
|
|
294
|
+
// cryptographically; no Basic credential is presented. This preserves the
|
|
295
|
+
// prior privilege model (verified agents act with admin perms; per-agent data
|
|
296
|
+
// isolation is still enforced downstream via the x-tps-agent header below).
|
|
297
|
+
try {
|
|
298
|
+
request.user = await server.getUser("admin", null, request);
|
|
288
299
|
}
|
|
289
|
-
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
request.user = await server.getUser("admin", null, request);
|
|
293
|
-
}
|
|
294
|
-
catch { }
|
|
300
|
+
catch {
|
|
301
|
+
// Admin record unavailable — request proceeds as the verified tpsAgent
|
|
302
|
+
// without elevated perms; resource-level scoping still applies.
|
|
295
303
|
}
|
|
296
304
|
// Propagate authenticated agent to downstream resources via header.
|
|
297
305
|
// Resources can read this to enforce agent-level scoping.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure classifier for federation sync records — no Harper imports.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from Federation.ts so the decision can be unit-tested without
|
|
5
|
+
* spinning up Harper's database module. The same SkipReason names are used
|
|
6
|
+
* in SyncLog.skippedReasons so operators can grep for them.
|
|
7
|
+
*/
|
|
8
|
+
export function classifyRecord(record, peerRole, receiverInstanceId, local, knownTables, now = new Date()) {
|
|
9
|
+
if (!knownTables.has(record.table)) {
|
|
10
|
+
return { action: "skip", reason: "unknown_table" };
|
|
11
|
+
}
|
|
12
|
+
const originator = record.originatorInstanceId ?? receiverInstanceId;
|
|
13
|
+
if (originator !== receiverInstanceId && peerRole !== "hub") {
|
|
14
|
+
return { action: "skip", reason: "non_originator" };
|
|
15
|
+
}
|
|
16
|
+
const fiveMinFromNow = new Date(now.getTime() + 5 * 60 * 1000).toISOString();
|
|
17
|
+
if (record.updatedAt > fiveMinFromNow) {
|
|
18
|
+
return { action: "skip", reason: "future_timestamp" };
|
|
19
|
+
}
|
|
20
|
+
const remoteContentHash = record.data?.contentHash;
|
|
21
|
+
if (local &&
|
|
22
|
+
local.contentHash &&
|
|
23
|
+
remoteContentHash &&
|
|
24
|
+
local.contentHash === remoteContentHash &&
|
|
25
|
+
record.updatedAt <= (local.updatedAt ?? "")) {
|
|
26
|
+
return { action: "skip", reason: "no_op_same_hash" };
|
|
27
|
+
}
|
|
28
|
+
return { action: "merge", originator };
|
|
29
|
+
}
|
package/dist/resources/health.js
CHANGED
|
@@ -23,10 +23,10 @@ const exists = async (path) => {
|
|
|
23
23
|
* which is what makes /Health work for callers outside `authorizeLocal`'s
|
|
24
24
|
* localhost-bypass. Without this, Harper's intrinsic Basic-auth gate fires
|
|
25
25
|
* BEFORE our HTTP middleware can apply the /Health bypass list in
|
|
26
|
-
* auth-middleware.ts, so remote callers (e.g., from
|
|
26
|
+
* auth-middleware.ts, so remote callers (e.g., from a remote host hitting a Fabric-
|
|
27
27
|
* hosted Flair) get a 401 even though the Resource handler is intentionally
|
|
28
28
|
* unauthenticated. Symptom matched: Fabric returned 401 + WWW-Authenticate:
|
|
29
|
-
* Basic on /Health while
|
|
29
|
+
* Basic on /Health while a localhost caller returned 200 — the difference was
|
|
30
30
|
* Harper's localhost-auto-auth, not anything in our code.
|
|
31
31
|
*
|
|
32
32
|
* Same pattern as `FederationPair.allowCreate(){ return true }` (PR #299):
|
|
@@ -201,13 +201,18 @@ export class HealthDetail extends Resource {
|
|
|
201
201
|
for await (const s of db.flair.Soul.search({}))
|
|
202
202
|
souls.push(s);
|
|
203
203
|
stats.soulEntries = souls.length;
|
|
204
|
-
|
|
204
|
+
// Soul entries have no severity dimension — they are keyed identity facts
|
|
205
|
+
// (role / project / standards / …). A per-priority breakdown was dead
|
|
206
|
+
// telemetry: nothing writes Soul.priority to anything but "standard", and
|
|
207
|
+
// the `?? "standard"` fallback also mislabelled *unset* as *standard*, so
|
|
208
|
+
// it always read 100% standard regardless of the data. Report the honest
|
|
209
|
+
// dimension instead — a count per key.
|
|
210
|
+
const byKey = {};
|
|
205
211
|
for (const s of souls) {
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
byPriority[p]++;
|
|
212
|
+
const k = (s.key ?? "(unkeyed)");
|
|
213
|
+
byKey[k] = (byKey[k] ?? 0) + 1;
|
|
209
214
|
}
|
|
210
|
-
stats.soul = { total: souls.length,
|
|
215
|
+
stats.soul = { total: souls.length, byKey };
|
|
211
216
|
}
|
|
212
217
|
catch {
|
|
213
218
|
stats.soulEntries = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/flair",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
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.0.
|
|
58
|
+
"@harperfast/harper": "5.0.21",
|
|
59
59
|
"@types/js-yaml": "^4.0.9",
|
|
60
60
|
"commander": "14.0.3",
|
|
61
61
|
"harper-fabric-embeddings": "0.2.3",
|
|
@@ -64,6 +64,15 @@
|
|
|
64
64
|
"tar": "^7.5.13",
|
|
65
65
|
"tweetnacl": "1.0.3"
|
|
66
66
|
},
|
|
67
|
+
"optionalDependencies": {
|
|
68
|
+
"@node-llama-cpp/linux-arm64": "3.18.1",
|
|
69
|
+
"@node-llama-cpp/linux-armv7l": "3.18.1",
|
|
70
|
+
"@node-llama-cpp/linux-x64": "3.18.1",
|
|
71
|
+
"@node-llama-cpp/mac-arm64-metal": "3.18.1",
|
|
72
|
+
"@node-llama-cpp/mac-x64": "3.18.1",
|
|
73
|
+
"@node-llama-cpp/win-arm64": "3.18.1",
|
|
74
|
+
"@node-llama-cpp/win-x64": "3.18.1"
|
|
75
|
+
},
|
|
67
76
|
"devDependencies": {
|
|
68
77
|
"@playwright/test": "^1.59.1",
|
|
69
78
|
"@types/node": "24.11.0",
|
|
@@ -30,7 +30,8 @@ type Peer @table(database: "flair") @export {
|
|
|
30
30
|
role: String @indexed # "hub" | "spoke"
|
|
31
31
|
endpoint: String # wss:// URL for the peer
|
|
32
32
|
status: String @indexed # "paired" | "connected" | "disconnected" | "revoked"
|
|
33
|
-
lastSyncAt: String # last
|
|
33
|
+
lastSyncAt: String # last contact with this peer (liveness signal)
|
|
34
|
+
lastMergeAt: String # last sync where merged > 0 (data-progress signal)
|
|
34
35
|
lastSyncCursor: String # Lamport clock or ISO timestamp for incremental sync
|
|
35
36
|
relayOnly: Boolean # true for spoke-to-spoke peers announced by hub
|
|
36
37
|
pairedAt: String
|
|
@@ -43,9 +44,11 @@ type SyncLog @table(database: "flair") {
|
|
|
43
44
|
id: ID @primaryKey
|
|
44
45
|
peerId: String! @indexed # which peer this sync was with
|
|
45
46
|
direction: String! @indexed # "push" | "pull"
|
|
46
|
-
recordCount: Int # how many records in this
|
|
47
|
+
recordCount: Int # how many records merged in this batch
|
|
48
|
+
skippedCount: Int # how many records skipped (sum of skippedReasons)
|
|
49
|
+
skippedReasons: String # JSON: { unknown_table: N, non_originator: N, future_timestamp: N, no_op_same_hash: N, merge_error: N }
|
|
47
50
|
status: String @indexed # "success" | "partial" | "failed"
|
|
48
|
-
error: String #
|
|
51
|
+
error: String # human-readable summary if partial/failed (includes per-record error messages, capped)
|
|
49
52
|
durationMs: Int # how long the sync took
|
|
50
53
|
createdAt: String! @indexed
|
|
51
54
|
}
|
package/schemas/schema.graphql
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# ─── Observatory tables ───────────────────────────────────────────────────────
|
|
3
3
|
|
|
4
4
|
type ObsOffice @table(database: "flair") @export {
|
|
5
|
-
id: ID @primaryKey # e.g. "
|
|
5
|
+
id: ID @primaryKey # e.g. "office1"
|
|
6
6
|
name: String!
|
|
7
7
|
publicKey: String! # Ed25519 hex public key for auth verification
|
|
8
8
|
status: String @indexed # "online" | "offline" | "degraded"
|