@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
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Flair + Claude Code
|
|
2
|
+
|
|
3
|
+
Give Claude Code persistent memory across sessions. Works with subagents too.
|
|
4
|
+
|
|
5
|
+
## Setup (5 minutes)
|
|
6
|
+
|
|
7
|
+
### 1. Install Flair
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @tpsdev-ai/flair
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### 2. Initialize
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
flair init
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This starts a local Flair server (Harper) and creates `~/.flair/`.
|
|
20
|
+
|
|
21
|
+
### 3. Create an agent identity
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# One agent per project, or one shared agent — your call
|
|
25
|
+
flair agent add my-project
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
This generates an Ed25519 key pair at `~/.flair/keys/my-project.key` and registers the agent with Flair.
|
|
29
|
+
|
|
30
|
+
### 4. Add to your project's CLAUDE.md
|
|
31
|
+
|
|
32
|
+
Copy this into your project's `CLAUDE.md` (or `.claude/settings.md`, `AGENTS.md`, etc.):
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
> **Start of CLAUDE.md snippet** — copy everything between the lines.
|
|
37
|
+
|
|
38
|
+
## Memory
|
|
39
|
+
|
|
40
|
+
You have persistent memory via Flair. Use it to remember context across sessions.
|
|
41
|
+
|
|
42
|
+
### On session start
|
|
43
|
+
|
|
44
|
+
Run this FIRST, before doing anything else:
|
|
45
|
+
|
|
46
|
+
flair bootstrap --agent my-project --max-tokens 4000
|
|
47
|
+
|
|
48
|
+
Read the output — that's your soul and recent memories.
|
|
49
|
+
|
|
50
|
+
### During work
|
|
51
|
+
|
|
52
|
+
- Remember something: `flair memory add --agent my-project --content "what you learned"`
|
|
53
|
+
- Search memory: `flair search "your query" --agent my-project`
|
|
54
|
+
- Store a lesson: `flair memory add --agent my-project --content "lesson text" --type lesson --durability persistent`
|
|
55
|
+
- Store a decision: `flair memory add --agent my-project --content "decision text" --type decision --durability persistent`
|
|
56
|
+
|
|
57
|
+
### What to remember
|
|
58
|
+
|
|
59
|
+
- Lessons learned (bugs, workarounds, patterns)
|
|
60
|
+
- Decisions made (why we chose X over Y)
|
|
61
|
+
- Project-specific context (architecture, conventions, constraints)
|
|
62
|
+
- User preferences (coding style, review standards)
|
|
63
|
+
|
|
64
|
+
### What NOT to remember
|
|
65
|
+
|
|
66
|
+
- Transient task details (what file am I editing right now)
|
|
67
|
+
- Things already in the codebase (read the code instead)
|
|
68
|
+
- Secrets or credentials (never store these in memory)
|
|
69
|
+
|
|
70
|
+
### Durability levels
|
|
71
|
+
|
|
72
|
+
- persistent — survives indefinitely. Use for lessons, decisions, preferences.
|
|
73
|
+
- standard — default. Good for session context, observations.
|
|
74
|
+
- ephemeral — auto-expires after 24h. Use for temporary notes.
|
|
75
|
+
|
|
76
|
+
> **End of CLAUDE.md snippet.**
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
That's it. Claude Code will now bootstrap context on start and store important things as it works.
|
|
81
|
+
|
|
82
|
+
## Multiple Projects
|
|
83
|
+
|
|
84
|
+
Create a separate agent per project:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
flair agent add project-alpha
|
|
88
|
+
flair agent add project-beta
|
|
89
|
+
flair agent add infra-ops
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Each project's `CLAUDE.md` uses its own agent ID. Memories are fully isolated between projects.
|
|
93
|
+
|
|
94
|
+
## Subagents
|
|
95
|
+
|
|
96
|
+
Claude Code subagents (spawned via `/run` or background tasks) can share the parent's memory:
|
|
97
|
+
|
|
98
|
+
### Subagents
|
|
99
|
+
Subagents share memory with the parent session. Use the same agent ID:
|
|
100
|
+
FLAIR_AGENT_ID=my-project
|
|
101
|
+
|
|
102
|
+
When spawning subagents, pass the agent ID so they can access shared context.
|
|
103
|
+
|
|
104
|
+
Or give subagents their own identity for isolation:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
flair agent add my-project-review # code review subagent
|
|
108
|
+
flair agent add my-project-test # test runner subagent
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Environment Variables
|
|
112
|
+
|
|
113
|
+
Instead of passing `--agent` every time, set environment variables:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# In your shell profile or .envrc
|
|
117
|
+
export FLAIR_AGENT_ID=my-project
|
|
118
|
+
export FLAIR_URL=http://localhost:19926 # default, only needed if custom
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Then the CLAUDE.md simplifies to:
|
|
122
|
+
|
|
123
|
+
## Memory
|
|
124
|
+
- Bootstrap: `flair bootstrap`
|
|
125
|
+
- Remember: `flair memory add --content "what you learned"`
|
|
126
|
+
- Search: `flair search "your query"`
|
|
127
|
+
|
|
128
|
+
## Soul (Personality / Context)
|
|
129
|
+
|
|
130
|
+
Want Claude Code to have consistent personality or project context? Set soul entries:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Project context
|
|
134
|
+
flair soul set --agent my-project --key project \
|
|
135
|
+
--value "E-commerce platform. Rust backend, React frontend. Ship quality over speed."
|
|
136
|
+
|
|
137
|
+
# Coding standards
|
|
138
|
+
flair soul set --agent my-project --key standards \
|
|
139
|
+
--value "Always write tests. Prefer composition over inheritance. No any types in TypeScript."
|
|
140
|
+
|
|
141
|
+
# Review guidelines
|
|
142
|
+
flair soul set --agent my-project --key review \
|
|
143
|
+
--value "Check for: error handling, edge cases, performance implications, security."
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Soul entries are included in every `flair bootstrap` — they're the persistent context that shapes how Claude Code thinks about your project.
|
|
147
|
+
|
|
148
|
+
## Remote Flair
|
|
149
|
+
|
|
150
|
+
If you want to share memory across machines (e.g., work laptop + home setup):
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# On your server
|
|
154
|
+
npm install -g @tpsdev-ai/flair
|
|
155
|
+
flair init
|
|
156
|
+
flair agent add my-project
|
|
157
|
+
|
|
158
|
+
# On client machines
|
|
159
|
+
npm install -g @tpsdev-ai/flair # for the CLI
|
|
160
|
+
export FLAIR_URL=http://your-server:19926
|
|
161
|
+
export FLAIR_AGENT_ID=my-project
|
|
162
|
+
# Copy the key from the server:
|
|
163
|
+
scp server:~/.flair/keys/my-project.key ~/.flair/keys/
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Or use an SSH tunnel:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
ssh -f -N -L 19926:localhost:19926 your-server
|
|
170
|
+
# Now FLAIR_URL=http://localhost:19926 works
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Programmatic Access
|
|
174
|
+
|
|
175
|
+
For custom tooling, use the lightweight client library:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
npm install @tpsdev-ai/flair-client
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
import { FlairClient } from '@tpsdev-ai/flair-client'
|
|
183
|
+
|
|
184
|
+
const flair = new FlairClient({ agentId: 'my-project' })
|
|
185
|
+
|
|
186
|
+
await flair.memory.write('learned that X causes Y', {
|
|
187
|
+
type: 'lesson',
|
|
188
|
+
durability: 'persistent',
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
const results = await flair.memory.search('what causes Y')
|
|
192
|
+
const context = await flair.bootstrap({ maxTokens: 4000 })
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Tips
|
|
196
|
+
|
|
197
|
+
- **Bootstrap is cheap.** Run it at the start of every session. It's one HTTP call.
|
|
198
|
+
- **Write lessons immediately.** Don't wait for the session to end — you might not get the chance.
|
|
199
|
+
- **Use durability wisely.** Most things are `standard`. Only promote to `persistent` for things that should survive months.
|
|
200
|
+
- **Search is semantic.** "deployment issues" finds memories about "CI pipeline failures" — you don't need exact keywords.
|
|
201
|
+
- **Temporal queries work.** "What happened today" and "what did we ship recently" are understood.
|
|
202
|
+
- **Dedup is automatic.** Writing the same fact twice won't create duplicates (0.7 similarity threshold).
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Deployment Guide
|
|
2
|
+
|
|
3
|
+
Run Flair on macOS, Linux, or Docker.
|
|
4
|
+
|
|
5
|
+
## macOS (Apple Silicon)
|
|
6
|
+
|
|
7
|
+
### Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @tpsdev-ai/flair
|
|
11
|
+
flair init
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`flair init` will:
|
|
15
|
+
- Download Harper and the nomic-embed-text embedding model
|
|
16
|
+
- Create `~/.flair/` (config, data, keys)
|
|
17
|
+
- Generate admin credentials
|
|
18
|
+
- Install a launchd plist for auto-start on boot
|
|
19
|
+
- Start the server
|
|
20
|
+
|
|
21
|
+
### Verify
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
flair status
|
|
25
|
+
flair doctor
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Auto-start
|
|
29
|
+
|
|
30
|
+
`flair init` installs a launchd plist at `~/Library/LaunchAgents/ai.tpsdev.flair.plist`. Flair starts automatically on login and restarts if it crashes.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Manual control
|
|
34
|
+
flair stop
|
|
35
|
+
flair start
|
|
36
|
+
flair restart
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Port
|
|
40
|
+
|
|
41
|
+
Default port is `19926`. Override during init:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
flair init --port 8000
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or edit `~/.flair/config.yaml` and restart.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Linux
|
|
52
|
+
|
|
53
|
+
### Prerequisites
|
|
54
|
+
|
|
55
|
+
- Node.js >= 22
|
|
56
|
+
- systemd (for auto-start)
|
|
57
|
+
|
|
58
|
+
### Install
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npm install -g @tpsdev-ai/flair
|
|
62
|
+
flair init
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Same as macOS — detects the platform and generates a systemd unit file instead of a launchd plist.
|
|
66
|
+
|
|
67
|
+
### Verify
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
flair status
|
|
71
|
+
flair doctor
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Auto-start
|
|
75
|
+
|
|
76
|
+
The systemd unit file is installed at `~/.config/systemd/user/flair.service`.
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Manual control
|
|
80
|
+
systemctl --user start flair
|
|
81
|
+
systemctl --user stop flair
|
|
82
|
+
systemctl --user restart flair
|
|
83
|
+
|
|
84
|
+
# View logs
|
|
85
|
+
journalctl --user -u flair -f
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Docker
|
|
91
|
+
|
|
92
|
+
### Quick test (from-scratch validation)
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
cd docker/
|
|
96
|
+
./test-from-scratch.sh
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
This runs a clean install in a container — useful for verifying the install path works on a fresh machine.
|
|
100
|
+
|
|
101
|
+
### Production Docker
|
|
102
|
+
|
|
103
|
+
```dockerfile
|
|
104
|
+
FROM node:22-slim
|
|
105
|
+
RUN npm install -g @tpsdev-ai/flair
|
|
106
|
+
RUN flair init --skip-soul
|
|
107
|
+
EXPOSE 19926
|
|
108
|
+
CMD ["flair", "start", "--foreground"]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Note: embeddings run on CPU in Docker (no Metal acceleration). Performance is acceptable for small-to-medium memory stores (< 10K memories).
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Harper Fabric
|
|
116
|
+
|
|
117
|
+
Deploying to a Harper Fabric cluster is a different mechanism from the installs above — `flair deploy` pushes Flair as a cluster component instead of `npm install -g`. To upgrade an already-deployed Fabric instance in place, use `FABRIC_USER=<admin> FABRIC_PASSWORD=<pass> flair upgrade --target <fabric-url>` (or `--fabric-password-file <path>` in place of the env var), not the local upgrade path. Inline `--fabric-user`/`--fabric-password` flags also work but are discouraged — both leak to shell history and `ps`. See [`docs/upgrade.md` — Upgrading a Fabric-deployed instance](upgrade.md#upgrading-a-fabric-deployed-instance) for the full walkthrough, including the automatic post-deploy fleet-convergence sweep.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Remote Access
|
|
122
|
+
|
|
123
|
+
### SSH tunnel (simplest)
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
ssh -f -N -L 19926:localhost:19926 your-server
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Then set `FLAIR_URL=http://localhost:19926` on the client.
|
|
130
|
+
|
|
131
|
+
### Direct network access
|
|
132
|
+
|
|
133
|
+
Edit `~/.flair/config.yaml`:
|
|
134
|
+
|
|
135
|
+
```yaml
|
|
136
|
+
http:
|
|
137
|
+
port: 19926
|
|
138
|
+
host: 0.0.0.0 # listen on all interfaces
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Security:** Flair uses Ed25519 authentication. Agents must present a valid signature to read or write. However, the `/Health` endpoint is unauthenticated. For internet-facing deployments, put Flair behind a reverse proxy with TLS.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Configuration
|
|
146
|
+
|
|
147
|
+
All configuration lives in `~/.flair/`:
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
~/.flair/
|
|
151
|
+
├── config.yaml # port, host, embedding model
|
|
152
|
+
├── data/ # Harper database
|
|
153
|
+
├── keys/ # Ed25519 keypairs per agent
|
|
154
|
+
└── backups/ # flair backup output
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Key config options (`~/.flair/config.yaml`)
|
|
158
|
+
|
|
159
|
+
```yaml
|
|
160
|
+
http:
|
|
161
|
+
port: 19926 # API port (ops port = this - 1)
|
|
162
|
+
host: 127.0.0.1 # bind address
|
|
163
|
+
|
|
164
|
+
clustering:
|
|
165
|
+
nodeName: flair
|
|
166
|
+
|
|
167
|
+
logging:
|
|
168
|
+
level: warn
|
|
169
|
+
stdStreams: true
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Environment variables
|
|
173
|
+
|
|
174
|
+
Set these in the Flair process environment (`~/Library/LaunchAgents/ai.tpsdev.flair.plist` on macOS, the systemd unit on Linux, the component env on Fabric).
|
|
175
|
+
|
|
176
|
+
| Variable | What it does | When to set it |
|
|
177
|
+
|----------|--------------|----------------|
|
|
178
|
+
| `FLAIR_PUBLIC_URL` | The URL operators reach this Flair on (e.g. `https://flair.example.com`). Surfaced in the AdminInstance pane's Endpoints table and used by OAuth metadata + A2A discovery so external clients see a reachable URL. | **Always set on remote / Fabric / VPS deployments.** Local-only installs can leave it unset. |
|
|
179
|
+
| `HDB_ADMIN_PASSWORD` | Bootstrap password for the embedded Harper. After first start, the persisted user record is the source of truth; rotate via the Harper ops API, not by changing this env var. | Set at install time. See [secrets-and-keys.md](secrets-and-keys.md) for rotation. |
|
|
180
|
+
| `FLAIR_KEY_PASSPHRASE` | Passphrase used to derive the AES-256-GCM key that wraps federation private-key seeds at rest. Auto-generated to `~/.flair/keys/.passphrase` if unset. | Set explicitly for production federation deployments so the passphrase isn't auto-generated and lost on disk wipe. |
|
|
181
|
+
| `HTTP_PORT` | Override the Harper HTTP port. Useful for sandboxes; production deployments should configure the port in `config.yaml` instead. | Rare. |
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Backup & Restore
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
# Backup all data (agents, memories, souls)
|
|
189
|
+
flair backup > ~/flair-backup-$(date +%Y%m%d).json
|
|
190
|
+
|
|
191
|
+
# Restore to a fresh instance
|
|
192
|
+
flair restore < ~/flair-backup-20260405.json
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Always backup before upgrades.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Uninstall
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
flair uninstall # stops server, removes ~/.flair/, removes launchd/systemd service
|
|
203
|
+
npm uninstall -g @tpsdev-ai/flair
|
|
204
|
+
```
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Entity Vocabulary
|
|
2
|
+
|
|
3
|
+
The **attention plane** — an agent seeing what its teammates are actively touching, and
|
|
4
|
+
where its own work collides with theirs — needs one thing to work at all: a single,
|
|
5
|
+
consistent way to name the "things" (repos, issues, customers, subsystems, people, agents)
|
|
6
|
+
that memories, workspace state, org events, and relationships all reference. Without it,
|
|
7
|
+
"what touches entity E" can never be an indexed lookup — it degenerates into fuzzy string
|
|
8
|
+
matching across free-text fields.
|
|
9
|
+
|
|
10
|
+
This document is that convention. It's written down and enforced by a small validator
|
|
11
|
+
(`resources/entity-vocab.ts`) **before** anything is built on top of it, because three
|
|
12
|
+
surfaces already consume entity strings today (the `Relationship` graph's `subject`/`object`,
|
|
13
|
+
and the new `entities` fields on `WorkspaceState`/`OrgEvent`/`Memory`), plus a future
|
|
14
|
+
attention query and any future MCP scope field — retrofitting a divergent vocabulary later
|
|
15
|
+
is expensive.
|
|
16
|
+
|
|
17
|
+
> This doc covers the vocabulary and validator only — the foundation slice of the attention
|
|
18
|
+
> plane (flair#675). The attention query ("what touches entity E in the last N days") and
|
|
19
|
+
> bootstrap collision surfacing are separate, later slices. See `FLAIR-ATTENTION-PLANE.md`
|
|
20
|
+
> for the full design.
|
|
21
|
+
|
|
22
|
+
## The form
|
|
23
|
+
|
|
24
|
+
An entity is a single string: `type:value`.
|
|
25
|
+
|
|
26
|
+
- **type** — lowercase, drawn from a **closed** set (below). Not user-extensible at write
|
|
27
|
+
time; new types are added deliberately, by changing `ENTITY_TYPES` in
|
|
28
|
+
`resources/entity-vocab.ts` (and this doc) in the same change.
|
|
29
|
+
- **value** — a stable identifier whose grammar depends on the type (see table).
|
|
30
|
+
|
|
31
|
+
Matching is **exact on the full string** — `repo:tpsdev-ai/flair` and
|
|
32
|
+
`repo:tpsdev-ai/flair-mcp` are unrelated entities, not prefix-related. This is what makes the
|
|
33
|
+
indexed `entities: [String] @indexed` fields a plain equality lookup rather than a scan.
|
|
34
|
+
|
|
35
|
+
## The closed type set
|
|
36
|
+
|
|
37
|
+
| Type | Form | Example | Value grammar |
|
|
38
|
+
|------|------|---------|---------------|
|
|
39
|
+
| `repo` | `repo:<owner>/<name>` | `repo:tpsdev-ai/flair` | Two lowercase path segments (alnum + `.`/`-`/`_` internal separators) joined by exactly one `/`. |
|
|
40
|
+
| `issue` | `issue:<repo>#<n>` | `issue:tpsdev-ai/flair#504` | A valid `repo` value, then `#`, then a positive integer with no leading zero. |
|
|
41
|
+
| `customer` | `customer:<slug>` | `customer:acme` | A lowercase slug (alnum segments joined by single `-`/`_`). |
|
|
42
|
+
| `subsystem` | `subsystem:<slug>` | `subsystem:embeddings` | Same slug grammar as `customer`. |
|
|
43
|
+
| `agent` | `agent:<agentId>` | `agent:flint` | Same slug grammar — the agent's registered id. |
|
|
44
|
+
| `person` | `person:<id>` | `person:nathan` | Same slug grammar. |
|
|
45
|
+
|
|
46
|
+
Rules that apply across every type:
|
|
47
|
+
|
|
48
|
+
- The type prefix is **always lowercase** — `Repo:x`, `CUSTOMER:x` etc. are invalid (not a
|
|
49
|
+
case-insensitive match against the closed set — they simply aren't in it).
|
|
50
|
+
- No leading/trailing whitespace anywhere in the string.
|
|
51
|
+
- No empty type or empty value (`:acme`, `repo:`, `repo` with no colon are all invalid).
|
|
52
|
+
- Slugs never have leading/trailing/doubled separators (`-embeddings`, `embeddings-`,
|
|
53
|
+
`embed--dings` are all invalid).
|
|
54
|
+
|
|
55
|
+
## The validator
|
|
56
|
+
|
|
57
|
+
`resources/entity-vocab.ts` is the single source of truth — every write path that persists
|
|
58
|
+
an `entities` value validates against it, not a local reimplementation of the grammar.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { isValidEntity, validateEntities, invalidEntitiesResponse } from "./entity-vocab.js";
|
|
62
|
+
|
|
63
|
+
isValidEntity("repo:tpsdev-ai/flair"); // true
|
|
64
|
+
isValidEntity("project:flair"); // false — "project" isn't in the closed type set
|
|
65
|
+
isValidEntity("Customer:Acme"); // false — uppercase type and value both reject
|
|
66
|
+
|
|
67
|
+
validateEntities(["repo:tpsdev-ai/flair", "bogus"]);
|
|
68
|
+
// => { valid: false, invalid: ["bogus"] }
|
|
69
|
+
|
|
70
|
+
validateEntities(undefined);
|
|
71
|
+
// => { valid: true, invalid: [] } — the field is additive/optional; absence is not an error
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`invalidEntitiesResponse(entities)` is a thin convenience wrapper for Harper resource write
|
|
75
|
+
paths: it returns a ready-to-return `400 { error: "invalid_entities", invalid: [...] }`
|
|
76
|
+
`Response` when validation fails, or `null` when the field is absent or every entry is valid.
|
|
77
|
+
`WorkspaceState.ts`, `OrgEvent.ts`, and `Memory.ts` all call it early in `post()`/`put()`.
|
|
78
|
+
|
|
79
|
+
## Where `entities` lives today
|
|
80
|
+
|
|
81
|
+
Per the attention-plane spec's K&S-approved refinements, `entities: [String] @indexed` is an
|
|
82
|
+
**additive, nullable** field on:
|
|
83
|
+
|
|
84
|
+
- `WorkspaceState` (`schemas/workspace.graphql`)
|
|
85
|
+
- `OrgEvent` (`schemas/event.graphql`)
|
|
86
|
+
- `Memory` (`schemas/memory.graphql`) — added in v1 (not deferred to v2) for index-pushdown
|
|
87
|
+
uniformity across all three sources the future attention query joins.
|
|
88
|
+
|
|
89
|
+
Existing rows on all three tables simply carry no `entities` — readers must tolerate absence,
|
|
90
|
+
the same pattern already used for `Presence.activityUpdatedAt`. No migration, no backfill.
|
|
91
|
+
|
|
92
|
+
`Relationship` gets **no** `entities` field: its `subject`/`object` columns already carry
|
|
93
|
+
free-form entity-reference strings and are already indexed — they're the vocabulary carrier
|
|
94
|
+
for that table. They are lowercased on write today but not yet validated against this
|
|
95
|
+
vocabulary; wiring that validation is a follow-up, not part of this foundation slice.
|
|
96
|
+
|
|
97
|
+
## What's explicitly NOT in this slice
|
|
98
|
+
|
|
99
|
+
- The attention query (`AttentionQuery` / `flair attention <entity>`) that joins Memory,
|
|
100
|
+
Relationship, WorkspaceState, Presence, and OrgEvent by entity — shipped in flair#678
|
|
101
|
+
(`resources/AttentionQuery.ts`), a later slice.
|
|
102
|
+
- Bootstrap collision surfacing ("others in the room") — shipped in flair#681
|
|
103
|
+
(`resources/collision-lib.ts` + `MemoryBootstrap.ts`'s "Others in the room" section), a
|
|
104
|
+
later slice.
|
|
105
|
+
- Automatic entity extraction/tagging on write (producers set `entities` themselves today,
|
|
106
|
+
where they choose to; there's no NLP-derived auto-population yet).
|
|
107
|
+
- Validating `Relationship.subject`/`object` against this vocabulary.
|
|
108
|
+
|
|
109
|
+
These are tracked as follow-ups in `FLAIR-ATTENTION-PLANE.md`.
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Federation
|
|
2
|
+
|
|
3
|
+
Hub-and-spoke sync between Flair instances. A hub instance coordinates sync for one or more spoke instances.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Federation lets multiple Flair instances share memories, relationships, and agent records. Each instance maintains its own Ed25519 identity. Sync requests are signed and verified against pinned peer public keys.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Spoke A ──[signed sync]──▶ Hub ◀──[signed sync]── Spoke B
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- **Hub:** accepts sync pushes from paired spokes, can relay records between peers
|
|
14
|
+
- **Spoke:** pushes local changes to the hub, receives changes from other spokes via the hub
|
|
15
|
+
|
|
16
|
+
## Pairing a New Spoke (Bootstrap-User Flow)
|
|
17
|
+
|
|
18
|
+
Pairing connects a spoke to a hub with mutual key pinning and an auth-aware handshake that works across all Harper topologies, including Harper Fabric.
|
|
19
|
+
|
|
20
|
+
### Step-by-step
|
|
21
|
+
|
|
22
|
+
**1. Hub admin generates a pairing token triple**
|
|
23
|
+
|
|
24
|
+
On the hub machine, the admin runs `flair federation token`. The command emits a JSON triple containing a one-time bootstrap credential:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
flair federation token --admin-pass <hub-admin-password>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Output (a single JSON object):
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{"token":"<one-time-pairing-token>","user":"<bootstrap-username>","password":"<bootstrap-password>","expiresAt":"<ISO-8601-timestamp>"}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Tokens expire after 60 minutes by default. Use `--ttl <minutes>` to adjust.
|
|
37
|
+
|
|
38
|
+
**2. Hub admin shares the triple with the spoke admin**
|
|
39
|
+
|
|
40
|
+
The JSON triple is shared out-of-band (secure file transfer, password manager, or similar). It must be stored as a plain JSON file on the spoke side or piped via stdin.
|
|
41
|
+
|
|
42
|
+
**3. Spoke admin runs the pair command**
|
|
43
|
+
|
|
44
|
+
On the spoke machine:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# From a file
|
|
48
|
+
flair federation pair <hub-url> --token-from /path/to/triple.json
|
|
49
|
+
|
|
50
|
+
# From stdin
|
|
51
|
+
cat triple.json | flair federation pair <hub-url> --token-from -
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**4. Behind the scenes**
|
|
55
|
+
|
|
56
|
+
- The bootstrap user authenticates at the platform layer (works on standalone deployments and Harper Fabric alike).
|
|
57
|
+
- The resource handler validates the pairing token, the signed request body, and the binding between the bootstrap user and the token.
|
|
58
|
+
- On success the hub creates a `Peer` record for the spoke and removes the temporary bootstrap user. The spoke records the hub as its peer.
|
|
59
|
+
|
|
60
|
+
After pairing, both instances pin each other's Ed25519 public keys and are ready to sync.
|
|
61
|
+
|
|
62
|
+
## Why the Bootstrap-User Flow?
|
|
63
|
+
|
|
64
|
+
Earlier designs relied on `allowCreate=true` combined with body-only authentication. That approach works on single-component deployments but breaks on Harper Fabric, where the platform authentication gate fires before the resource handler sees the request. The bootstrap-user flow (Option B) makes the pair handshake auth-aware so it operates correctly on all Harper topologies: standalone, Fabric single-node, and Fabric multi-node.
|
|
65
|
+
|
|
66
|
+
## Fabric Pairing Example
|
|
67
|
+
|
|
68
|
+
When the hub runs on Harper Fabric, adapt the hub URL to the Fabric pattern:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# 1. Hub admin generates the triple (on the Fabric host)
|
|
72
|
+
ssh hub-host
|
|
73
|
+
flair federation token --admin-pass <hub-admin-password> > /tmp/pair-triple.json
|
|
74
|
+
|
|
75
|
+
# 2. Transfer the triple to the spoke admin (out-of-band)
|
|
76
|
+
scp hub-host:/tmp/pair-triple.json ./pair-triple.json
|
|
77
|
+
|
|
78
|
+
# 3. Spoke admin pairs using the Fabric URL
|
|
79
|
+
flair federation pair https://<fabric-node>:19926/<instance-name> --token-from ./pair-triple.json
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Replace `<fabric-node>`, `<instance-name>`, and `<hub-admin-password>` with your actual values.
|
|
83
|
+
|
|
84
|
+
## Sync
|
|
85
|
+
|
|
86
|
+
Push local changes to the hub:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
flair federation sync --admin-pass <password>
|
|
90
|
+
# Output: ✅ Synced 12 records (0 skipped) in 145ms
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Security
|
|
94
|
+
|
|
95
|
+
### Signed requests
|
|
96
|
+
|
|
97
|
+
Every federation request (pair, sync) is signed with the sender's Ed25519 private key. The receiver verifies the signature against the peer's pinned public key. Unsigned or tampered requests are rejected with 401.
|
|
98
|
+
|
|
99
|
+
The signature covers the canonical JSON of the request body (keys sorted recursively, signature field excluded).
|
|
100
|
+
|
|
101
|
+
### Encrypted key storage
|
|
102
|
+
|
|
103
|
+
Private key seeds are stored in `~/.flair/keys/<instanceId>.key`, encrypted with AES-256-GCM. The encryption key is derived via HKDF from:
|
|
104
|
+
|
|
105
|
+
1. `FLAIR_KEY_PASSPHRASE` environment variable (recommended for production), or
|
|
106
|
+
2. An auto-generated random passphrase at `~/.flair/keys/.passphrase` (mode 0600)
|
|
107
|
+
|
|
108
|
+
If neither the env var nor the passphrase file can be accessed, federation identity creation fails. Private keys are never stored in the database.
|
|
109
|
+
|
|
110
|
+
### Pairing tokens
|
|
111
|
+
|
|
112
|
+
New peers must present a valid, unexpired, unused pairing token. This prevents unauthorized instances from joining the federation. Tokens are generated by the hub admin and are single-use.
|
|
113
|
+
|
|
114
|
+
Re-pairing an existing peer (same instance ID, same public key) does not require a token.
|
|
115
|
+
|
|
116
|
+
### Originator enforcement
|
|
117
|
+
|
|
118
|
+
Spoke instances can only push records they originated. A spoke cannot overwrite records from another spoke or from the hub. The hub can relay records from any origin.
|
|
119
|
+
|
|
120
|
+
### Timestamp ceiling
|
|
121
|
+
|
|
122
|
+
Records with `updatedAt` more than 5 minutes in the future are rejected. This prevents an attacker from using far-future timestamps to permanently win last-write-wins (LWW) merge conflicts.
|
|
123
|
+
|
|
124
|
+
## CLI Reference
|
|
125
|
+
|
|
126
|
+
| Command | Description |
|
|
127
|
+
|---------|-------------|
|
|
128
|
+
| `flair federation status` | Show instance identity and peer connections |
|
|
129
|
+
| `flair federation pair <hub-url> --token-from <file>` | Pair this spoke with a hub using a token triple file (or `-` for stdin) |
|
|
130
|
+
| `flair federation sync` | Push local changes to the hub (one-shot) |
|
|
131
|
+
| `flair federation watch [--interval <s>]` | Run sync in a foreground daemon loop (default 30s) |
|
|
132
|
+
| `flair federation reachability` | Probe local instance + each paired peer (read-only) |
|
|
133
|
+
| `flair federation token [--ttl <min>]` | Generate a one-time pairing token triple (hub only) |
|
|
134
|
+
|
|
135
|
+
## Conflict Resolution
|
|
136
|
+
|
|
137
|
+
Federation uses record-level last-write-wins (LWW) with ISO timestamp comparison. When two instances modify the same record, the one with the later `updatedAt` wins. Field-level LWW is planned for a future version.
|
|
138
|
+
|
|
139
|
+
## Troubleshooting
|
|
140
|
+
|
|
141
|
+
### config.yaml port drift
|
|
142
|
+
|
|
143
|
+
If the hub's configured port in `config.yaml` differs from the port the spoke is targeting, federation requests fail with a connection error. Verify the port matches between the spoke's pair URL and the hub's `config.yaml` (`federation.port` or the instance's listen port).
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
# On the hub, confirm the listening port
|
|
147
|
+
grep -E 'port|federation' ~/.flair/config.yaml
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Local FederationInstance fetch needs auth
|
|
151
|
+
|
|
152
|
+
When troubleshooting on the hub, fetching `/federation/instances/<id>` locally (e.g. via `curl localhost`) may return a 401 if the request does not carry the authentication headers the platform layer expects. On Fabric this gate is enforced even on localhost. Use the CLI tooling (`flair federation status`) instead of raw HTTP calls for local inspection.
|
|
153
|
+
|
|
154
|
+
### `flair_pair_initiator` role not found on hub
|
|
155
|
+
|
|
156
|
+
If pairing fails with a role-not-found error, the hub instance may be missing the `flair_pair_initiator` role. This role is created automatically during `flair init --remote` but can be lost if the database was reset or migrated manually. Re-run `flair init --remote` on the hub to restore default roles, then retry pairing.
|
|
157
|
+
|
|
158
|
+
### Bootstrap user not deleted on Fabric
|
|
159
|
+
|
|
160
|
+
After a successful pairing, the temporary bootstrap user is automatically deleted. If it persists on a Fabric deployment, check that the hub's Harper operations log does not show a rollback or permission error during the cleanup step. Manually removing the stale bootstrap user via the Harper Studio is safe if needed — it is never used after pairing completes.
|
|
161
|
+
|
|
162
|
+
### Stale Peer record on spoke
|
|
163
|
+
|
|
164
|
+
If a spoke was previously paired with a different hub (or the hub's identity key changed), the spoke may retain a stale `Peer` record pointing to the old hub. Remove the stale record before pairing with the new hub:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
# Show current peers
|
|
168
|
+
flair federation status
|
|
169
|
+
|
|
170
|
+
# Remove a specific peer (replace <instanceId>)
|
|
171
|
+
flair federation unpin <instanceId>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Limitations (1.0)
|
|
175
|
+
|
|
176
|
+
- **HTTP push only** — no persistent WebSocket connections or real-time sync
|
|
177
|
+
- **Manual or watch-loop sync** — `flair federation sync` is one-shot; `flair federation watch --interval 30` runs it on a loop in a foreground daemon (wrap in launchd / systemd for production)
|
|
178
|
+
- **Single hub** — spoke-to-spoke sync goes through the hub
|
|
179
|
+
- **Record-level LWW** — not field-level; concurrent edits to different fields of the same record may lose data
|