@tpsdev-ai/flair 0.44.5 → 0.44.7

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 CHANGED
@@ -23,7 +23,7 @@ import { detectClients, renderWiringSummary, wireClaudeCode, wireCodex, wireGemi
23
23
  import { flairCliVersion, clearFlairCliVersionCache, mcpServerSpec, unpinnedSpecWarning } from "./lib/mcp-spec.js";
24
24
  import { resolveAgentKeyPath, loadEd25519PrivateKeyFromFile, signClientAssertion, buildTokenRequestForm, getMcpAccessToken, McpTokenRequestError, defaultMcpClientId, defaultMcpTokenEndpoint, defaultMcpResource, defaultMcpIssuer, MAX_ASSERTION_LIFETIME_SECONDS, } from "./mcp-client-assertion.js";
25
25
  import { enableMcp, disableMcp, mcpStatus, checkLocalOriginRefusal, selfVerifyMcpMetadata, } from "./lib/mcp-enable.js";
26
- import { readClientMcpBlock, checkClaudeMdBootstrap, inspectSessionStartHook, upgradeSessionStartHookCommand, fixClaudeMdBootstrap, fixSessionStartHook, applyOrReportClaudeMdBootstrap, applyOrReportSessionStartHook, resolveWireFlairUrl, planAgentIterations, inferSoleAgentId, fixCommandAgentHint, describeAgentGateFinding, embeddingsSkipRemedy, classifyKeyFile, resolveCollisionSafeName, pruneDateStamp, PRUNED_DIR_NAME, } from "./doctor-client.js";
26
+ import { readClientMcpBlock, checkClaudeMdBootstrap, inspectSessionStartHook, upgradeSessionStartHookCommand, fixClaudeMdBootstrap, fixSessionStartHook, applyOrReportClaudeMdBootstrap, applyOrReportSessionStartHook, resolveWireFlairUrl, planAgentIterations, fixCommandAgentHint, isNodeKeyId, partitionKeyIds, resolveFixAgentId, describeAgentGateFinding, embeddingsSkipRemedy, classifyKeyFile, resolveCollisionSafeName, pruneDateStamp, PRUNED_DIR_NAME, } from "./doctor-client.js";
27
27
  import { installHook, uninstallHook, hookStatus, isSupportedHarness, SUPPORTED_HARNESSES, } from "./hook-install.js";
28
28
  import { readSecretFileSecure, readAdminPassFileSecure, defaultAdminPassPath, defaultKeysDir, resolveLocalAdminPass, resolveKeyPath, buildEd25519Auth, authFetch, KeyLoadError, isLocalBase, authedRequest, } from "./lib/auth-resolve.js";
29
29
  import { resolveSigningIdentity, emitSigningIdentityDebug, } from "./lib/signing-identity.js";
@@ -1503,8 +1503,12 @@ export async function verifySemanticSearch(baseUrl, agentIdOpt, keysDir) {
1503
1503
  if (!agentId) {
1504
1504
  try {
1505
1505
  const keyFiles = readdirSync(keysDir).filter((f) => f.endsWith(".key"));
1506
- if (keyFiles.length > 0)
1507
- agentId = keyFiles[0].replace(/\.key$/, "");
1506
+ // Skip node-scoped federation keys (flair#1193): they can't sign, so
1507
+ // picking one here would fail the probe with a decode error that reads
1508
+ // like a semantic-search regression rather than "no agent to sign as".
1509
+ const agentKeyFile = keyFiles.find((f) => !isNodeKeyId(f.replace(/\.key$/, ""), keysDir));
1510
+ if (agentKeyFile)
1511
+ agentId = agentKeyFile.replace(/\.key$/, "");
1508
1512
  }
1509
1513
  catch { /* keysDir missing */ }
1510
1514
  }
@@ -9796,8 +9800,12 @@ program
9796
9800
  await (async () => {
9797
9801
  const agentId = resolveAgentIdOrEnv({}) ?? (() => {
9798
9802
  try {
9799
- const keyFiles = readdirSync(defaultKeysDir()).filter((f) => f.endsWith(".key"));
9800
- return keyFiles.length > 0 ? keyFiles[0].replace(/\.key$/, "") : null;
9803
+ const kd = defaultKeysDir();
9804
+ const keyFiles = readdirSync(kd).filter((f) => f.endsWith(".key"));
9805
+ // Node-scoped federation keys aren't agents (flair#1193) — never
9806
+ // pin-refresh a connector as one.
9807
+ const agentKeyFile = keyFiles.find((f) => !isNodeKeyId(f.replace(/\.key$/, ""), kd));
9808
+ return agentKeyFile ? agentKeyFile.replace(/\.key$/, "") : null;
9801
9809
  }
9802
9810
  catch {
9803
9811
  return null;
@@ -11843,9 +11851,27 @@ program
11843
11851
  const keysDir = defaultKeysDir();
11844
11852
  if (existsSync(keysDir)) {
11845
11853
  const keyFiles = (await import("node:fs")).readdirSync(keysDir).filter((f) => f.endsWith(".key"));
11846
- if (keyFiles.length > 0) {
11847
- keyAgentIds = keyFiles.map((f) => f.replace(/\.key$/, ""));
11848
- console.log(` ${render.icons.ok} Keys found: ${render.wrap(render.c.bold, String(keyFiles.length))} agent(s) in ${render.wrap(render.c.dim, keysDir)}`);
11854
+ // ~/.flair/keys is shared by agent Ed25519 signing keys and node-scoped
11855
+ // federation keys (flair#1193). Only agent keys are signing identities;
11856
+ // node keys are AES-GCM keystore blobs that must never be parsed as, or
11857
+ // inferred as, an agent. Partition them out here so every downstream
11858
+ // consumer of keyAgentIds (registration checks, --fix inference,
11859
+ // fixCommandAgentHint) is node-free by construction.
11860
+ const { agentKeyIds, nodeKeyIds } = partitionKeyIds(keyFiles.map((f) => f.replace(/\.key$/, "")), keysDir);
11861
+ keyAgentIds = agentKeyIds;
11862
+ if (agentKeyIds.length > 0) {
11863
+ console.log(` ${render.icons.ok} Keys found: ${render.wrap(render.c.bold, String(agentKeyIds.length))} agent(s) in ${render.wrap(render.c.dim, keysDir)}`);
11864
+ if (nodeKeyIds.length > 0) {
11865
+ console.log(` ${render.icons.info} ${render.wrap(render.c.dim, `${nodeKeyIds.length} node-scoped federation key(s) present — not agent signing keys; skipping`)}`);
11866
+ }
11867
+ }
11868
+ else if (nodeKeyIds.length > 0) {
11869
+ // Node keys but no agent key: functionally there is no agent identity
11870
+ // here. Report it plainly (not the old DECODER false alarm) and point
11871
+ // at the real remedy. Kept a warn — not an issues++ — so a genuine
11872
+ // federation-only host doesn't newly fail doctor's exit code.
11873
+ console.log(` ${render.icons.warn} No agent signing key found — only ${render.wrap(render.c.bold, String(nodeKeyIds.length))} node-scoped federation key(s) in ${render.wrap(render.c.dim, keysDir)}`);
11874
+ console.log(` ${render.wrap(render.c.dim, "These are Fabric node keys, not agent identities. Fix:")} flair init --agent-id <your-agent>`);
11849
11875
  }
11850
11876
  else {
11851
11877
  console.log(` ${render.icons.error} Keys directory exists but no .key files found`);
@@ -12103,13 +12129,24 @@ program
12103
12129
  // nothing else identifies one — the only case doctor can
12104
12130
  // infer without being told (see inferSoleAgentId's doc
12105
12131
  // comment in doctor-client.ts for why 0/2+ keys don't guess).
12106
- const fixAgentId = opts.agent || process.env.FLAIR_AGENT_ID || anyKnownAgentId || inferSoleAgentId(keyAgentIds);
12132
+ // flair#1193: resolveFixAgentId additionally refuses a
12133
+ // node-scoped federation id from ANY source (inference, env,
12134
+ // or a wired block a prior buggy run may have poisoned) — a
12135
+ // node id can't sign, so wiring it would authenticate the
12136
+ // connector as a phantom unregistered node.
12137
+ const fixAgentId = resolveFixAgentId({
12138
+ optsAgent: opts.agent,
12139
+ envAgentId: process.env.FLAIR_AGENT_ID,
12140
+ anyKnownAgentId,
12141
+ keyAgentIds,
12142
+ keysDir: defaultKeysDir(),
12143
+ });
12107
12144
  if (!fixAgentId) {
12108
12145
  if (keyAgentIds.length > 1) {
12109
12146
  console.log(` ${render.icons.warn} Cannot auto-wire ${client.label}: multiple agents found (${[...keyAgentIds].sort().join(", ")}) — pass --agent <id> to choose which one`);
12110
12147
  }
12111
12148
  else {
12112
- console.log(` ${render.icons.warn} Cannot auto-wire ${client.label}: no agent registered — run \`flair agent add <id>\` first, then re-run \`flair doctor --fix\``);
12149
+ console.log(` ${render.icons.warn} Cannot auto-wire ${client.label}: no agent identity found in keys/ — run \`flair init --agent <name>\` or \`flair agent add <name>\` before wiring a connector`);
12113
12150
  }
12114
12151
  }
12115
12152
  else {
@@ -907,3 +907,78 @@ export function classifyKeyFile(agentId, seedValid, registration, baseUrl) {
907
907
  reason: `agent '${agentId}' is not registered on ${baseUrl}${registration?.detail ? ` (${registration.detail})` : ""}`,
908
908
  };
909
909
  }
910
+ // ── Node-scoped federation keys vs agent signing keys (flair#1193) ─────────
911
+ //
912
+ // `~/.flair/keys/` is a namespace shared by two writers with two file shapes:
913
+ //
914
+ // • agent Ed25519 signing keys — a 32-byte raw seed at `<name>.key`, ALWAYS
915
+ // written together with a sibling `<name>.pub` (see the keypair write in
916
+ // src/cli.ts: the seed and the public key are emitted in the same block).
917
+ // • node-scoped federation keys — `flair_<hex8>.key`, an AES-256-GCM
918
+ // keystore blob written by FileKeyStore during Fabric provisioning
919
+ // (flair#1026). The id is minted as `flair_${randomBytes(4).toString("hex")}`
920
+ // in resources/Federation.ts, and NO `.pub` is ever written for it.
921
+ //
922
+ // Nothing used to tell them apart, so doctor tried to Ed25519-parse the node
923
+ // blob — a "DECODER routines::unsupported" warning that reads as agent-auth
924
+ // breakage when agent auth is fine — and `doctor --fix` could infer the node
925
+ // id as the sole "agent" and wire it as a connector identity, authenticating
926
+ // as a phantom, unregistered node whose key cannot sign (flair#1193).
927
+ //
928
+ // The guard is STRUCTURAL, not a parse attempt: a node id matches
929
+ // `flair_<hex8>` AND has no sibling `.pub`. We deliberately do NOT classify by
930
+ // parsing the file and treating a decode failure as "must be a node key" —
931
+ // that is the exact fails-open move flair#1026 warns against (a genuinely
932
+ // corrupt agent key would be misread as a node key and silently skipped).
933
+ // A real agent always has a `.pub`; a node key never does, so `.pub` presence
934
+ // is the primary, falsifiable signal and classification never depends on the
935
+ // parse-failure of the thing being classified.
936
+ /** The shape a Fabric node id always has: `flair_` + 8 lowercase hex chars. */
937
+ const NODE_KEY_ID_RE = /^flair_[0-9a-f]{8}$/;
938
+ /**
939
+ * True iff `id` names a node-scoped federation key rather than an agent
940
+ * signing key: it is shaped like a node id AND has no sibling `<id>.pub` in
941
+ * `keysDir`. Both conditions are required — an agent that happened to be named
942
+ * `flair_deadbeef` would still have a `.pub`, so it is never misclassified.
943
+ */
944
+ export function isNodeKeyId(id, keysDir) {
945
+ if (!NODE_KEY_ID_RE.test(id))
946
+ return false;
947
+ return !existsSync(join(keysDir, `${id}.pub`));
948
+ }
949
+ /**
950
+ * Partition `.key`-derived ids into agent signing keys and node-scoped
951
+ * federation keys (see isNodeKeyId). Node keys must never feed agent handling —
952
+ * Ed25519 parsing, registration checks, or connector-identity inference
953
+ * (flair#1193) — so callers keep only `agentKeyIds` for those paths and report
954
+ * `nodeKeyIds` informatively.
955
+ */
956
+ export function partitionKeyIds(ids, keysDir) {
957
+ const agentKeyIds = [];
958
+ const nodeKeyIds = [];
959
+ for (const id of ids) {
960
+ (isNodeKeyId(id, keysDir) ? nodeKeyIds : agentKeyIds).push(id);
961
+ }
962
+ return { agentKeyIds, nodeKeyIds };
963
+ }
964
+ /**
965
+ * Resolve the agent id `doctor --fix` should wire a connector as, or undefined
966
+ * when none can be safely determined. A node-scoped federation id is NEVER
967
+ * returned regardless of source (flair#1193): it cannot sign, so wiring it
968
+ * yields a connector that authenticates as a phantom unregistered node and
969
+ * fails every read/write. When this returns undefined the caller MUST refuse
970
+ * and tell the user to create/register an agent — never fall back to a node id.
971
+ *
972
+ * `keyAgentIds` is expected to already be node-free (its producer partitions
973
+ * node keys out at enumeration), so `inferSoleAgentId` never sees one; the
974
+ * explicit `isNodeKeyId` guard additionally covers `optsAgent` / `envAgentId` /
975
+ * `anyKnownAgentId`, since a prior buggy run may have poisoned a wired block
976
+ * with a node id that would otherwise be read back and re-propagated.
977
+ */
978
+ export function resolveFixAgentId(args) {
979
+ const { optsAgent, envAgentId, anyKnownAgentId, keyAgentIds, keysDir } = args;
980
+ const candidate = optsAgent || envAgentId || anyKnownAgentId || inferSoleAgentId(keyAgentIds);
981
+ if (candidate && isNodeKeyId(candidate, keysDir))
982
+ return undefined;
983
+ return candidate;
984
+ }
@@ -376,7 +376,15 @@ async function bootstrap(agent, args) {
376
376
  // flair#831 — attach the running Flair version to the RESPONSE (not the
377
377
  // delegated request body) so the calling agent learns the server version
378
378
  // on its very first call.
379
- const result = unwrap(await h.post(body));
379
+ //
380
+ // flair#1182 — `unwrap` is async: it must be AWAITED before the result is
381
+ // spread, exactly as every sibling tool does (`await unwrap(...)` in
382
+ // memory_store / memory_update / memory_get). Without the await, `result` is
383
+ // the still-pending PROMISE, and `{ ...aPromise }` copies no own-enumerable
384
+ // keys — so the entire computed payload (resolved agentId, scope, soul,
385
+ // memories, predicted, the #1182.1 containers, the abstention verdict) was
386
+ // silently discarded and the caller saw ONLY the injected `flairVersion`.
387
+ const result = await unwrap(await h.post(body));
380
388
  if (result && typeof result === "object" && !Array.isArray(result)) {
381
389
  return { ...result, flairVersion: resolveVersion() };
382
390
  }
@@ -384,14 +392,29 @@ async function bootstrap(agent, args) {
384
392
  }
385
393
  async function soulSet(agent, args) {
386
394
  const Cls = await handler("Soul");
387
- const h = new Cls(undefined, delegationContext(agent));
388
- // Soul records are keyed `id = agentId:key` (see flair-client SoulApi.set and
389
- // schemas/memory.graphql). Use PUT with the explicit id so soul_get's
390
- // `${agentId}:${key}` lookup finds it a plain post() would mint a random id
391
- // and orphan the entry from get(). Soul.put enforces write ownership via
392
- // resolveAgentAuth (non-admin can only write agentId === self).
395
+ // flair#1181 this write MUST go through a COLLECTION-bound instance
396
+ // (`collectionResource(Cls, ctx).post(...)`), the same create path the sibling
397
+ // write tools use (memoryStore / workspaceSet / orgEvent). The previous
398
+ // `new Cls(undefined, ctx).put({ id, ... })` was a PUT on an UNLOADED instance:
399
+ // an unloaded instance has no primary key, so Harper's instance put()/save()
400
+ // threw `Invalid primary key type: undefined` (the same defect class the
401
+ // memoryGet/update/delete/soulGet by-id READS were migrated off of — see those
402
+ // wrappers, and resources/in-process.ts's header: "flair itself got [collection
403
+ // binding] wrong in four MCP tool paths"). soul_set's only prior test drove a
404
+ // MOCKED handler, so the real instance-put never ran and it shipped broken on
405
+ // the connector path.
406
+ //
407
+ // A COLLECTION post (not a static `Cls.put(record, ctx)`) is the right form:
408
+ // it routes through Soul.post(), which stamps createdAt (a schema-required,
409
+ // non-null field). Static `Cls.put` reaches Soul.put(), which does NOT stamp
410
+ // createdAt on a create, so it fails a "Property createdAt is required"
411
+ // validation. Soul.post honors the explicit body `id`, so the record is still
412
+ // keyed `id = agentId:key` and soul_get's `${agentId}:${key}` lookup finds it —
413
+ // a random-id create would orphan the entry from get(). Soul.post enforces
414
+ // write ownership via resolveAgentAuth (non-admin can only write agentId === self).
393
415
  const id = `${agent.agentId}:${args?.key}`;
394
- return unwrap(await h.put({
416
+ const h = await collectionResource(Cls, delegationContext(agent));
417
+ return unwrap(await h.post({
395
418
  id,
396
419
  agentId: agent.agentId,
397
420
  key: args?.key,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpsdev-ai/flair",
3
- "version": "0.44.5",
3
+ "version": "0.44.7",
4
4
  "packageManager": "bun@1.3.10",
5
5
  "description": "Identity, memory, and soul for AI agents. Cryptographic identity (Ed25519), semantic memory with local embeddings, and persistent personality — all in a single process.",
6
6
  "type": "module",