Skip to content

Releases: tpsdev-ai/flair

v0.22.1

Choose a tag to compare

@github-actions github-actions released this 14 Jul 15:37
74deb45

🐛 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, where reserve = 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 in resources/migrations/space.ts).
  • FLAIR_MIGRATION_RESERVE_BYTES overrides the computed reserve for constrained deployments (validated finite/non-negative; 0 disables the reserve check entirely, leaving only the raw fit test) — mirrors the existing FLAIR_MIGRATION_TEST_FREE_BYTES test-override pattern.
  • headroomFloor (the old fraction-of-total DI knob on checkSpace/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 the dataDir volume's fraction and never could have helped this class of halt) — now states the human-readable bytes needed vs. available vs. the reserve, and names FLAIR_MIGRATION_RESERVE_BYTES as 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 new humanBytes() 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

Choose a tag to compare

@github-actions github-actions released this 13 Jul 23:45
bce6b27

⬆️ 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.

  • authorizeLocal now defaults to false (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 as super_user now gets rejected. The admin credential (~/.flair/admin-pass, --admin-pass, or FLAIR_ADMIN_PASS) is now load-bearing for any local tooling that talks to the ops API directly. flair init/agent add/principal add are unaffected (they already passed real credentials). To restore the old, less-safe behavior for local dev only, set authorizeLocal: true in config.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_ENABLED to true, which changes getModelId()'s output (rows now stamp <model>+searchprefix instead of the bare model id). Every memory written under 0.21.0 or earlier reads as stale under the new stamp. The always-on embedding-stamp migration (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-only exists 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_CONFIG env var, which turned out to persist into harper-config.yaml and 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 resulting models: {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.ts calls harper-fabric-embeddings's own register() factory directly on every boot) — nothing is ever written to harper-config.yaml for 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 rapid and requires manual Harper config to actually run. flair rem rapid now calls Harper's models.generate() server-side and stages MemoryCandidate rows by default, instead of just printing a prompt for you to paste elsewhere. --prompt-only restores 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 a models: block to Harper's root instance config (harper-config.yaml/harperdb-config.yaml at the Harper data directory) — not flair's own config.yaml, which Harper only ever loads as a non-root component config. The specific key is models.generative.<logicalName> (distinct from embeddings' models.embedding.default namespace under the same root block). Verified example from the now-shipped docs/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 via FLAIR_REM_MODEL=hosted; apiKey must 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 own docs/secrets-and-keys.md does 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 no models: backend is configured, flair rem rapid (execute-mode) and the nightly runner's distillation step both fail closed (503 no_backend) — the nightly runner logs it into the audit row's errors[] 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.md via #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's thinking field, which the backend doesn't read, so every REM execute run fails closed with distillation_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-next staged 7 quality candidates in ~7s, dedup held on a second run, a promoted candidate landed with derivedFrom intact.
    • Nightly cycle now spends model tokens/compute nightly, once a backend is configured — step 5 of the nightly runner calls /ReflectMemories with execute: true after 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 a models: backend.
    • Review loop is unchanged and still the only promotion path: flair rem candidates lists 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 enable installs 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.

🌙 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: true on POST /ReflectMemories (resources/MemoryReflect.ts, resources/memory-reflect-lib.ts) runs distillation server-side via models.generate() — schema-constrained output on the first attempt, a json-mode fallback with one retry on malformed output, fail-closed (no retry) on a thrown network/timeout error. Validated output stages MemoryCandidate rows: shape validation, sourceMemoryIds checked 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 via FLAIR_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/harper 5.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. generatedBy on a staged candida...
Read more

CI model mirror (not a flair release)

Pre-release

Choose a tag to compare

@tps-flint tps-flint released this 14 Jul 00:56
bce6b27

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

Choose a tag to compare

