Releases: tpsdev-ai/flair
Release list
v0.22.1
🐛 Migration disk-headroom pre-flight blocked trivially-small migrations on normally-full personal disks (flair#720)
checkSpace() (resources/migrations/space.ts) required a migration's needed bytes to fit AND that spending them not push disk usage past 90% of TOTAL disk size — a rule designed for a flair-dedicated volume. On a general-purpose machine (a personal Mac especially, where APFS purgeable space makes statfs.bavail understate real availability) the system volume routinely sits above 90% used already, so every migration halted regardless of its own footprint: the first 0.22.0 boot on such a disk halted the embedding-stamp migration needing 220 KB with 18.6 GB free.
- New rule:
ok = neededBytes <= freeBytes AND (freeBytes - neededBytes) >= reserve, wherereserve = clamp(5% of total disk, 256 MiB, 2 GiB). Only the migration's own impact on free space is judged now, not the disk's pre-existing fullness —RESERVE_MIN_BYTES/RESERVE_MAX_BYTES/RESERVE_FRACTION(new named exports inresources/migrations/space.ts). FLAIR_MIGRATION_RESERVE_BYTESoverrides the computed reserve for constrained deployments (validated finite/non-negative;0disables the reserve check entirely, leaving only the raw fit test) — mirrors the existingFLAIR_MIGRATION_TEST_FREE_BYTEStest-override pattern.headroomFloor(the old fraction-of-total DI knob oncheckSpace/runMigrationCycle) is removed — it was never wired from production config, only ever exercised by the fraction-based tests this fix rewrites, and the new rule has no fraction to override (the env var above is the operator-facing lever now).- Failure message rewritten to be truthful and actionable: no longer suggests pruning snapshots or
FLAIR_SNAPSHOT_DIR(neither changes thedataDirvolume's fraction and never could have helped this class of halt) — now states the human-readable bytes needed vs. available vs. the reserve, and namesFLAIR_MIGRATION_RESERVE_BYTESas the remedy for constrained setups. All byte quantities in the message are formatted human-readable (e.g.220.0 KB,17.37 GB,2.00 GB) via a newhumanBytes()export, never raw byte counts — structured fields (SpaceCheckResult) still carry raw numbers for machine consumers.
🐛 flair doctor --fix reported issues found and exited 1 even after fixing everything (flair#721)
doctor --fix tracked a single issues counter — every detected problem incremented it, and the summary/exit code read only that counter, with no record of which of those issues --fix actually resolved during the same run. A run that interactively fixed every issue it found (e.g. wiring an MCP client, adding the Claude Code SessionStart hook) still printed N issues found — see fixes above and exited 1, indistinguishable from a run that fixed nothing — breaking scripted use (flair doctor --fix && ...).
doctor now tracks fixed-vs-remaining explicitly: each check that offers an in-run fix (port-drift config rewrite, dead-Harper restart, version-mismatch restart, stale PID-file removal, MCP client wiring, CLAUDE.md bootstrap line, SessionStart hook) counts toward a separate fixed total only when that fix actually succeeds, not merely attempted or declined. The summary now reads: all fixed → N issues found, N fixed ✓ and exit 0; some remaining (declined prompts, unfixable checks, --dry-run) → N issues found, M fixed, K remaining and exit 1; zero issues → unchanged No issues found / exit 0. Without --fix, behavior is unchanged: N issues found — see fixes above / exit 1. The decision logic is extracted into a pure summarizeDoctorRun(found, fixed, autoFix) helper, unit-tested directly (test/unit/doctor-summary.test.ts).
v0.22.0
⬆️ Upgrade notes
Read this before upgrading from 0.21.0. Three of the four items below change default behavior; none require a manual command, but the first boot after upgrade does more work than a typical patch bump.
-
authorizeLocalnow defaults tofalse(flair#654, #671). Already shipped in this Unreleased batch (see existing 🔒 Security entry) but worth restating at the top: a credential-less loopback call to Harper's raw ops API (:9925) that used to be auto-authorized assuper_usernow gets rejected. The admin credential (~/.flair/admin-pass,--admin-pass, orFLAIR_ADMIN_PASS) is now load-bearing for any local tooling that talks to the ops API directly.flair init/agent add/principal addare unaffected (they already passed real credentials). To restore the old, less-safe behavior for local dev only, setauthorizeLocal: trueinconfig.yaml. Not remotely exploitable either way — this only ever governed loopback. -
Embeddings re-embed automatically on first boot — no manual step, but it takes time. #700 flips
EMBEDDING_PREFIXES_ENABLEDtotrue, which changesgetModelId()'s output (rows now stamp<model>+searchprefixinstead of the bare model id). Every memory written under 0.21.0 or earlier reads as stale under the new stamp. The always-onembedding-stampmigration (part of the zero-touch auto-migration runner shipped in #690) picks these rows up and re-embeds them automatically on the first boot that reaches them — this is stated as intentional in #700's own PR description ("this is intended, not incidental — it's this migration's first real payload since it shipped"). Nothing to run by hand;flair reembed --stale-onlyexists if you want to trigger it deliberately instead of waiting for boot. The migration runner enforces a 90%-disk-headroom pre-flight check, takes a risk-scoped snapshot before running, and halts (rather than partially completing) if it can't proceed safely — see the migration-runner section below. Neither PR states an expected re-embed duration for a production-sized corpus — budget first-boot time proportional to corpus size; don't assume it's instant. -
Embeddings config registration changed mechanism (again) mid-release — the version you're actually getting is the safe one. #685/#689 initially registered the embeddings backend via Harper's
HARPER_CONFIGenv var, which turned out to persist intoharper-config.yamland brick a downgrade back to 0.21.0 (flair#694, root-caused in #698's PR body: Harper's env-config layer deletes the persisted keys individually when a build that predates the feature boots without setting the env var, and the resultingmodels: {embedding: {default: {}}}fails Harper's own config validator on the next boot). #698 (merged before the prefix flip landed) replaced this with fully in-process registration (resources/embeddings-boot.tscallsharper-fabric-embeddings's ownregister()factory directly on every boot) — nothing is ever written toharper-config.yamlfor embeddings config as of this release. No operator action needed; flagging so you know the mechanism is reassert-only, not persisted, if you go looking for it on disk and don't find it. -
REM execute-mode is now the default for
flair rem rapidand requires manual Harper config to actually run.flair rem rapidnow calls Harper'smodels.generate()server-side and stagesMemoryCandidaterows by default, instead of just printing a prompt for you to paste elsewhere.--prompt-onlyrestores the exact pre-0.22.0 prompt-return behavior (no model call, no staging). For execute-mode to work at all, an operator must add amodels:block to Harper's root instance config (harper-config.yaml/harperdb-config.yamlat the Harper data directory) — not flair's ownconfig.yaml, which Harper only ever loads as a non-root component config. The specific key ismodels.generative.<logicalName>(distinct from embeddings'models.embedding.defaultnamespace under the same root block). Verified example from the now-shippeddocs/rem.md(origin/main, via #711), local zero-key Ollama default:# harper-config.yaml models: generative: default: # unset FLAIR_REM_MODEL resolves to this logical name backend: ollama host: localhost:11434 # optional — already the default model: llama3.1 # required — Ollama has no built-in default model
Hosted providers (OpenAI/Anthropic/Bedrock) are supported the same way under a different logical name (e.g.
models.generative.hosted), selected viaFLAIR_REM_MODEL=hosted;apiKeymust be${ENV_VAR}indirection, never a literal in the YAML (flagged at Harper boot) — on Fabric this env var is provisioned through Harper's own Fabric secrets mechanism (enc:v1:at rest), a Harper-side concern flair's owndocs/secrets-and-keys.mddoes not cover. Pointing this at a hosted provider sends the memory content being reflected on to that provider — the docs call this out explicitly as a "data egress is a configuration decision" warning; local Ollama is the only backend that keeps everything on-box.
If nomodels:backend is configured,flair rem rapid(execute-mode) and the nightly runner's distillation step both fail closed (503no_backend) — the nightly runner logs it into the audit row'serrors[]and otherwise proceeds normally, so an un-configured instance is not broken, it just never gets execute-mode REM until a backend is configured.- Non-thinking model requirement. Per real dogfooding (documented in
docs/rem.mdvia #713): thinking/reasoning models (qwen3-next,deepseek-r1, and similar) "currently return empty generations through Harper's Ollama backend: Ollama routes their output into the response'sthinkingfield, which the backend doesn't read, so every REM execute run fails closed withdistillation_failed" — an availability failure, not a correctness one (zero partial/bad candidates). Use a non-thinking model (llama3.1,qwen3-coder-next,gemma3, …) instead. Dogfooded successfully:qwen3-coder-nextstaged 7 quality candidates in ~7s, dedup held on a second run, a promoted candidate landed withderivedFromintact. - Nightly cycle now spends model tokens/compute nightly, once a backend is configured — step 5 of the nightly runner calls
/ReflectMemorieswithexecute: trueafter maintenance succeeds (skipped entirely under--dry-run). This is a new recurring cost that didn't exist pre-0.22.0; there is no separate opt-out from the nightly step short of not configuring amodels:backend. - Review loop is unchanged and still the only promotion path:
flair rem candidateslists pending rows,flair rem promote <id> --rationale "<why>"/flair rem reject <id> --reason "<why>"decide them — nothing self-promotes, execute-mode or not. - Clustered/Fabric deploys:
flair rem nightly enableinstalls a platform timer (launchd/systemd) on whichever single node runs the command — v1 requires picking exactly one node deliberately (enabling on every node would run the cycle N times); this is a pre-existing v1 constraint, not new in 0.22.0, but worth knowing before enabling nightly REM on a multi-node deploy. Snapshot locality follows the timer's node.
- Non-thinking model requirement. Per real dogfooding (documented in
🌙 REM: in-process distillation — /ReflectMemories execute mode (flair#707, #708, #710, #711)
REM (Reflect · Extract · Merge) is flair's memory-curation cycle — it reads an agent's recent memories, distills them into candidate insights, and stages those as reviewable rows for explicit promotion; nothing self-promotes. Before this slice, /ReflectMemories could only return a prompt for a human or another agent to paste into an LLM elsewhere — the actual distillation step was always a manual handoff (flair rem rapid produced homework, not results). This slice closes that gap: REM now executes reflection itself, in-process, against Harper's own model-serving surface, and stages the result as reviewable MemoryCandidate rows directly. Three PRs: a K&S-reviewed spec (#708), the resource-level execute mode (#710), and the nightly runner + CLI + docs wiring (#711).
execute: trueonPOST /ReflectMemories(resources/MemoryReflect.ts,resources/memory-reflect-lib.ts) runs distillation server-side viamodels.generate()— schema-constrained output on the first attempt, ajson-mode fallback with one retry on malformed output, fail-closed (no retry) on a thrown network/timeout error. Validated output stagesMemoryCandidaterows: shape validation,sourceMemoryIdschecked as a subset of the gathered memory set, named-constant batch caps.execute: false(the pre-0.22.0 behavior) still returns a prompt only — nothing changed there.- Data-not-directives hardening applies to both modes: memory content is now delimiter-wrapped (
<memory id="…">…</memory>, replacing the old bracket-list prompt format) with an explicit instruction that memory content is data, not instructions — closes a prompt-injection-shaped surface where a memory's own content could otherwise be read as directives by the distillation call. - Backend is pluggable, zero provider code in flair: whatever Harper's
models.generative.<logicalName>config points at — local Ollama by default (zero-key, nothing leaves the box), or a hosted OpenAI/Anthropic/Bedrock backend selected viaFLAIR_REM_MODEL, with the API key required to be${ENV_VAR}indirection (never a literal in the YAML) and, on Fabric, provisioned through Harper's own Fabric secrets mechanism (enc:v1:at rest) — a Harper-side concern, not something flair's own code implements. Verified against@harperfast/harper5.1.17:responseFormat: { schema }is honored by the Ollama/OpenAI backends; Anthropic accepts but ignores it, so output is independently re-validated regardless of which backend is configured.generatedByon a staged candida...
CI model mirror (not a flair release)
First-party mirror of the pinned embedding models CI depends on, per #715. Not a software release — assets only. SHA-256 checksums are pinned in the workflows that consume these.
v0.21.0
Federation edge-hardening, open-within-org memory read, an adopter-adoptability sweep (now including automatic MCP presence), and a security closure on Presence/OAuthAuthorize auth-bypass gaps — on harper 5.1.15.
🧠 Open-within-org memory read (#578)
Cross-agent read opens up within an org: a verified in-org agent can read another agent's non-private memories (resolveReadScope returns non-private OR own), while private stays owner-only on every path. Replaces the prior grant-gated model — knowledge is org-readable by default, access-gated only at the federation edge. Live + verified on both rockit and Fabric.
- Bootstrap teammate-findings aligned to the open model (#606, completes #550) — the "teammate findings" surfacing already rode on
resolveReadScope()(never its ownMemoryGranttraversal), so it picked up #578's behavior with zero code changes needed. Corrected stale comments/nudge copy that still described the retired grant-gated model, and added the missing test proving aMemoryGrantis NOT required to see a teammate's memory — every prior test seeded one as harmless leftover from pre-#578 authoring, masking the gap.
🔒 Federation edge-hardening (slices 1–4)
Hardens what crosses the federation boundary:
- Server-stamped verified provenance on writes (#575) — provenance captured server-side (verified identity + timestamp), not client-claimed.
- Write-time originator tagging (#576) — synced tables carry an
originatorInstanceIdstamped at write. - Push-side private-visibility filter (#577) — private memories are filtered before they leave the instance.
- Per-record signing + verification (#580) — each synced record is signed over its canonical form and verified on receipt, closing a hub-forgery hole where a relay could forge records for another originator.
- Persistent anti-replay nonce store (#581) — the nonce store survives restarts, so replay protection holds across process boundaries.
🧰 Adopter adoptability
Making Flair actually work for a fresh adopter instead of silently half-working:
flair doctorverifies client integration (#599) — a new "Client integration" section answers "is Flair working for my agent?": per detected MCP client, the MCP block +FLAIR_URLreachability + agent registration; for Claude Code, the CLAUDE.md bootstrap line +SessionStarthook.--fixwires missing pieces (idempotent, merge-safe).flair doctorreports not-registered on 401/403, not just 404 (#603, closes #602) — the auth middleware rejects an unregistered agent's signed request before the resource handler runs (401unknown_agent), so the 404-only branch was dead code and a missing agent showed "⚠ couldn't-verify" instead of "✗ not-registered." Now 401/403 with theunknown_agentmarker and a resolved local key correctly reports not-registered, with the fix hint.flair initwires all three legs (#600) — init now installs theSessionStarthook + CLAUDE.md line alongside the MCP block, instead of leaving them manual (silent partial setups).--skip-hook/--skip-claude-mdopt-outs; prints the exact missing snippet when skipped.flair-mcpauto-sets presence on session-start + rate-limited heartbeat (#608, closes #598) — the session-start hook and bootstrap seedactivity/currentTask; every other MCP tool call refresheslastHeartbeatAt(rate-limited, 3min default). Fire-and-forget + fail-open — never blocks a tool call or startup. Complements #601's read-side gating below.- Version-behind nudge (#594) —
flair status/doctorsurface when the installed version is behind the published latest (cached, offline-tolerant, never blocks). agent add/principal addadmin-pass fallback (#593) — fall back to the local~/.flair/admin-passfile instead of hard-requiring--admin-pass.
🔒 Security
- OAuthAuthorize consent required real auth; Presence PUT/DELETE scoped correctly (#609, closes #604) — closes the
authorizeLocalescalation class: a credential-less loopback POST (which Harper'sauthorizeLocalforges assuper_user) could mint an admin OAuth code without a realAuthorizationheader. Loopback-only, HIGH severity — verified not remotely exploitable (Fabric rejects the unauthed remote request with 401). Also scopes the/Presenceearly-return to GET-only so PUT/DELETE correctly transit the auth middleware, and fixes a pre-existing bug whereResponse.redirect's immutable Headers 500'd everyPOST /OAuthAuthorizeon main. Presence.currentTaskgated to verified readers (#601, closes #592) — anonymousGET /Presencereturned agents' freeformcurrentTask(which can hold customer/host/incident strings) verbatim on a public endpoint. Now gated behind a verified Ed25519 signature (not justresolveAgentAuth, which Harper'sauthorizeLocalcan spoof for a loopback caller) — anonymous, loopback, and Basic-admin callers get the low-risk roster withcurrentTasknulled; the rest of the roster stays public.
📦 Dependencies
- harper 5.1.14 → 5.1.15 (#595) — pins the models extension API (
registerBackend, unblocks sovereign local embeddings), replication/deploy reliability fixes, and the MCP row-level RBAC fix. Also fixes the Fabric deploy abort.
🧹 Tooling / CI / hygiene
- Wire
flair-mcppackage tests into the merge gate (#605, closes #491) — the 34packages/flair-mcp/test/*tests weren't gated by CI (roottest.ymlonly rantest/unit/); now buildsflair-clientfirst (flair-mcp imports its builtdist/), then runs the package's own suite. - Self-healing CI/deploy: timeout+retry the flaky sfw (Socket-firewall) install (#583), retry peer-replication with
--ignore-replication-errorson deploy (#582), de-flake the E2E CLI smoke test (#584). - Strip internal ops- tracker refs from shipping comments/tests (#586)* — consumer-facing code references public flair# issues only.
- DESIGN.md in-repo (#579) — design invariants documented adopter-facing.
v0.20.1
🛠 Self-verifying flair deploy (#573)
The deploy CLI can no longer report false success — it verifies the deployed component is actually serving before declaring victory.
- Timeout passthrough —
--deployment-timeout/--install-timeout(default 600000, envFABRIC_DEPLOYMENT_TIMEOUT/FABRIC_INSTALL_TIMEOUT), threaded into the harper deploy args. Fixes the 120s peer-replication abort that previously forced hand-rolled deploys. - Post-deploy served-API verification — after harper reports success, the CLI polls the served target through the post-deploy restart, then GETs each of the component's resources (derived from the built package, not hardcoded) and fails loudly on 404 (
component is not serving; likely deployed the wrong package root). A 401/200 means serving.--no-verifyescape hatch;--verify-resource <name>override;--verify-timeout <ms>(default 300000).flair upgradeinherits the same protection.
v0.20.0
Writer-controlled memory sharing (Kris flair#522/#550), a memory recall-correctness sweep, and cross-agent authz hardening.
✨ Writer-controlled memory sharing (#522 / #550)
- Layer 1 —
Memory.visibility= private/shared + centralized read-scoping (#565). A single chokepoint (resolveReadScope) that every cross-agent read path routes through (Memory.search/get, SemanticSearch, MemoryBootstrap, the by-id guard). Durability-keyed default (permanent/persistent → shared, ephemeral → private); aprivatememory is never returned to a non-owner on any path. Migration-invariant — existing memories keep their exact access (visibility != privatetreats no-visibility as shared). Also deletes the SemanticSearchvisibility=="office"global read leak. - Surface teammate findings (#568). Bootstrap surfaces grant-visible teammate memories relevant to
currentTaskin a distinct, attributed section; the agent's own-context sections stay own-only.
🔧 Memory recall correctness
- Dedup signal on singleton results (#564, ops-ume4). Harper omits
$distancewhen a cosine-sort result set is a singleton → dedup silently scored 0. Fallback: point-lookup the candidate and compute cosine directly. - Superseded records no longer resurface in recall (#566 SemanticSearch/BM25, #567 bootstrap). A server-superseded record (past
validTo, not archived) not co-present with its successor could resurface; now excluded unconditionally in every recall path. - openclaw-flair supersede: write-new-before-close-old + observable failure (#563, ops-mmh9).
🔒 Security
- Cross-agent delete authz regression guards for
Relationship.delete(#569) andCredential.delete(#570) — both verified safe against real Harper (the target record is bound before the method runs), now guarded so a future refactor can't silently reintroduce a bypass. - Consolidated 3 Ed25519 nonce caches + crypto helpers into one shared guard (#559, ops-c4op).
🧰 Tooling / CI
release.shaligns bun.lock leaf specifiers after bump — stops the recurring--frozen-lockfiledesync (#560, ops-i9w8).- Fail-fast timeouts on the two timeout-less CI jobs whose sfw (Socket firewall) install could hang and block merge indefinitely (#571, ops-fumh).
- Real-Harper dedup/supersede e2e (#562, which found ops-ume4) + Memory.get RequestTarget routing coverage (#561).
v0.19.0
The read-gate security sweep: three distinct anonymous/cross-agent read exposures, all found from one Sherlock sweep RED and closed.
🔒 SECURITY: Memory/Soul by-id reads were ungated — anonymous content leak (#556, ops-ckrr)
Memory and Soul gated writes and search() but defined no allowRead() and no get() override, so Harper's direct by-id path (GET /Memory/<id>) and the collection-describe (GET /Memory) were ungated — an anonymous caller received full record content, and a verified non-admin agent could read another agent's memory by enumerable id (search() only guarded the query path). Fix: allowRead()=allowVerified on both; an owner/grant-scoped get() on Memory (404 never 403, no id enumeration) branching on isCollection so collection/query reads delegate to the already-scoped search(); delete() reads via super.get() to preserve the permanent-delete guard.
🔒 SECURITY: admin console reachable by verified non-admin agents (#557, ops-2ty0)
P0, live-confirmed. The /Admin auth-middleware gate only 401s requests with no Authorization header; a validly-signed non-admin Ed25519 agent passed verification, de-elevated to flair-agent, and reached the seven custom Admin* resources — which had no allowRead — returning the full admin console (/AdminMemory all-agents memory browse + provenance, /AdminPrincipals, /AdminDashboard). Fix: allowRead()=allowAdmin on all seven (Basic super_user and admin agents retain access; non-admins → 403).
🔒 SECURITY: family read-gate — WorkspaceState / Relationship / Integration / MemoryGrant (#557, ops-oox7)
The same by-id/describe leak class as Memory: search() and writes gated, but no allowRead()/get(). Fix: allowRead()=allowVerified + isCollection-branched owner-scoped get() (404 never 403) on all four; MemoryGrant scopes ownerId OR granteeId (both parties to a grant); delete() uses super.get().
v0.18.0
🧠 Memory integrity: the dedup gate no longer silently loses writes (#553 — closes #526, #548)
memory_store's dedup gate was raw-cosine-only at 0.95 and silently dropped the new write on a match — so distinct-but-topically-close findings vanished (#526, the field case: replication route-directionality vs an unrelated DDL/schema memory) and update-intent writes preserved stale state (#548). Since flair-mcp enabled dedup by default, every MCP write was exposed. The fix:
- Never-silent-loss invariant — the gate never suppresses a write. It always writes; a near-duplicate is surfaced only as a signal (
deduplicated/matchedId/matchConfidence), never a reason to drop. - Conservative same-fact detection — a candidate is a duplicate only if cosine AND lexical (Jaccard token-overlap) both clear their thresholds, so a topic collision (high cosine, low lexical) is no longer merged.
- Gate moved server-side into
Memory— both the HTTP write path and the native/mcppath (which previously had no dedup) now behave identically. memory_update(new MCP tool, both surfaces) — id-targeted, dedup-bypassed, default same-id overwrite; opt-in supersede-link mode. Retires the racy, identity-breaking delete+store workaround.- Supersede is transactional + observable — validity-window close is write-new-before-close-old and logs on failure (no more silent
.catch(() => {})); a cross-agent supersede requires awritegrant.
🔎 Cross-encoder reranker in SemanticSearch — default-OFF (#496)
An in-process cross-encoder re-scores query+candidate together and reorders the retrieval set before the final slice, composing with the BM25+union-RRF hybrid path, fail-open to vector order. Default-OFF behind FLAIR_RERANK_ENABLED; enabling waits on the recall measurement gate.
v0.17.0
🔒 SECURITY: cross-agent isolation break — getContext() not this.request (#551, ops-sal4)
P0, live-confirmed. Harper v5 never populates this.request on Resource subclasses; the #236/#487 getContext() sweep missed 8 handlers, so their per-agent ownership guards silently read undefined and became dead no-ops (fail-open). Any verified agent could read any other agent's WorkspaceState (GET /WorkspaceLatest/{id}) and OrgEvent catch-up feed (GET /OrgEventCatchup/{id}), and every approved OAuth consent grant was minted for the admin principal regardless of who approved it.
All 8 handlers now resolve identity via the canonical resolveAgentAuth(getContext()) helper (the same path 31 other resources already use), with fail-closed guards (anonymous → 403; a verified agent may only reach its own id; internal/admin pass). The OAuth authorize handler now returns 401 on an unresolved principal instead of silently granting admin. A new NECESSITY test suite (cross-agent-isolation.test.ts) asserts cross-agent reads are denied — the coverage gap (the deelevation suite only tested self-reads) that let this ship green — confirmed to fail on the unpatched tree and pass after the fix. Also fixes three fail-closed functional breaks from the same root cause (AgentSeed onboarding, IngestEvents, AdminMemory query params).
✨ Bootstrap: team roster + cross-agent search nudge (#549)
BootstrapMemories now emits a fixed-cost ## Team section listing the other active agents in the office with a nudge to search their memories before deep-diving an unfamiliar problem — bootstrap previously only ever loaded the caller's own context, so agents never learned teammates' findings were one memory_search away. Agent IDs are wrapped via wrapUntrusted (registrant-chosen, untrusted). External contribution from @kriszyp.
🔐 Native /mcp OAuth surface — Model 2 (custom withMCPAuth-guarded handler), default-OFF (ops-b6uk)
Flair speaks MCP natively over a custom in-process /mcp JSON-RPC handler wrapped with @harperfast/oauth's withMCPAuth — a per-agent OAuth identity replaces the local flair-mcp stdio proxy's key-holding. This is the Model 2 path (Nathan approved 2026-07-01): a custom handler rather than Harper's native application-MCP profile, so it sidesteps the Harper native-MCP gating gaps and is curated by construction (the handler only implements the 9 flair tools — no raw CRUD surface).
Default-OFF behind FLAIR_MCP_OAUTH. When the flag is unset (the shipped default), flair boots byte-identically: no /mcp route is registered, @harperfast/oauth is never imported, and the default auth chain (Ed25519) is unchanged. resources/auth-middleware.ts, XAA.ts, OAuth.ts, config.yaml, and every delegated handler resource are untouched.
resources/mcp-handler.ts— a minimal MCP handler (initialize/tools/list/tools/call/ping). Ontools/callit resolves thewithMCPAuth-verified tokensub→ a flairAgentviaCredential(kind:"idp", idpSubject=sub)→principalId(the same identity surface XAA uses), establishes therequest.tpsAgentscoping context, and delegates to the existing resource handler. An unresolvablesubis denied — never run as anonymous or admin. JIT-provisioning of an unknown sub is gated behind an explicit trust anchor (FLAIR_MCP_JIT_PROVISION, default OFF).resources/mcp-tools.ts— the 9 curated tools (memory_search/store/get/delete, bootstrap, soul_set/get, flair_workspace_set, flair_orgevent), each a thin wrapper over the existing handler (Memory / SemanticSearch / BootstrapMemories / Soul / WorkspaceState / OrgEvent). Handlers lazy-loaded so the /mcp module graph carries no top-level Harper link. Fixed a soul-keying bug carried from the design-A slice (soul_set now PUTs withid = agentId:keyso soul_get can find it).resources/mcp-oauth.ts— registersserver.http(withMCPAuth(mcpHandler), { urlPath: '/mcp' })only when the flag is on (its own dispatch chain; flair's default auth-middleware doesn't run for/mcp).getConfigpins issuer/resource so iss/aud checks match the minted tokens.- Sherlock's 4 reqs: (1) short-lived tokens via
mcp.accessTokenTtl(5–15 min) + refresh; (2) RS256 pinning — the plugin is RS256-only by construction (none/HS256 structurally rejected); (3) dual-auth precedence —/mcpis OAuth-only on its own chain, Ed25519 never reaches it, they can't collide; (4) DCR authentication viainitialAccessToken+ the JIT trust anchor. @harperfast/oauth@2.1.0added exact-pinned; on the supply-chain keep-current allow-list (same high-trust@harperfast/*owner as@harperfast/harper; only loaded when the default-OFF surface is enabled — zero exposure in the default build). Documented indocs/supply-chain-policy.mdanddocs/notes/mcp-oauth-model2.md.- Deferred (not shipped): live
config.yamlwiring of the AS plugin (kept out to preserve byte-identical flag-OFF; documented for operators) and migrating the homegrownOAuth.ts/XAA.ts(deprecate-don't-delete — they stay for the Ed25519 path).
v0.16.1
🐛 flair upgrade — detect an installed-but-stale flair-mcp, drop openclaw noise, fix formatting (#543)
The bin --version probe missed a globally-installed flair-mcp (older installs predate --version) → it now falls back to the lib probe (reads the installed package.json version, version-independent), so a stale-but-present flair-mcp is correctly detected. The openclaw-flair line is suppressed when openclaw isn't installed (still shown under --all), dropping noise on machines without openclaw. Fixed a double-space in the restart hint. Added a one-line scope note: flair upgrade covers the npm-global surface + openclaw plugins; pi-flair / langgraph-flair / n8n-nodes-flair / hermes-flair upgrade within their own ecosystems. Fixes ops-p42n (surfaced by Kyle's real-world use).
🤖 Auto-cut GitHub releases from the CHANGELOG on tag (#544)
Every v* tag now creates its GitHub release from the matching CHANGELOG section — idempotent (create-or-edit), injection-safe (tag/version passed via env, notes via --notes-file), and independent of the npm 2FA staging gate.