@tpsdev-ai/flair 0.26.0 → 0.27.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/README.md +2 -0
- package/dist/cli.js +434 -50
- package/dist/doctor-client.js +47 -0
- package/docs/assets/flair-cross-orchestrator.cast +33 -0
- package/docs/assets/flair-cross-orchestrator.gif +0 -0
- package/docs/assets/flair-demo.cast +18 -0
- package/docs/assets/flair-demo.gif +0 -0
- package/docs/auth.md +178 -0
- package/docs/bridges.md +291 -0
- package/docs/claude-code.md +202 -0
- package/docs/deployment.md +204 -0
- package/docs/entity-vocabulary.md +109 -0
- package/docs/federation.md +179 -0
- package/docs/integrations.md +197 -0
- package/docs/mcp-clients.md +240 -0
- package/docs/n8n-management.md +142 -0
- package/docs/n8n.md +122 -0
- package/docs/notes/adk-spike-findings-2026-05-05.md +331 -0
- package/docs/notes/mcp-agent-auth-consumer.md +229 -0
- package/docs/notes/mcp-oauth-model2.md +132 -0
- package/docs/notes/rem-ux.md +39 -0
- package/docs/openclaw.md +131 -0
- package/docs/quickstart.md +153 -0
- package/docs/releasing.md +173 -0
- package/docs/rem.md +60 -0
- package/docs/rerank-provisioning.md +67 -0
- package/docs/secrets-and-keys.md +173 -0
- package/docs/spoke-bringup.md +303 -0
- package/docs/supply-chain-policy.md +156 -0
- package/docs/system-requirements.md +42 -0
- package/docs/the-team.md +129 -0
- package/docs/troubleshooting.md +187 -0
- package/docs/upgrade.md +416 -0
- package/package.json +2 -1
package/docs/rem.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# REM — reflection, distillation, and review
|
|
2
|
+
|
|
3
|
+
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 candidates for explicit human/agent review — nothing is ever auto-promoted. `flair rem rapid` runs it on demand; `flair rem nightly enable` runs it on a schedule. See [`docs/notes/rem-ux.md`](notes/rem-ux.md) for the full trigger model, locality guarantees, and the review-loop UX this page's commands feed into.
|
|
4
|
+
|
|
5
|
+
## Configuration
|
|
6
|
+
|
|
7
|
+
Distillation runs **server-side**, via Harper's model-access API (`models.generate()`). Flair ships zero provider code — which backend answers a REM call is entirely a Harper `models:` configuration decision.
|
|
8
|
+
|
|
9
|
+
The `models:` block goes in **Harper's root instance config** (`harper-config.yaml` / `harperdb-config.yaml` at the Harper data directory) — **not** in flair's own `config.yaml` component config, which Harper only loads as a non-root component config and never reads a `models:` block from.
|
|
10
|
+
|
|
11
|
+
**Local Ollama (zero-key default):**
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
# harper-config.yaml
|
|
15
|
+
models:
|
|
16
|
+
generative:
|
|
17
|
+
default: # unset FLAIR_REM_MODEL resolves to this logical name
|
|
18
|
+
backend: ollama
|
|
19
|
+
host: localhost:11434 # optional — already the default
|
|
20
|
+
model: llama3.1 # required — Ollama has no built-in default model
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
No credentials, nothing leaves the box (see the warning below).
|
|
24
|
+
|
|
25
|
+
> **⚠️ Pick a non-thinking model for the Ollama backend.**
|
|
26
|
+
> 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` (no candidates are ever staged — the failure is availability, not correctness). Use a non-thinking model (`llama3.1`, `qwen3-coder-next`, `gemma3`, …) until the upstream backend behavior changes. Tracked in #712.
|
|
27
|
+
|
|
28
|
+
**Hosted provider** (OpenAI / Anthropic / Bedrock also supported — `backend: openai|anthropic|bedrock`):
|
|
29
|
+
|
|
30
|
+
```yaml
|
|
31
|
+
# harper-config.yaml
|
|
32
|
+
models:
|
|
33
|
+
generative:
|
|
34
|
+
hosted:
|
|
35
|
+
backend: openai
|
|
36
|
+
apiKey: ${OPENAI_API_KEY} # env-var indirection — never a literal key in YAML
|
|
37
|
+
model: gpt-4o-mini
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
A literal (non-`${VAR}`) `apiKey` in the config file is flagged at Harper boot — keep it out of the YAML on disk. On Fabric / managed deploys, the env var itself is provisioned through Harper's Fabric secrets mechanism, which encrypts the value at rest (`enc:v1:` storage format) rather than holding it in plaintext; consult Harper's Fabric secrets documentation for provisioning that env var. Flair's own [`docs/secrets-and-keys.md`](secrets-and-keys.md) covers Flair's Ed25519 agent identity and general client-side credential patterns, but does not cover this Harper-side mechanism.
|
|
41
|
+
|
|
42
|
+
> **⚠️ Data egress is a configuration decision.**
|
|
43
|
+
> Pointing `models:` at a hosted provider (OpenAI, Anthropic, Bedrock, or any other network backend) sends the memory content being reflected on to that provider. A local Ollama backend keeps everything on the box — nothing transits the network. Default posture: local. Choose a hosted backend deliberately, and know what leaves when you do.
|
|
44
|
+
|
|
45
|
+
### `FLAIR_REM_MODEL`
|
|
46
|
+
|
|
47
|
+
Selects which `models.generative.<logicalName>` entry a REM call uses. Unset → Harper's default routing (the `default` logical name above). Set it to route to a different registered backend, e.g. `FLAIR_REM_MODEL=hosted`.
|
|
48
|
+
|
|
49
|
+
### Clustered deploys — nightly enable is per-node, deliberately
|
|
50
|
+
|
|
51
|
+
`flair rem nightly enable` installs a platform-native timer (launchd / systemd) **on the host it runs on**. In a multi-node or Fabric deploy, enabling it on every node would run the cycle N times and scatter N sets of pre-cycle snapshots. The v1 rule: **exactly one node gets the timer** — pick it deliberately, the same way you'd pick a cron owner for any single-writer job. This is a v1 constraint, not a permanent one; see #709 for the roadmap toward a coordinated multi-node story.
|
|
52
|
+
|
|
53
|
+
Snapshot locality follows from this: a nightly cycle's pre-run snapshot (`~/.flair/snapshots/<agent>/`) lands on **the node that ran that cycle** — `flair rem restore <date>` and `flair rem snapshot list` only see local snapshots. If you move which node owns the timer, snapshot history doesn't move with it.
|
|
54
|
+
|
|
55
|
+
## Interactive vs nightly
|
|
56
|
+
|
|
57
|
+
- **Interactive (`flair rem rapid`):** one bounded, synchronous distillation call — gather cap 50 memories, bounded output tokens, seconds not minutes. Executes by default, staging candidates and printing a summary; `--prompt-only` returns the reflection prompt instead, for the bring-your-own-model handoff.
|
|
58
|
+
- **Nightly (`flair rem nightly enable` / `run-once`):** fully detached — the scheduler runs the full cycle (snapshot → maintenance → distillation), candidates land as pending rows, and an audit row lands in `~/.flair/logs/rem-nightly.jsonl`. The operator reviews in the morning via `flair rem candidates`.
|
|
59
|
+
|
|
60
|
+
Either path, the review loop is the same: `flair rem candidates` lists pending rows, `flair rem promote <id> --rationale "<why>"` / `flair rem reject <id> --reason "<why>"` decide them. Nothing self-promotes — see [`docs/notes/rem-ux.md`](notes/rem-ux.md) for why that gate is load-bearing and how the surface is expected to evolve.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Reranker model provisioning
|
|
2
|
+
|
|
3
|
+
Flair's optional cross-encoder rerank stage (`resources/rerank-provider.ts`, gated
|
|
4
|
+
behind `FLAIR_RERANK_ENABLED`) loads its model GGUF **in-process** via the same
|
|
5
|
+
node-llama-cpp engine the embedding engine ships. The GGUF is **not** committed to
|
|
6
|
+
the repo (`*.gguf` is gitignored) — it is provisioned into `models/` manually,
|
|
7
|
+
exactly like the embedding model.
|
|
8
|
+
|
|
9
|
+
The reranker is **OFF by default**. You only need to provision a GGUF if you are
|
|
10
|
+
turning it on (`FLAIR_RERANK_ENABLED=true`) or running the recall-bench A/B.
|
|
11
|
+
|
|
12
|
+
## Models
|
|
13
|
+
|
|
14
|
+
| `FLAIR_RERANK_MODEL` | GGUF filename (under `models/`) | Source | Inference mode |
|
|
15
|
+
|---|---|---|---|
|
|
16
|
+
| `qwen3-reranker-0.6b-q8` (default) | `Qwen3-Reranker-0.6B-q8_0.gguf` | `Mungert/Qwen3-Reranker-0.6B-GGUF` (q8_0) | generative yes/no |
|
|
17
|
+
| `jina-reranker-v2` | `jina-reranker-v2-base.Q8_0.gguf` | `gpustack/jina-reranker-v2-base-multilingual-GGUF` (q8_0) | rank-pooling cross-encoder |
|
|
18
|
+
|
|
19
|
+
Download into `models/` next to the embedding GGUF, e.g.:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
# quality model (Qwen3 generative path)
|
|
23
|
+
huggingface-cli download Mungert/Qwen3-Reranker-0.6B-GGUF \
|
|
24
|
+
Qwen3-Reranker-0.6B-q8_0.gguf --local-dir models/
|
|
25
|
+
|
|
26
|
+
# latency model (jina rank API)
|
|
27
|
+
huggingface-cli download gpustack/jina-reranker-v2-base-multilingual-GGUF \
|
|
28
|
+
jina-reranker-v2-base.Q8_0.gguf --local-dir models/
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
If the GGUF is missing, the provider logs one warning and recall falls back to
|
|
32
|
+
vector order — it never blocks or breaks search.
|
|
33
|
+
|
|
34
|
+
## Config
|
|
35
|
+
|
|
36
|
+
| Env var | Default | Meaning |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `FLAIR_RERANK_ENABLED` | unset (**OFF**) | Master flag. `"true"` to enable. |
|
|
39
|
+
| `FLAIR_RERANK_MODEL` | `qwen3-reranker-0.6b-q8` | Model + inference mode (table above). |
|
|
40
|
+
| `FLAIR_RERANK_TOPN` | `50` | Candidate count fed to the reranker; caps the HNSW fetch. |
|
|
41
|
+
| `FLAIR_RERANK_BUDGET_MS` | `2500` | Hard latency budget; exceeded → vector order. |
|
|
42
|
+
| `FLAIR_RERANK_MIN_CANDIDATES` | `2` | Skip rerank below this many candidates. |
|
|
43
|
+
|
|
44
|
+
## Why this serving path (not Ollama, not a microservice)
|
|
45
|
+
|
|
46
|
+
- **In-process node-llama-cpp** is the same engine the embedding engine already
|
|
47
|
+
ships — no new infra, no network hop, no auth boundary. The reranker GGUF lives
|
|
48
|
+
next to the embedding GGUF and loads via the same addon-discovery pattern as
|
|
49
|
+
`embeddings-provider.ts`.
|
|
50
|
+
- **Ollama is out:** it has no rerank endpoint and silently drops next-token
|
|
51
|
+
logprobs, so it can serve neither the jina rank path nor the Qwen3 generative
|
|
52
|
+
yes/no path. (Verified live against newton's Ollama 0.30.10.)
|
|
53
|
+
|
|
54
|
+
## Known limitation — Qwen3 generative path inside Harper
|
|
55
|
+
|
|
56
|
+
The Qwen3 generative path scores `P(yes)/(P(yes)+P(no))` via node-llama-cpp's
|
|
57
|
+
`controlledEvaluate` with next-token probabilities. This works standalone and in a
|
|
58
|
+
plain Node worker thread, but **inside Harper's resource runtime — where HFE's
|
|
59
|
+
embedding engine has already initialized a separate native llama backend —
|
|
60
|
+
`controlledEvaluate` returns an empty result (no decoded logits).** The provider
|
|
61
|
+
detects this (`out.length === 0`), throws, and **fails open to vector order** (it
|
|
62
|
+
never writes corrupt scores). Net effect today: with `FLAIR_RERANK_MODEL=qwen3-...`
|
|
63
|
+
the rerank stage cleanly no-ops inside Harper.
|
|
64
|
+
|
|
65
|
+
The `jina-reranker-v2` rank-API path (`createRankingContext()` / `rankAll`) works
|
|
66
|
+
inside Harper. See the integration PR for the live recall-bench A/B numbers and the
|
|
67
|
+
go/no-go read.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Secrets and keys
|
|
2
|
+
|
|
3
|
+
Flair owns **identity**. Flair does **not** own arbitrary secrets. This page draws the line and shows you how to wire both into your agent setup without leaking anything into shell history, repo configs, or process arguments.
|
|
4
|
+
|
|
5
|
+
## What Flair owns: agent identity (Ed25519)
|
|
6
|
+
|
|
7
|
+
For each registered agent, Flair stores:
|
|
8
|
+
|
|
9
|
+
- A **public key** in the `Agent` table (server-side; used to verify signed requests).
|
|
10
|
+
- A **private key** at `~/.flair/keys/<agent>.key` on the host that owns that agent (PKCS8 base64). Created by `flair agent add <id>`. Mode `0600`.
|
|
11
|
+
|
|
12
|
+
Agents sign every request to Flair with this key. Flair refuses unsigned requests and refuses signatures that don't match the registered public key. The signed payload is `<agentId>:<timestamp>:<nonce>:<METHOD>:<path>` with a 30-second replay window and nonce dedup — replays inside that window are rejected.
|
|
13
|
+
|
|
14
|
+
**This is the only secret material Flair manages.** Lose the key file and the agent is locked out (`flair agent rotate <id>` to issue a new pair).
|
|
15
|
+
|
|
16
|
+
## Flair admin password (Harper instance)
|
|
17
|
+
- If not provided via `--admin-pass`, `--admin-pass-file`, `FLAIR_ADMIN_PASS`, or `HDB_ADMIN_PASSWORD`, a random password is generated and written to `~/.flair/admin-pass` (mode `0o600`). The password is **not** printed to the console.
|
|
18
|
+
- The `--admin-pass-file <path>` option allows reading the password from a file (for pre-staged secrets).
|
|
19
|
+
- The `--admin-pass <pass>` option is deprecated due to shell history exposure; use `--admin-pass-file` or environment variables instead. A warning is printed when this option is used.
|
|
20
|
+
- Environment variables `FLAIR_ADMIN_PASS` and `HDB_ADMIN_PASSWORD` are also supported.
|
|
21
|
+
## What Flair does *not* own: API keys, tokens, third-party credentials
|
|
22
|
+
|
|
23
|
+
Things that are NOT Flair's job:
|
|
24
|
+
|
|
25
|
+
- LLM provider API keys (Anthropic, OpenAI, Gemini, DeepSeek, Ollama Cloud, etc.)
|
|
26
|
+
- Database connection strings
|
|
27
|
+
- Cloud provider credentials (AWS, GCP, Azure)
|
|
28
|
+
- GitHub PATs, GitLab tokens, npm publish tokens
|
|
29
|
+
- Webhook URLs containing secrets (Discord, Slack, etc.)
|
|
30
|
+
- Anything else your agent needs to talk to the rest of the world
|
|
31
|
+
|
|
32
|
+
These belong in your **OS keyring** (macOS Keychain, Linux secret-service, Windows Credential Manager) or a dedicated secrets manager (1Password, HashiCorp Vault, age-sops, AWS Secrets Manager). Flair stays focused on what it's good at — identity and memory — and inherits the OS-level security model for everything else.
|
|
33
|
+
|
|
34
|
+
## Patterns for wiring secrets into agent CLIs
|
|
35
|
+
|
|
36
|
+
The general principle: **never put a secret in a config file checked into a repo, never pass one as a command-line argument, never `echo $SECRET` in a shell that has history**. Read it at process-start from the OS keyring or an env-only source, and pass it through the env to the child process.
|
|
37
|
+
|
|
38
|
+
### macOS — Keychain
|
|
39
|
+
|
|
40
|
+
Store once via the Keychain Access app, or:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
security add-generic-password -a "$USER" -s "anthropic-api-key" -w "sk-ant-..."
|
|
44
|
+
# read at use:
|
|
45
|
+
security find-generic-password -a "$USER" -s "anthropic-api-key" -w
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
In your `.mcp.json` / `~/.gemini/settings.json` / `~/.codex/config.toml`, reference an env var (don't put the secret literal). Then export the env var from a shell wrapper that reads from Keychain at start:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# ~/.config/agent-env.sh — sourced by your shell rc, NOT checked into git
|
|
52
|
+
export ANTHROPIC_API_KEY="$(security find-generic-password -a "$USER" -s "anthropic-api-key" -w)"
|
|
53
|
+
export OPENAI_API_KEY="$(security find-generic-password -a "$USER" -s "openai-api-key" -w)"
|
|
54
|
+
export GEMINI_API_KEY="$(security find-generic-password -a "$USER" -s "gemini-api-key" -w)"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Then your agent CLI configs can reference `${ANTHROPIC_API_KEY}` etc. by name.
|
|
58
|
+
|
|
59
|
+
### Linux — secret-service (GNOME Keyring / KWallet via libsecret)
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Store
|
|
63
|
+
secret-tool store --label="Anthropic API Key" service anthropic-api-key
|
|
64
|
+
# (paste the secret when prompted)
|
|
65
|
+
|
|
66
|
+
# Read
|
|
67
|
+
secret-tool lookup service anthropic-api-key
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Same wrapper-script pattern: read from `secret-tool` in `~/.config/agent-env.sh`, export as env vars.
|
|
71
|
+
|
|
72
|
+
### 1Password CLI (cross-platform, recommended for teams)
|
|
73
|
+
|
|
74
|
+
1Password's `op` CLI gives you reproducible secret-loading in scripts and CI:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
op signin
|
|
78
|
+
|
|
79
|
+
# Read at use
|
|
80
|
+
ANTHROPIC_API_KEY="$(op item get "Anthropic" --field credential --reveal)"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
For agent CLI configs, run them under `op run`, which substitutes `op://` references at process-start without ever touching disk:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
op run --env-file=.env.agent -- claude
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Where `.env.agent` (checked-in-able, just references — no secrets) contains:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
ANTHROPIC_API_KEY=op://Personal/Anthropic/credential
|
|
93
|
+
OPENAI_API_KEY=op://Personal/OpenAI/credential
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### age + sops (for repo-checked-in encrypted secrets)
|
|
97
|
+
|
|
98
|
+
If you must store secrets in a repo (e.g. a deployment config that includes a webhook URL), encrypt them with [sops](https://github.com/getsops/sops) using [age](https://github.com/FiloSottile/age) keys. Decrypt at deploy time, never in source.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# Encrypt a secrets file
|
|
102
|
+
sops --age $(cat ~/.config/sops/age/keys.txt | grep public | cut -d' ' -f4) \
|
|
103
|
+
--encrypt --in-place secrets.env
|
|
104
|
+
|
|
105
|
+
# At process start
|
|
106
|
+
sops --decrypt secrets.env > /tmp/.env.runtime && \
|
|
107
|
+
set -a && . /tmp/.env.runtime && set +a && \
|
|
108
|
+
shred -u /tmp/.env.runtime
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Wiring keys into the major agent CLIs
|
|
112
|
+
|
|
113
|
+
The pattern is identical across CLIs: the CLI config references env var names, your shell wrapper exports those env vars from the OS keyring at start. **The CLI config never holds the secret literal.**
|
|
114
|
+
|
|
115
|
+
### Claude Code
|
|
116
|
+
|
|
117
|
+
`claude` reads `ANTHROPIC_API_KEY` from the env. Set it via the wrapper-from-Keychain pattern above; never `claude --api-key sk-ant-...` (writes to shell history).
|
|
118
|
+
|
|
119
|
+
The flair-mcp server (`@tpsdev-ai/flair-mcp`) reads `FLAIR_AGENT_ID` and (optionally) `FLAIR_KEY_PATH` from its own env block in `.mcp.json`. The Flair private key isn't a "secret" you load from Keychain — it's a key file that already lives at a fixed path with `0600` mode, owned by Flair.
|
|
120
|
+
|
|
121
|
+
### Gemini CLI
|
|
122
|
+
|
|
123
|
+
`gemini` reads `GEMINI_API_KEY` (or `GOOGLE_API_KEY` depending on the auth mode) from the env. Same wrapper pattern.
|
|
124
|
+
|
|
125
|
+
For the flair-mcp server: in `~/.gemini/settings.json`, the `mcpServers.flair.env` block declares `FLAIR_AGENT_ID`, but the value is just a string (the agent id, not a secret).
|
|
126
|
+
|
|
127
|
+
### OpenAI Codex CLI
|
|
128
|
+
|
|
129
|
+
`codex` reads `OPENAI_API_KEY` from the env. Same wrapper pattern.
|
|
130
|
+
|
|
131
|
+
For the flair-mcp server: in `~/.codex/config.toml`, the `[mcp_servers.flair.env]` table declares `FLAIR_AGENT_ID` (just a string).
|
|
132
|
+
|
|
133
|
+
### Hermes (Nous Research)
|
|
134
|
+
|
|
135
|
+
Hermes uses `~/.hermes/.env` for provider API keys (managed by `hermes auth`). The Flair plugin (`packages/hermes-flair/`) reads `FLAIR_AGENT_ID` and `FLAIR_KEY_PATH` from env or `$HERMES_HOME/flair.json`. Per the plugin's own `get_config_schema()`, secret fields go to `.env`, non-secret fields go to JSON.
|
|
136
|
+
|
|
137
|
+
## What to do with the Flair private key itself
|
|
138
|
+
|
|
139
|
+
`~/.flair/keys/<agent>.key` is the only secret Flair generates. Treat it like an SSH private key:
|
|
140
|
+
|
|
141
|
+
- **Stays on the host that owns the agent.** If your agent runs on a given host, the key lives on that host. If you spin up the same agent on another machine, **don't copy the key** — register a new agent identity (`flair agent add <id>-on-<other-host>`) on that machine. Different identities, same Flair instance can store memories for both, you decide cross-agent visibility.
|
|
142
|
+
- **`chmod 600` enforced** by `flair agent add`. Don't relax it.
|
|
143
|
+
- **Don't check it into git.** `.gitignore` should already exclude `~/.flair/keys/`; if you're ever tempted to share keys for "convenience," rotate first (`flair agent rotate <id>`).
|
|
144
|
+
- **Backup separately**, encrypted. The `flair backup` command excludes private keys by default. Roll your own backup of `~/.flair/keys/` via age-encrypted archive if you want offsite recovery.
|
|
145
|
+
|
|
146
|
+
## What about a `flair secret` CLI?
|
|
147
|
+
|
|
148
|
+
Considered, deferred. Flair could ship a thin wrapper around the OS keyring (`flair secret get/set/list`) — but the OS primitives already work and are universally trusted. Adding a Flair-shaped wrapper would mean we own the bug surface for marginal ergonomic gain. Better path: document the OS primitives well (this page) and stay focused on identity + memory.
|
|
149
|
+
|
|
150
|
+
If you find yourself wanting one anyway, your agent can call `security find-generic-password` / `secret-tool lookup` / `op read` directly — no Flair involvement needed.
|
|
151
|
+
|
|
152
|
+
## Threat model summary
|
|
153
|
+
|
|
154
|
+
| Asset | Owned by | If compromised → |
|
|
155
|
+
|---|---|---|
|
|
156
|
+
| Flair agent private key (`~/.flair/keys/<agent>.key`) | Flair (you, on the host) | Attacker can **write** memories under that agent's identity and read that agent's **`private`**-marked memories until you rotate. Use `flair agent rotate <id>`. Other agents' write identity is unaffected — they can't be impersonated with this key. |
|
|
157
|
+
| LLM provider API keys (Anthropic, OpenAI, etc.) | OS keyring / 1Password | Standard provider revocation: rotate the key in the provider's console, update keyring entry. |
|
|
158
|
+
| Cross-host secrets (1Password vault, age-sops) | The secret manager itself | Trust falls back to that manager's MFA / key handling. Document recovery in your team's ops runbook. |
|
|
159
|
+
| Memory contents | Flair (server-side) | Write access requires the owning agent's key → see "Per-agent write isolation, org-wide non-private read" below. |
|
|
160
|
+
|
|
161
|
+
### Per-agent write isolation, org-wide non-private read
|
|
162
|
+
|
|
163
|
+
Writes are scoped per `agentId` and isolation is enforced **server-side** by Ed25519 signature verification — not by client convention. An attacker with another agent's key can write memories as that agent, but cannot forge writes under *your* agent's identity without *your* key.
|
|
164
|
+
|
|
165
|
+
Reads are a different, and intentionally more open, story: within one Flair instance (one org), **any** verified agent — not just an attacker with a stolen key — can already read **any other agent's non-private memory**. That's the shipped access model (open-within-org read; see [SECURITY.md](../SECURITY.md)), not a consequence of key compromise. A key leak's actual *incremental* exposure is narrower than "read your agent's memories": it's (1) the ability to write under that agent's identity, and (2) the ability to read that agent's `visibility: private` memories, which stay owner-only under normal operation. Non-private memories were already org-readable before the leak.
|
|
166
|
+
|
|
167
|
+
The hard boundary in this model is the **federation edge** — a separate Flair instance / org. That's a different threat model from password-based or API-key-based memory services where a leaked key gives access to the full namespace across every trust boundary, not just one org.
|
|
168
|
+
|
|
169
|
+
## See also
|
|
170
|
+
|
|
171
|
+
- [`docs/auth.md`](auth.md) — full auth scheme and signature format
|
|
172
|
+
- [`docs/mcp-clients.md`](mcp-clients.md) — wiring the flair-mcp server into Claude Code / Gemini CLI / Codex CLI
|
|
173
|
+
- [`packages/hermes-flair/README.md`](../packages/hermes-flair/README.md) — Hermes-specific plugin auth notes
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# Spoke Bring-Up Recipe
|
|
2
|
+
|
|
3
|
+
Headless recipe to stand up a new Flair spoke that federates to the `flair.dtrt` hub. This is a **cut-and-paste operator guide** — every command is real and tested against the current CLI.
|
|
4
|
+
|
|
5
|
+
**Target:** fresh linux-x64 VM.
|
|
6
|
+
**Result:** a running spoke with an Ed25519-registered agent, paired to the hub, and pushing memories.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 1. Prerequisites
|
|
11
|
+
|
|
12
|
+
- **Node.js ≥ 22** on a linux-x64 VM. Verify:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
node --version # ≥ v22.x
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- **Outbound HTTPS** to the hub. The spoke pushes to the hub on `flair federation sync`; the hub never dials back.
|
|
19
|
+
|
|
20
|
+
- **Admin access** to the hub — you'll need someone (or yourself, on the hub host) to run `flair federation token` and give you the resulting JSON triple.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 2. Install Flair
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install -g @tpsdev-ai/flair
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Confirm the CLI is reachable:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
flair --version
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 3. Bootstrap the Spoke (init + agent)
|
|
39
|
+
|
|
40
|
+
`flair init` handles Harper install, data-dir setup, admin-password generation, and agent registration in one shot.
|
|
41
|
+
|
|
42
|
+
### 3a. Default (auto-generated admin pass, default data dir)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
flair init --agent-id <spoke-agent-id>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- Harper installs into `~/.flair/data/`.
|
|
49
|
+
- Admin password is auto-generated and written to `~/.flair/admin-pass` (mode 0600).
|
|
50
|
+
- An Ed25519 keypair is created at `~/.flair/keys/<agent-id>.key`.
|
|
51
|
+
- After boot, the interactive soul wizard runs — press `s` to skip in a headless pipe, or use `--skip-soul`.
|
|
52
|
+
|
|
53
|
+
### 3b. Explicit data dir + admin pass (headless)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
export FLAIR_ADMIN_PASS="$(openssl rand -base64 24)"
|
|
57
|
+
mkdir -p /data/flair
|
|
58
|
+
|
|
59
|
+
flair init \
|
|
60
|
+
--agent-id <spoke-agent-id> \
|
|
61
|
+
--data-dir /data/flair \
|
|
62
|
+
--admin-pass "$FLAIR_ADMIN_PASS" \
|
|
63
|
+
--skip-soul
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The admin password is passed via env (`FLAIR_ADMIN_PASS` / `HDB_ADMIN_PASSWORD`), which keeps it out of shell history. If you use `--admin-pass inline-value`, the CLI prints a warning.
|
|
67
|
+
|
|
68
|
+
### 3c. Custom port
|
|
69
|
+
|
|
70
|
+
Default is **19926** for REST / **19925** for ops. If you need to override (port conflict, multiple instances):
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
flair init --agent-id <id> --port 8000
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Port is persisted to `~/.flair/config.yaml`. The ops port is always REST port − 1 (derived automatically).
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 4. Verify the Spoke Is Running
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
flair status
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Expected output: green **🟢 running** with your agent listed. If you see **🔴 unreachable**, check [docs/troubleshooting.md](troubleshooting.md).
|
|
87
|
+
|
|
88
|
+
To check the instance's federation identity:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
flair federation status
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
At this point you'll see your instance ID, public key, and role: `spoke`. The peer list will be empty — pairing comes next.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 5. Pair with the Hub
|
|
99
|
+
|
|
100
|
+
### 5a. Generate a pairing token on the hub
|
|
101
|
+
|
|
102
|
+
On the **hub host**, an admin runs:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
flair federation token --admin-pass <hub-admin-pass> > pair-triple.json
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Output (a single JSON object):
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"token": "<one-time-pairing-token>",
|
|
113
|
+
"user": "pair-bootstrap-xxxxxxxx",
|
|
114
|
+
"password": "<bootstrap-password>",
|
|
115
|
+
"expiresAt": "<ISO-8601>"
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Tokens expire after **60 minutes** by default. Use `--ttl <minutes>` to extend.
|
|
120
|
+
|
|
121
|
+
### 5b. Transfer the triple to the spoke
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
scp hub-host:/path/to/pair-triple.json ./pair-triple.json
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 5c. Pair from the spoke
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
flair federation pair https://<hub-url> \
|
|
131
|
+
--token-from ./pair-triple.json \
|
|
132
|
+
--admin-pass "$FLAIR_ADMIN_PASS"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The `--admin-pass` is required so the CLI can write the hub as a local `Peer` record. Without it, pairing succeeds on the hub side but the spoke never records its peer, and `flair federation sync` reports "No hub peer configured."
|
|
136
|
+
|
|
137
|
+
### 5d. What pairing does
|
|
138
|
+
|
|
139
|
+
1. The spoke POSTs a signed pairing request to the hub's `/FederationPair` endpoint.
|
|
140
|
+
2. The bootstrap user authenticates at the Harper platform layer (works on standalone and Fabric deployments alike).
|
|
141
|
+
3. The hub validates the one-time token, verifies the Ed25519 signature, and creates a `Peer` record.
|
|
142
|
+
4. The spoke writes a `Peer` record pointing to the hub so sync knows where to push.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 6. Sync for the First Time
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
flair federation sync --admin-pass "$FLAIR_ADMIN_PASS"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Expected:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
Syncing to hub: <hub-instance-id>...
|
|
156
|
+
✅ Synced 0 records (0 skipped) in 45ms
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Then verify reachability across the federation:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
flair federation reachability
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Both local and hub peer should report `OK`.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## 7. Persist Sync (Production)
|
|
170
|
+
|
|
171
|
+
For the spoke to stay connected continuously, wrap `flair federation sync` in a loop.
|
|
172
|
+
|
|
173
|
+
### systemd timer (Linux)
|
|
174
|
+
|
|
175
|
+
Create `~/.config/systemd/user/flair-sync.service`:
|
|
176
|
+
|
|
177
|
+
```ini
|
|
178
|
+
[Unit]
|
|
179
|
+
Description=Flair federation sync (one-shot)
|
|
180
|
+
After=flair.service
|
|
181
|
+
|
|
182
|
+
[Service]
|
|
183
|
+
Type=oneshot
|
|
184
|
+
ExecStart=/usr/bin/env bash -c 'FLAIR_ADMIN_PASS="$(<~/.flair/admin-pass)" flair federation sync'
|
|
185
|
+
Environment=HOME=%h
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
and a timer at `~/.config/systemd/user/flair-sync.timer`:
|
|
189
|
+
|
|
190
|
+
```ini
|
|
191
|
+
[Unit]
|
|
192
|
+
Description=Flair federation sync timer
|
|
193
|
+
|
|
194
|
+
[Timer]
|
|
195
|
+
OnUnitActiveSec=30s
|
|
196
|
+
Persistent=true
|
|
197
|
+
|
|
198
|
+
[Install]
|
|
199
|
+
WantedBy=timers.target
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
systemctl --user daemon-reload
|
|
204
|
+
systemctl --user enable --now flair-sync.timer
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Built-in watch (foreground, for testing)
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
flair federation watch --interval 30
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Press Ctrl-C to stop.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## 8. Known Gotchas
|
|
218
|
+
|
|
219
|
+
### 🔸 bun pins the wrong llama.cpp platform binary
|
|
220
|
+
|
|
221
|
+
Harper's NAPI modules (`llama.cpp`-backed embeddings) are built for **Node.js**, not bun. The test harness explicitly spawns Harper under `node` because bun 1.3.x doesn't support `uv_ip6_addr` — Harper crashes with a NAPI resolve panic.
|
|
222
|
+
|
|
223
|
+
**Fix:** Always run Flair under Node.js. Set `NODE_HOSTNAME=127.0.0.1` (IPv4 only) in the Harper environment to bypass the IPv6 path entirely. The `flair init` command does this automatically.
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
# In Harper env (done automatically by flair init):
|
|
227
|
+
NODE_HOSTNAME=127.0.0.1
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### 🔸 Symlink not followed by Harper sandbox
|
|
231
|
+
|
|
232
|
+
The Harper v5 sandbox blocks `node:module` (used by `createRequire` for bridge resolution) but `process.dlopen` (native addon loading) works. Some bridge packages and the OpenClaw Flair plugin reference workspace files via symlinks — Harper's sandbox won't follow them, and the plugin explicitly skips symlink escapes with a warning.
|
|
233
|
+
|
|
234
|
+
**Fix:** Keep workspace files and Flair data on the same filesystem without symlinks crossing mount boundaries. If you see "skipping anchor symlink escape" warnings in the plugin logs, copy the file instead of symlinking.
|
|
235
|
+
|
|
236
|
+
### 🔸 Old data-dir `lockdown:freeze` jails
|
|
237
|
+
|
|
238
|
+
If Harper was previously installed with `lockdown:true` in `harper-config.yaml` or if the data dir contains a stale `hdb_boot_properties.file` from an unrelated Harper install, the install step crashes in Harper v5 beta.6+ (`checkForExistingInstall` queries the database before environment init).
|
|
239
|
+
|
|
240
|
+
**Fix:** Start with a clean data dir. If reusing an existing dir, ensure no `~/.harperdb/hdb_boot_properties.file` exists and that the data dir's `harper-config.yaml` doesn't set `lockdown: true`.
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
# Clean up old Harper state before init:
|
|
244
|
+
rm -f ~/.harperdb/hdb_boot_properties.file
|
|
245
|
+
# If the data dir has lock-related entries, nuke it and re-init:
|
|
246
|
+
rm -rf /data/flair
|
|
247
|
+
flair init --data-dir /data/flair --agent-id <id>
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### 🔸 Legacy hub on the old default (9926) vs a fresh spoke (19926)
|
|
251
|
+
|
|
252
|
+
Flair's default REST port changed from 9926 to 19926 (ops: 9925 → 19925) to avoid Harper port collisions (see CHANGELOG.md). `flair init` today always resolves and persists the CLI's real `DEFAULT_PORT` — **19926** — to `~/.flair/config.yaml`, and that's what a fresh spoke gets. But a hub that was provisioned before that bump may still be running on the old 9926/9925 pair — nothing auto-migrates an existing instance's config. Don't assume the hub is on the current default; confirm its actual port with the hub admin, or check `~/.flair/config.yaml` on the hub host.
|
|
253
|
+
|
|
254
|
+
**Fix:** Pass the hub's actual URL (with its real port) to `flair federation pair` — never assume 19926. On the spoke side, confirm your own config matches what `flair init` actually wrote:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
# Verify the spoke's own config:
|
|
258
|
+
grep port ~/.flair/config.yaml
|
|
259
|
+
# → port: 19926 (fresh install, or whatever you passed via --port)
|
|
260
|
+
|
|
261
|
+
# Or set explicitly if pairing to a hub still on the legacy default:
|
|
262
|
+
export FLAIR_URL=http://127.0.0.1:9926
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### 🔸 Fabric hosts split REST and ops ports
|
|
266
|
+
|
|
267
|
+
Fabric deployments split the REST API and the Harper operations API onto adjacent ports. The derivation rule is **ops = REST − 1**. When pointing a CLI at a remote Fabric instance, pass the REST URL and the ops URL is derived automatically — substitute the hub's actual REST port (19926 for a fresh deployment, or whatever it was provisioned with):
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
flair federation pair https://fabric-node.example.com:19926/<instance> --token-from triple.json
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
For explicit ops control:
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
flair federation pair https://fabric-node.example.com:19926/<instance> \
|
|
277
|
+
--token-from triple.json \
|
|
278
|
+
--ops-target https://fabric-node.example.com:19925
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## 9. Quick Reference
|
|
284
|
+
|
|
285
|
+
| Step | Command |
|
|
286
|
+
|------|---------|
|
|
287
|
+
| Install | `npm install -g @tpsdev-ai/flair` |
|
|
288
|
+
| Init spoke | `flair init --agent-id <id> --data-dir /data/flair --skip-soul` |
|
|
289
|
+
| Check status | `flair status` / `flair federation status` |
|
|
290
|
+
| Hub: mint token | `flair federation token --admin-pass <pass> > triple.json` |
|
|
291
|
+
| Spoke: pair | `flair federation pair <hub-url> --token-from ./triple.json --admin-pass <pass>` |
|
|
292
|
+
| Spoke: sync | `flair federation sync --admin-pass <pass>` |
|
|
293
|
+
| Spoke: verify | `flair federation reachability` |
|
|
294
|
+
| Watch loop | `flair federation watch --interval 30` |
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## See Also
|
|
299
|
+
|
|
300
|
+
- [federation.md](federation.md) — hub-and-spoke architecture, security model, CLI reference
|
|
301
|
+
- [deployment.md](deployment.md) — systemd/launchd setup, backup/restore
|
|
302
|
+
- [system-requirements.md](system-requirements.md) — RAM, disk, and scaling expectations
|
|
303
|
+
- [troubleshooting.md](troubleshooting.md) — common failure modes and fixes
|