@tpsdev-ai/flair 0.23.0 → 0.24.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/dist/cli.js +1278 -410
- package/dist/deploy.js +16 -1
- package/dist/hook-install.js +324 -0
- package/dist/lib/auth-resolve.js +369 -0
- package/dist/lib/mcp-enable.js +787 -0
- package/dist/resources/Memory.js +18 -0
- package/dist/resources/Presence.js +4 -4
- package/dist/resources/agent-auth.js +4 -5
- package/dist/resources/auth-middleware.js +4 -4
- package/dist/resources/ed25519-auth.js +37 -0
- package/dist/resources/embeddings-boot.js +38 -1
- package/package.json +3 -3
package/dist/resources/Memory.js
CHANGED
|
@@ -291,8 +291,26 @@ async function hasWriteGrant(granteeId, ownerId) {
|
|
|
291
291
|
* requires a "write" MemoryGrant from the target's owner (reuses the existing
|
|
292
292
|
* agent-auth/grant machinery — no parallel auth logic). Returns a Response to
|
|
293
293
|
* short-circuit with (400/403), or null to continue.
|
|
294
|
+
*
|
|
295
|
+
* flair#704: an explicit `supersedes: null` — the shape most JSON writers
|
|
296
|
+
* produce for an unset optional field (`JSON.stringify({supersedes: undefined})`
|
|
297
|
+
* drops the key, but plenty of writers instead do `{supersedes: x ?? null}`)
|
|
298
|
+
* — must be treated identically to the key being OMITTED, per the
|
|
299
|
+
* additive-schema convention (flair#695: an explicit null on an
|
|
300
|
+
* optional/nullable field reads as absent, not as a distinct value). Fixed by
|
|
301
|
+
* deleting the key BEFORE the type check below, so (a) the check never
|
|
302
|
+
* rejects it, and (b) `super.put()`/`super.post()` — Harper full-record
|
|
303
|
+
* replacement, see table-helpers.ts's header comment — never persists a
|
|
304
|
+
* literal `null` where "absent" was intended: the stored row ends up
|
|
305
|
+
* byte-for-byte identical to the omitted-key case, so every downstream
|
|
306
|
+
* `!content.supersedes` / `content.supersedes &&` check below (and in
|
|
307
|
+
* closeSupersededIfNeeded) already treats it as unset with no further
|
|
308
|
+
* changes needed.
|
|
294
309
|
*/
|
|
295
310
|
async function validateAndAuthorizeSupersedes(content, auth) {
|
|
311
|
+
if (content.supersedes === null) {
|
|
312
|
+
delete content.supersedes;
|
|
313
|
+
}
|
|
296
314
|
if (content.supersedes !== undefined && typeof content.supersedes !== "string") {
|
|
297
315
|
return new Response(JSON.stringify({ error: "supersedes must be a string (memory ID)" }), {
|
|
298
316
|
status: 400, headers: { "Content-Type": "application/json" },
|
|
@@ -32,7 +32,7 @@ import { dirname, join } from "node:path";
|
|
|
32
32
|
import { fileURLToPath } from "node:url";
|
|
33
33
|
import { createRequire } from "node:module";
|
|
34
34
|
import { resolveAgentAuth, verifyAgentRequest } from "./agent-auth.js";
|
|
35
|
-
import { WINDOW_MS, isNonceReplay, recordNonce, importEd25519Key, b64ToArrayBuffer } from "./ed25519-auth.js";
|
|
35
|
+
import { WINDOW_MS, isNonceReplay, recordNonce, importEd25519Key, b64ToArrayBuffer, parseTpsEd25519Header } from "./ed25519-auth.js";
|
|
36
36
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
37
37
|
const CURRENT_TASK_MAX_LENGTH = 200;
|
|
38
38
|
const VALID_ACTIVITIES = new Set(["coding", "reviewing", "planning", "debugging", "idle"]);
|
|
@@ -409,11 +409,11 @@ export class Presence extends databases.flair.Presence {
|
|
|
409
409
|
agentId = middlewareAgent;
|
|
410
410
|
}
|
|
411
411
|
else {
|
|
412
|
-
const
|
|
413
|
-
if (!
|
|
412
|
+
const parsed = parseTpsEd25519Header(authHeader);
|
|
413
|
+
if (!parsed) {
|
|
414
414
|
return new Response(JSON.stringify({ error: "Ed25519 agent auth required for heartbeat" }), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
415
415
|
}
|
|
416
|
-
const
|
|
416
|
+
const { agentId: headerAgentId, tsRaw, nonce, signatureB64: sigB64 } = parsed;
|
|
417
417
|
const ts = Number(tsRaw);
|
|
418
418
|
const now = Date.now();
|
|
419
419
|
if (!Number.isFinite(ts) || Math.abs(now - ts) > WINDOW_MS) {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* 30s timestamp window + a per-(agent,nonce) seen-set pruned to that window.
|
|
17
17
|
*/
|
|
18
18
|
import { databases } from "@harperfast/harper";
|
|
19
|
-
import { WINDOW_MS, isNonceReplay, recordNonce, importEd25519Key, b64ToArrayBuffer } from "./ed25519-auth.js";
|
|
19
|
+
import { WINDOW_MS, isNonceReplay, recordNonce, importEd25519Key, b64ToArrayBuffer, parseTpsEd25519Header } from "./ed25519-auth.js";
|
|
20
20
|
/**
|
|
21
21
|
* Shared Harper user that verified Ed25519 agents resolve to (least-privilege
|
|
22
22
|
* `flair_agent` role), replacing the old admin super_user elevation. Single
|
|
@@ -56,15 +56,14 @@ async function getAdminAgents() {
|
|
|
56
56
|
export async function isAdmin(agentId) {
|
|
57
57
|
return (await getAdminAgents()).has(agentId);
|
|
58
58
|
}
|
|
59
|
-
const HEADER_RE = /^TPS-Ed25519\s+([^:]+):(\d+):([^:]+):(.+)$/;
|
|
60
59
|
async function doVerify(request) {
|
|
61
60
|
const header = request?.headers?.get?.("authorization") ??
|
|
62
61
|
request?.headers?.asObject?.authorization ??
|
|
63
62
|
"";
|
|
64
|
-
const
|
|
65
|
-
if (!
|
|
63
|
+
const parsed = parseTpsEd25519Header(header);
|
|
64
|
+
if (!parsed)
|
|
66
65
|
return null;
|
|
67
|
-
const
|
|
66
|
+
const { agentId, tsRaw, nonce, signatureB64 } = parsed;
|
|
68
67
|
const ts = Number(tsRaw);
|
|
69
68
|
const now = Date.now();
|
|
70
69
|
if (!Number.isFinite(ts) || Math.abs(now - ts) > WINDOW_MS)
|
|
@@ -2,7 +2,7 @@ import { patchRecord } from "./table-helpers.js";
|
|
|
2
2
|
import { server, databases } from "@harperfast/harper";
|
|
3
3
|
import { getEmbedding } from "./embeddings-provider.js";
|
|
4
4
|
import { isAdmin, FLAIR_AGENT_USERNAME } from "./agent-auth.js";
|
|
5
|
-
import { WINDOW_MS, isNonceReplay, recordNonce, importEd25519Key, b64ToArrayBuffer } from "./ed25519-auth.js";
|
|
5
|
+
import { WINDOW_MS, isNonceReplay, recordNonce, importEd25519Key, b64ToArrayBuffer, parseTpsEd25519Header } from "./ed25519-auth.js";
|
|
6
6
|
import { resolveReadScope } from "./memory-read-scope.js";
|
|
7
7
|
// --- Admin credentials ---
|
|
8
8
|
// Admin auth is sourced exclusively from Harper's own environment variables
|
|
@@ -230,8 +230,8 @@ server.http(async (request, nextLayer) => {
|
|
|
230
230
|
return nextLayer(request);
|
|
231
231
|
}
|
|
232
232
|
// ── Ed25519 agent auth ────────────────────────────────────────────────────
|
|
233
|
-
const
|
|
234
|
-
if (!
|
|
233
|
+
const parsed = parseTpsEd25519Header(header);
|
|
234
|
+
if (!parsed) {
|
|
235
235
|
// For browser-accessible admin pages, emit `WWW-Authenticate: Basic` so
|
|
236
236
|
// the browser shows a native auth dialog instead of a bare 401 page.
|
|
237
237
|
// JSON API endpoints don't get this — they should keep the structured
|
|
@@ -260,7 +260,7 @@ server.http(async (request, nextLayer) => {
|
|
|
260
260
|
catch { /* frozen headers — annotation on the request object still applies */ }
|
|
261
261
|
return nextLayer(request);
|
|
262
262
|
}
|
|
263
|
-
const
|
|
263
|
+
const { agentId, tsRaw, nonce, signatureB64 } = parsed;
|
|
264
264
|
const ts = Number(tsRaw);
|
|
265
265
|
const now = Date.now();
|
|
266
266
|
if (!Number.isFinite(ts) || Math.abs(now - ts) > WINDOW_MS)
|
|
@@ -42,6 +42,43 @@ export { b64ToArrayBuffer };
|
|
|
42
42
|
* stated "plugin-shaped, config via env" design intent.
|
|
43
43
|
*/
|
|
44
44
|
export const WINDOW_MS = Number(process.env.FLAIR_AGENT_AUTH_WINDOW_MS) || 30_000;
|
|
45
|
+
// ─── Auth header parsing (single shared implementation) ────────────────────
|
|
46
|
+
//
|
|
47
|
+
// One parser for the `Authorization: TPS-Ed25519 <id>:<ts>:<nonce>:<sig>`
|
|
48
|
+
// header, used by all 3 call sites (auth-middleware.ts, agent-auth.ts,
|
|
49
|
+
// Presence.ts) so the grammar and its input bounds can't drift.
|
|
50
|
+
/**
|
|
51
|
+
* Upper bound on the accepted Authorization header length. A well-formed
|
|
52
|
+
* TPS-Ed25519 header (`TPS-Ed25519 <id>:<ts>:<nonce>:<sig>`) is a few hundred
|
|
53
|
+
* chars at most; anything materially larger is malformed. The header is
|
|
54
|
+
* untrusted client input, so we cap its length before running the regex,
|
|
55
|
+
* keeping parse cost bounded regardless of the input's shape.
|
|
56
|
+
*/
|
|
57
|
+
export const MAX_AUTH_HEADER_LEN = 4096;
|
|
58
|
+
/**
|
|
59
|
+
* TPS-Ed25519 auth header grammar.
|
|
60
|
+
*
|
|
61
|
+
* The two colon-delimited text captures use `[^:\s]+` (not `[^:]+`) so they are
|
|
62
|
+
* disjoint from the preceding `\s+`: with no character shared between the two
|
|
63
|
+
* adjacent quantifiers there is exactly one way to split any input, so matching
|
|
64
|
+
* is linear-time on every input (including long, degenerate ones). A real
|
|
65
|
+
* agentId / nonce / signature never contains whitespace, so excluding it is
|
|
66
|
+
* behavior-preserving for well-formed headers.
|
|
67
|
+
*/
|
|
68
|
+
export const TPS_ED25519_HEADER_RE = /^TPS-Ed25519\s+([^:\s]+):(\d+):([^:\s]+):(.+)$/;
|
|
69
|
+
/**
|
|
70
|
+
* Parse a TPS-Ed25519 `Authorization` header value into its fields, or return
|
|
71
|
+
* null if it is over `MAX_AUTH_HEADER_LEN` or doesn't match the grammar.
|
|
72
|
+
* Callers treat null exactly as a header that carries no valid agent auth.
|
|
73
|
+
*/
|
|
74
|
+
export function parseTpsEd25519Header(header) {
|
|
75
|
+
if (!header || header.length > MAX_AUTH_HEADER_LEN)
|
|
76
|
+
return null;
|
|
77
|
+
const m = TPS_ED25519_HEADER_RE.exec(header);
|
|
78
|
+
if (!m)
|
|
79
|
+
return null;
|
|
80
|
+
return { agentId: m[1], tsRaw: m[2], nonce: m[3], signatureB64: m[4] };
|
|
81
|
+
}
|
|
45
82
|
// ─── Replay guard (single shared instance) ─────────────────────────────────
|
|
46
83
|
//
|
|
47
84
|
// nonceSeen is the ONE module-level singleton — the whole point of this
|
|
@@ -87,6 +87,41 @@
|
|
|
87
87
|
import { resolveModelsDir } from "./embeddings-provider.js";
|
|
88
88
|
const LOGICAL_NAME = "default";
|
|
89
89
|
const MODEL_NAME = "nomic-embed-text";
|
|
90
|
+
/**
|
|
91
|
+
* Pooling declaration (HFE 0.5.0+, harper-fabric-embeddings' `init()`
|
|
92
|
+
* `pooling` option). Verification, not override: the native addon has no
|
|
93
|
+
* pooling knob of its own — llama.cpp always pools by whatever the GGUF's
|
|
94
|
+
* own `<arch>.pooling_type` metadata says. Declaring the expectation makes
|
|
95
|
+
* HFE assert it at init and fail loudly on absent/mismatched metadata,
|
|
96
|
+
* instead of a metadata-less or mismatched conversion silently pooling the
|
|
97
|
+
* wrong way (see node_modules/harper-fabric-embeddings/README.md's `init()`
|
|
98
|
+
* table for the exact contract this PR bumps to).
|
|
99
|
+
*
|
|
100
|
+
* "mean" is nomic-embed-text-v1.5's actual pooling type — NOT assumed from
|
|
101
|
+
* the model's reputation, directly confirmed against the shipped GGUF:
|
|
102
|
+
* `node_modules/.bin/node-llama-cpp inspect gguf
|
|
103
|
+
* ~/.flair/data/models/nomic-embed-text-v1.5.Q4_K_M.gguf` reports
|
|
104
|
+
* `"nomic-bert": { pooling_type: 1 }`, and llama.cpp's
|
|
105
|
+
* `enum llama_pooling_type` maps 1 -> `LLAMA_POOLING_TYPE_MEAN` (0 = none,
|
|
106
|
+
* 1 = mean, 2 = cls, 3 = last, 4 = rank). nomic-embed-text is a
|
|
107
|
+
* NomicBertModel architecture, not Qwen3 (last-token) — flair registers no
|
|
108
|
+
* Qwen3-class embedding model today (the Qwen3-Reranker-0.6B in
|
|
109
|
+
* resources/rerank-provider.ts is a SEPARATE code path that calls
|
|
110
|
+
* node-llama-cpp directly for generative yes/no scoring, never goes through
|
|
111
|
+
* HFE's register()/init(), and has no embedding-pooling context at all — see
|
|
112
|
+
* that file's header). If a Qwen3-class (last-token-pooling) embedding model
|
|
113
|
+
* is ever registered here, it must declare `pooling: "last"`, not "mean".
|
|
114
|
+
*
|
|
115
|
+
* Applies to the bench-only `modelPath` override too (`benchModelPathOverride()`
|
|
116
|
+
* below): `FLAIR_RECALL_HARNESS_MODEL_PATH` lets an operator point this
|
|
117
|
+
* registration at an arbitrary GGUF for a Q4/Q8 bakeoff, and this constant
|
|
118
|
+
* assumes whatever file lands there is still nomic-family (mean-pooling) —
|
|
119
|
+
* true for every bakeoff run to date (same base model, different quant).
|
|
120
|
+
* HFE 0.5.0's verification is exactly the safety net that turns "someone
|
|
121
|
+
* points --model-file at a non-nomic, non-mean-pooling GGUF" into a loud
|
|
122
|
+
* boot-time failure instead of a silently-wrong pooling pass.
|
|
123
|
+
*/
|
|
124
|
+
const EMBEDDING_POOLING = "mean";
|
|
90
125
|
/**
|
|
91
126
|
* BENCH-ONLY escape hatch — NOT a production feature flag, never documented
|
|
92
127
|
* as an operator setting, and never read anywhere else in this codebase.
|
|
@@ -131,7 +166,9 @@ export async function registerEmbeddingsBackend() {
|
|
|
131
166
|
await register({
|
|
132
167
|
logicalName: LOGICAL_NAME,
|
|
133
168
|
kind: "embedding",
|
|
134
|
-
config: modelPath
|
|
169
|
+
config: modelPath
|
|
170
|
+
? { modelPath, pooling: EMBEDDING_POOLING }
|
|
171
|
+
: { modelName: MODEL_NAME, modelsDir: resolveModelsDir(), pooling: EMBEDDING_POOLING },
|
|
135
172
|
});
|
|
136
173
|
}
|
|
137
174
|
catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/flair",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.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,10 +55,10 @@
|
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@harperfast/harper": "5.1.17",
|
|
58
|
-
"@harperfast/oauth": "2.
|
|
58
|
+
"@harperfast/oauth": "2.4.0",
|
|
59
59
|
"@types/js-yaml": "4.0.9",
|
|
60
60
|
"commander": "14.0.3",
|
|
61
|
-
"harper-fabric-embeddings": "^0.
|
|
61
|
+
"harper-fabric-embeddings": "^0.5.0",
|
|
62
62
|
"jose": "6.2.2",
|
|
63
63
|
"js-yaml": "4.1.1",
|
|
64
64
|
"node-llama-cpp": "3.18.1",
|