@github-actions github-actions released this 07 Jul 20:51
0a74a97

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 own MemoryGrant traversal), 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 a MemoryGrant is 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 originatorInstanceId stamped 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 doctor verifies client integration (#599) — a new "Client integration" section answers "is Flair working for my agent?": per detected MCP client, the MCP block + FLAIR_URL reachability + agent registration; for Claude Code, the CLAUDE.md bootstrap line + SessionStart hook. --fix wires missing pieces (idempotent, merge-safe).
  • flair doctor reports 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 (401 unknown_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 the unknown_agent marker and a resolved local key correctly reports not-registered, with the fix hint.
  • flair init wires all three legs (#600) — init now installs the SessionStart hook + CLAUDE.md line alongside the MCP block, instead of leaving them manual (silent partial setups). --skip-hook / --skip-claude-md opt-outs; prints the exact missing snippet when skipped.
  • flair-mcp auto-sets presence on session-start + rate-limited heartbeat (#608, closes #598) — the session-start hook and bootstrap seed activity/currentTask; every other MCP tool call refreshes lastHeartbeatAt (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 / doctor surface when the installed version is behind the published latest (cached, offline-tolerant, never blocks).
  • agent add / principal add admin-pass fallback (#593) — fall back to the local ~/.flair/admin-pass file instead of hard-requiring --admin-pass.

🔒 Security

  • OAuthAuthorize consent required real auth; Presence PUT/DELETE scoped correctly (#609, closes #604) — closes the authorizeLocal escalation class: a credential-less loopback POST (which Harper's authorizeLocal forges as super_user) could mint an admin OAuth code without a real Authorization header. Loopback-only, HIGH severity — verified not remotely exploitable (Fabric rejects the unauthed remote request with 401). Also scopes the /Presence early-return to GET-only so PUT/DELETE correctly transit the auth middleware, and fixes a pre-existing bug where Response.redirect's immutable Headers 500'd every POST /OAuthAuthorize on main.
  • Presence.currentTask gated to verified readers (#601, closes #592) — anonymous GET /Presence returned agents' freeform currentTask (which can hold customer/host/incident strings) verbatim on a public endpoint. Now gated behind a verified Ed25519 signature (not just resolveAgentAuth, which Harper's authorizeLocal can spoof for a loopback caller) — anonymous, loopback, and Basic-admin callers get the low-risk roster with currentTask nulled; 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-mcp package tests into the merge gate (#605, closes #491) — the 34 packages/flair-mcp/test/* tests weren't gated by CI (root test.yml only ran test/unit/); now builds flair-client first (flair-mcp imports its built dist/), 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-errors on 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

Choose a tag to compare

@github-actions github-actions released this 05 Jul 04:04
6f19218

🛠 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, env FABRIC_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-verify escape hatch; --verify-resource <name> override; --verify-timeout <ms> (default 300000). flair upgrade inherits the same protection.

v0.20.0

Choose a tag to compare

@github-actions github-actions released this 05 Jul 00:52
d91ba37

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); a private memory is never returned to a non-owner on any path. Migration-invariant — existing memories keep their exact access (visibility != private treats no-visibility as shared). Also deletes the SemanticSearch visibility=="office" global read leak.
  • Surface teammate findings (#568). Bootstrap surfaces grant-visible teammate memories relevant to currentTask in 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 $distance when 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) and Credential.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.sh aligns bun.lock leaf specifiers after bump — stops the recurring --frozen-lockfile desync (#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

Choose a tag to compare

@github-actions github-actions released this 03 Jul 19:06
d3b86f3

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

Choose a tag to compare

@github-actions github-actions released this 03 Jul 14:18
7f2676d

🧠 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 /mcp path (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 a write grant.

🔎 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

Choose a tag to compare

@github-actions github-actions released this 02 Jul 21:56
2dde091

🔒 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). On tools/call it resolves the withMCPAuth-verified token sub → a flair Agent via Credential(kind:"idp", idpSubject=sub)principalId (the same identity surface XAA uses), establishes the request.tpsAgent scoping context, and delegates to the existing resource handler. An unresolvable sub is 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 with id = agentId:key so soul_get can find it).
  • resources/mcp-oauth.ts — registers server.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). getConfig pins 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 — /mcp is OAuth-only on its own chain, Ed25519 never reaches it, they can't collide; (4) DCR authentication via initialAccessToken + the JIT trust anchor.
  • @harperfast/oauth@2.1.0 added 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 in docs/supply-chain-policy.md and docs/notes/mcp-oauth-model2.md.
  • Deferred (not shipped): live config.yaml wiring of the AS plugin (kept out to preserve byte-identical flag-OFF; documented for operators) and migrating the homegrown OAuth.ts/XAA.ts (deprecate-don't-delete — they stay for the Ed25519 path).

v0.16.1

Choose a tag to compare

@github-actions github-actions released this 01 Jul 13:21
5fb960b

🐛 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.