@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/n8n.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Flair + n8n
|
|
2
|
+
|
|
3
|
+
Use Flair as the memory backend for n8n's AI Agent. The same memories are readable from Claude Code, OpenClaw, and any other Flair client — that's the point.
|
|
4
|
+
|
|
5
|
+
## When to use Flair vs n8n's built-in memory connectors
|
|
6
|
+
|
|
7
|
+
n8n ships memory connectors for Postgres, MongoDB, and Redis. Those are real, persistent, and work fine for **conversation-buffer** use cases inside a single n8n instance.
|
|
8
|
+
|
|
9
|
+
Flair is the right pick when you want:
|
|
10
|
+
|
|
11
|
+
| | Flair | n8n built-ins |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| **Shape** | Tagged + typed memories with semantic search, plus chat-buffer compatibility | Conversation-buffer only (LangChain `BaseMessage` records) |
|
|
14
|
+
| **Cross-orchestrator** | Same memory readable from Claude Code, OpenClaw, n8n | n8n-internal schema; nothing else reads it |
|
|
15
|
+
| **Cross-instance** | Hub-spoke federation built-in (local ↔ Fabric, etc.) | Single-instance unless you self-build replication |
|
|
16
|
+
| **Identity** | Ed25519 per-agent (planned) or admin-token (v1) | n8n credential per workflow |
|
|
17
|
+
|
|
18
|
+
If your AI Agent only needs to remember the last N turns of a single chat in a single n8n instance, Postgres-as-memory is fine. If you want the same memory to inform a Claude Code conversation tomorrow, or to persist across n8n redeploys via federated Flair, this package is the path.
|
|
19
|
+
|
|
20
|
+
## Setup (5 minutes)
|
|
21
|
+
|
|
22
|
+
### 1. Install Flair (if not already running)
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install -g @tpsdev-ai/flair
|
|
26
|
+
flair init
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This starts a local Flair server on `http://localhost:19926`. For shared/team setups, see [Deployment](./deployment.md).
|
|
30
|
+
|
|
31
|
+
### 2. Install the n8n community node
|
|
32
|
+
|
|
33
|
+
In n8n: **Settings → Community Nodes → Install** → enter `@tpsdev-ai/n8n-nodes-flair` → confirm and restart.
|
|
34
|
+
|
|
35
|
+
### 3. Create the credential
|
|
36
|
+
|
|
37
|
+
In n8n: **Credentials → New → Flair API**. Fill in:
|
|
38
|
+
|
|
39
|
+
| Field | Value |
|
|
40
|
+
|---|---|
|
|
41
|
+
| **Base URL** | `http://localhost:19926` (or your team's Flair URL) |
|
|
42
|
+
| **Agent ID** | Logical identity that will own memories from this n8n workspace. Workflows that share an Agent ID share memory ownership. Use distinct IDs when isolation matters. |
|
|
43
|
+
| **Admin Password** | Your Flair admin password (in `~/.flair/admin-pass` for local installs). **Sensitive** — see [Security](#security). |
|
|
44
|
+
|
|
45
|
+
Click **Test** — if the test request to `/Memory` returns 200, the credential is good.
|
|
46
|
+
|
|
47
|
+
### 4. Wire the nodes
|
|
48
|
+
|
|
49
|
+
Three nodes ship in the package:
|
|
50
|
+
|
|
51
|
+
- **Flair Chat Memory** — connects to an AI Agent's `Memory` socket. Stores chat history in Flair, scoped by Subject. Defaults to per-workflow memory; set the optional Session Sub-Key to `={{ $execution.id }}` for per-run isolation.
|
|
52
|
+
- **Flair Search** — connects to an AI Agent's `Tool` socket. Two operations:
|
|
53
|
+
- *Semantic Search* — agent calls `flair_search({ query })`, gets memories ranked by similarity.
|
|
54
|
+
- *Get By Subject* — agent calls `flair_get_by_subject()`, gets memories under a config-time-bound subject.
|
|
55
|
+
- **Flair Write** — a regular Main-input/Main-output pipeline node (not wired to the AI Agent's Tool socket). Takes an incoming item and writes its content as a Flair memory, with Subject, Tags, and Durability fields. For operator-driven capture-and-archive flows (mail → memory, webhook → memory, parsed-doc → memory) where the write is the workflow author's choice, not the LLM's.
|
|
56
|
+
|
|
57
|
+
A typical workflow:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
[Webhook] → [AI Agent]
|
|
61
|
+
├─ Model: Claude / OpenAI / etc.
|
|
62
|
+
├─ Memory: Flair Chat Memory (Subject: customer-support)
|
|
63
|
+
├─ Tool: Flair Search (Operation: Semantic Search)
|
|
64
|
+
└─ Tool: HTTP Request (etc.)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The agent now answers using both its current chat history (from Flair Chat Memory) and any relevant historical memories it pulls in via Flair Search.
|
|
68
|
+
|
|
69
|
+
## Subject and SessionId guidance
|
|
70
|
+
|
|
71
|
+
n8n memory connectors expose a `sessionKey` parameter that scopes the chat history. Flair has a richer model:
|
|
72
|
+
|
|
73
|
+
- **Subject** (required) — the entity / conversation / topic the memory is about. Indexed in Flair's schema; efficient to filter on. Default: `={{ $workflow.name }}`.
|
|
74
|
+
- **Session Sub-Key** (optional) — appended to the subject as `<subject>:<sessionKey>`. Use the n8n execution id (`={{ $execution.id }}`) for per-run isolation, or a customer/user id for per-customer scoping, or leave blank to share across runs.
|
|
75
|
+
|
|
76
|
+
Patterns:
|
|
77
|
+
|
|
78
|
+
- **"This assistant remembers"** — set Subject to a stable string (`customer-support`, `daily-standup`). Leave Session Sub-Key blank. All runs share memory.
|
|
79
|
+
- **Per-conversation isolation** — set Subject to the conversation owner (`customer:1234`), leave Session Sub-Key blank. Each conversation is isolated by subject.
|
|
80
|
+
- **Per-execution isolation** — set Session Sub-Key to `={{ $execution.id }}`. Each n8n run gets its own memory window. (This is most similar to n8n's default `sessionKey={{ $execution.id }}`.)
|
|
81
|
+
|
|
82
|
+
## Security
|
|
83
|
+
|
|
84
|
+
> **The admin password gives every workflow with this credential read/write access to the entire Flair instance**, not just the configured Agent ID. The blast radius is the whole memory store. Treat the credential as highly sensitive: n8n encrypts credentials at rest, but any n8n admin or backup restore can extract it.
|
|
85
|
+
|
|
86
|
+
For production deployments where untrusted workflow inputs reach Flair, wait for **Ed25519 per-agent authentication** (planned). v1 (admin password) is appropriate when:
|
|
87
|
+
|
|
88
|
+
- The n8n instance is single-tenant and operator-controlled
|
|
89
|
+
- Workflow inputs are trusted (your own CRM, your own webhook source)
|
|
90
|
+
- Memory leakage between agents is acceptable for the use case
|
|
91
|
+
|
|
92
|
+
If any of those don't hold, use Flair's CLI / SDK clients (which support per-agent Ed25519 today) and wait for the n8n credential update.
|
|
93
|
+
|
|
94
|
+
## Get By Tag — coming soon
|
|
95
|
+
|
|
96
|
+
The Flair Search node currently exposes Semantic Search and Get By Subject. **Get By Tag** is deferred until `flair-client.memory.list` exposes a `tags` filter (tracked in the [n8n-node spec](https://github.com/tpsdev-ai/flair/blob/main/specs/N8N-NODE-q3qf.md) §6). Workaround for now: use Semantic Search and let the model filter results by tags in the response.
|
|
97
|
+
|
|
98
|
+
## Worked examples
|
|
99
|
+
|
|
100
|
+
Two example workflows are coming in a follow-up release; they're authored inside a real n8n instance and round-tripped via Export so they import cleanly:
|
|
101
|
+
|
|
102
|
+
- **`chat-memory-demo.json`** — Webhook → AI Agent (Claude + Flair Chat Memory) → Respond. Demonstrates the conversation-buffer use case. Run twice with the same input to see memory replay.
|
|
103
|
+
- **`knowledge-search-demo.json`** — Schedule → AI Agent (Claude + Flair Chat Memory + Flair Search as Tool) → action. Demonstrates the structured-knowledge-search use case.
|
|
104
|
+
|
|
105
|
+
In the interim, follow the [Setup](#setup-5-minutes) and [Subject and SessionId guidance](#subject-and-sessionid-guidance) sections — wiring is straightforward without an example file.
|
|
106
|
+
|
|
107
|
+
## Compared to other Flair surfaces
|
|
108
|
+
|
|
109
|
+
| Surface | Use case | Setup |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| [Claude Code](./claude-code.md) | Personal AI assistant memory across CLI sessions | npm install + `flair init` |
|
|
112
|
+
| [OpenClaw](./openclaw.md) | Multi-agent OpenClaw deployments | OpenClaw plugin install |
|
|
113
|
+
| [MCP](./mcp-clients.md) | Any MCP client (Claude Desktop, etc.) | MCP server registration |
|
|
114
|
+
| **n8n (this doc)** | Workflow-engine AI Agents | n8n community-node install |
|
|
115
|
+
|
|
116
|
+
Same Flair instance, same memories, different surfaces.
|
|
117
|
+
|
|
118
|
+
## See also
|
|
119
|
+
|
|
120
|
+
- [Spec — `@tpsdev-ai/n8n-nodes-flair`](https://github.com/tpsdev-ai/flair/blob/main/specs/N8N-NODE-q3qf.md) — implementation plan, design decisions, anti-patterns
|
|
121
|
+
- [Bridges](./bridges.md) — how Flair memories flow between hosts and instances
|
|
122
|
+
- [Federation](./federation.md) — hub-and-spoke replication
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# Google ADK Spike — Plugin Contract Findings
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-05-05
|
|
4
|
+
**Investigator:** Ember
|
|
5
|
+
**Scope:** Verify plugin contract for a `flair-adk-py` adapter
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Q1: ADK Basics — Exists? Open-source? Python?
|
|
10
|
+
|
|
11
|
+
**Yes.** Google ADK is a real, open-source, Python-first framework.
|
|
12
|
+
|
|
13
|
+
| Field | Value |
|
|
14
|
+
|---|---|
|
|
15
|
+
| **Package** | `google-adk` on PyPI |
|
|
16
|
+
| **Current version** | 1.32.0 (released 2026-04-30) |
|
|
17
|
+
| **Repo** | https://github.com/google/adk-python |
|
|
18
|
+
| **License** | Apache 2.0 |
|
|
19
|
+
| **Docs** | https://google.github.io/adk-docs/ |
|
|
20
|
+
| **Language** | Python (primary). Java, Go, and TypeScript ports exist at `google/adk-java`, `google/adk-go`, `waldzellai/adk-typescript` |
|
|
21
|
+
| **Samples** | https://github.com/google/adk-samples |
|
|
22
|
+
|
|
23
|
+
From `pyproject.toml`:
|
|
24
|
+
```toml
|
|
25
|
+
name = "google-adk"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Q2: Memory Contract — Abstract Class for Custom Backends
|
|
31
|
+
|
|
32
|
+
ADK defines `BaseMemoryService(ABC)` in `google/adk/memory/base_memory_service.py`. This is the contract custom memory backends implement — equivalent to LangChain's `BaseChatMemory` or n8n's `BaseListChatMessageHistory`.
|
|
33
|
+
|
|
34
|
+
### Interface signature
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
class BaseMemoryService(ABC):
|
|
38
|
+
"""Base class for memory services."""
|
|
39
|
+
|
|
40
|
+
# --- REQUIRED (abstract) methods ---
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
async def add_session_to_memory(self, session: Session) -> None:
|
|
44
|
+
"""Ingest a full session's events into memory.
|
|
45
|
+
|
|
46
|
+
Called periodically or on-demand to persist conversation history.
|
|
47
|
+
Session may be added multiple times during its lifetime.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
async def search_memory(
|
|
52
|
+
self, *,
|
|
53
|
+
app_name: str,
|
|
54
|
+
user_id: str,
|
|
55
|
+
query: str,
|
|
56
|
+
) -> SearchMemoryResponse:
|
|
57
|
+
"""Semantic/text search over ingested memories for a user.
|
|
58
|
+
|
|
59
|
+
Returns SearchMemoryResponse containing list[MemoryEntry].
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
# --- OPTIONAL (default: raise NotImplementedError) ---
|
|
63
|
+
|
|
64
|
+
async def add_events_to_memory(
|
|
65
|
+
self, *,
|
|
66
|
+
app_name: str,
|
|
67
|
+
user_id: str,
|
|
68
|
+
events: Sequence[Event],
|
|
69
|
+
session_id: str | None = None,
|
|
70
|
+
custom_metadata: Mapping[str, object] | None = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Incremental delta: add a subset of events, not the full session.
|
|
73
|
+
|
|
74
|
+
Implementations should treat `events` as incremental, not replacing
|
|
75
|
+
the full session. custom_metadata keys are service-specific.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
async def add_memory(
|
|
79
|
+
self, *,
|
|
80
|
+
app_name: str,
|
|
81
|
+
user_id: str,
|
|
82
|
+
memories: Sequence[MemoryEntry],
|
|
83
|
+
custom_metadata: Mapping[str, object] | None = None,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Direct write: add explicit MemoryEntry objects (not from events).
|
|
86
|
+
|
|
87
|
+
For services that support writing memory facts directly.
|
|
88
|
+
"""
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Key data types
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
class MemoryEntry(BaseModel):
|
|
95
|
+
"""Represent one memory entry."""
|
|
96
|
+
content: types.Content # google.genai.types.Content (parts: text, inline_data, etc.)
|
|
97
|
+
custom_metadata: dict[str, Any] = Field(default_factory=dict)
|
|
98
|
+
id: Optional[str] = None
|
|
99
|
+
author: Optional[str] = None
|
|
100
|
+
timestamp: Optional[str] = None # ISO 8601 preferred
|
|
101
|
+
|
|
102
|
+
class SearchMemoryResponse(BaseModel):
|
|
103
|
+
memories: list[MemoryEntry] = Field(default_factory=list)
|
|
104
|
+
|
|
105
|
+
class Session(BaseModel):
|
|
106
|
+
id: str
|
|
107
|
+
app_name: str
|
|
108
|
+
user_id: str
|
|
109
|
+
state: dict[str, Any]
|
|
110
|
+
events: list[Event] # conversation events
|
|
111
|
+
last_update_time: float
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Scoping model
|
|
115
|
+
|
|
116
|
+
All memory operations are scoped to `(app_name, user_id)`. There is no global memory. `session_id` is an optional partition within `add_events_to_memory`.
|
|
117
|
+
|
|
118
|
+
### How memory is consumed by agents
|
|
119
|
+
|
|
120
|
+
ADK ships two built-in memory tools:
|
|
121
|
+
|
|
122
|
+
- **`LoadMemoryTool`** — reactive. The model decides when to call `load_memory(query)`, which calls `tool_context.search_memory(query)`. Only available when `FeatureName.JSON_SCHEMA_FOR_FUNC_DECL` is enabled.
|
|
123
|
+
- **`PreloadMemoryTool`** — automatic. On every LLM request, searches memory using the user's query text and prepends a `<PAST_CONVERSATIONS>` system instruction block. No model decision needed.
|
|
124
|
+
|
|
125
|
+
Both are `tools` added to an agent's `tool_executor`, not part of the `BaseMemoryService` interface.
|
|
126
|
+
|
|
127
|
+
### Memory is wired through `InvocationContext`
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
class InvocationContext(BaseModel):
|
|
131
|
+
# ...
|
|
132
|
+
memory_service: Optional[BaseMemoryService] = None
|
|
133
|
+
# ...
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
And agents access it via `Context` (the tool/tool call context):
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
# In agent tools / callbacks:
|
|
140
|
+
ctx = Context(...) # = ToolContext
|
|
141
|
+
await ctx.search_memory("what did user ask yesterday?")
|
|
142
|
+
await ctx.add_session_to_memory()
|
|
143
|
+
await ctx.add_events_to_memory(events=[...])
|
|
144
|
+
await ctx.add_memory(memories=[MemoryEntry(...)])
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Q3: Persistence Contract — "Memory" vs "Knowledge" / "Session"
|
|
150
|
+
|
|
151
|
+
**Yes, ADK explicitly separates these concepts into two distinct abstractions:**
|
|
152
|
+
|
|
153
|
+
### Session Service — `BaseSessionService`
|
|
154
|
+
|
|
155
|
+
Location: `google/adk/sessions/base_session_service.py`
|
|
156
|
+
|
|
157
|
+
Handles **conversation history** (ordered events within a session). Think "chat transcript".
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
class BaseSessionService(abc.ABC):
|
|
161
|
+
@abc.abstractmethod
|
|
162
|
+
async def create_session(self, *, app_name, user_id, state=None, session_id=None) -> Session
|
|
163
|
+
@abc.abstractmethod
|
|
164
|
+
async def get_session(self, *, app_name, user_id, session_id, config=None) -> Optional[Session]
|
|
165
|
+
@abc.abstractmethod
|
|
166
|
+
async def list_sessions(self, *, app_name, user_id=None) -> ListSessionsResponse
|
|
167
|
+
@abc.abstractmethod
|
|
168
|
+
async def delete_session(self, *, app_name, user_id, session_id) -> None
|
|
169
|
+
|
|
170
|
+
# non-abstract: append_event (adds events in-memory, updates session state)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Shipped implementations:
|
|
174
|
+
- `InMemorySessionService` — volatile, for prototyping
|
|
175
|
+
- `SqliteSessionService` — file-based SQLite
|
|
176
|
+
- `DatabaseSessionService` — generic SQL (supports PostgreSQL, MySQL)
|
|
177
|
+
- `VertexAiSessionService` — stores sessions in Vertex AI
|
|
178
|
+
|
|
179
|
+
### Memory Service — `BaseMemoryService`
|
|
180
|
+
|
|
181
|
+
Location: `google/adk/memory/base_memory_service.py`
|
|
182
|
+
|
|
183
|
+
Handles **searchable, cross-session knowledge** derived from conversation events. Think "vector store / RAG".
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
class BaseMemoryService(ABC):
|
|
187
|
+
@abstractmethod
|
|
188
|
+
async def add_session_to_memory(self, session: Session) -> None
|
|
189
|
+
@abstractmethod
|
|
190
|
+
async def search_memory(self, *, app_name, user_id, query) -> SearchMemoryResponse
|
|
191
|
+
# + optional add_events_to_memory, add_memory (see Q2)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Shipped implementations:
|
|
195
|
+
- `InMemoryMemoryService` — keyword matching, prototyping only
|
|
196
|
+
- `VertexAiMemoryBankService` — Google Cloud's managed Memory Bank (vector + LLM-summarized)
|
|
197
|
+
- `VertexAiRagMemoryService` — Vertex AI RAG corpora
|
|
198
|
+
|
|
199
|
+
### Retrieval tools — `BaseRetrievalTool`
|
|
200
|
+
|
|
201
|
+
A third concern: **external knowledge retrieval** (files, RAG corpora, databases).
|
|
202
|
+
|
|
203
|
+
Location: `google/adk/tools/retrieval/base_retrieval_tool.py`
|
|
204
|
+
|
|
205
|
+
This is a `Tool` (callable by the agent model), not a service. Shipped implementations:
|
|
206
|
+
- `FilesRetrieval` — local file-based retrieval
|
|
207
|
+
- `LlamaIndexRetrieval` — LlamaIndex-backed retrieval
|
|
208
|
+
- `VertexAiRagRetrieval` — Vertex AI RAG as a tool
|
|
209
|
+
|
|
210
|
+
**Summary:** ADK has a clean 3-way split:
|
|
211
|
+
1. **Sessions** = ordered conversation history (persist per-session)
|
|
212
|
+
2. **Memory** = searchable, cross-session knowledge (persist per-user)
|
|
213
|
+
3. **Retrieval** = external document/knowledge access (tool-based)
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Q4: Reference Implementations — Shipped & Community
|
|
218
|
+
|
|
219
|
+
### Shipped with ADK (in `google.adk.memory`)
|
|
220
|
+
|
|
221
|
+
| Implementation | Source | Description |
|
|
222
|
+
|---|---|---|
|
|
223
|
+
| `InMemoryMemoryService` | `google/adk/memory/in_memory_memory_service.py` | Keyword matching, thread-safe, dev/testing only |
|
|
224
|
+
| `VertexAiMemoryBankService` | `google/adk/memory/vertex_ai_memory_bank_service.py` | Google Cloud Memory Bank — uses `memories.ingest_events` + `memories.generate` + `memories.retrieve`. Supports TTL, revision tracking, metadata consolidation |
|
|
225
|
+
| `VertexAiRagMemoryService` | `google/adk/memory/vertex_ai_rag_memory_service.py` | Vertex AI RAG corpora — uploads session events as temp JSON files, retrieves via `rag.retrieval_query` |
|
|
226
|
+
|
|
227
|
+
### Community packages
|
|
228
|
+
|
|
229
|
+
| Package | Source | Description |
|
|
230
|
+
|---|---|---|
|
|
231
|
+
| `google-adk-redis` (0.0.4) | https://github.com/redis-developer/adk-redis | Redis integration — implements Memory, Sessions, and Search tools |
|
|
232
|
+
| `google-adk-community` (0.4.1) | PyPI | Community extensions (specifics unknown without install) |
|
|
233
|
+
|
|
234
|
+
### Shipped session backends (for reference)
|
|
235
|
+
|
|
236
|
+
| Implementation | Source |
|
|
237
|
+
|---|---|
|
|
238
|
+
| `InMemorySessionService` | `google/adk/sessions/in_memory_session_service.py` |
|
|
239
|
+
| `SqliteSessionService` | `google/adk/sessions/sqlite_session_session_service.py` |
|
|
240
|
+
| `DatabaseSessionService` | `google/adk/sessions/database_session_service.py` |
|
|
241
|
+
| `VertexAiSessionService` | `google/adk/sessions/vertex_ai_session_service.py` |
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Q5: Auth / Config Conventions
|
|
246
|
+
|
|
247
|
+
### LLM configuration
|
|
248
|
+
|
|
249
|
+
ADK uses the `google-genai` SDK client for model calls. Config conventions:
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
# Standard Gemini (AI Studio API key):
|
|
253
|
+
from google.adk.models.google_llm import Gemini
|
|
254
|
+
model = Gemini(model="gemini-2.5-flash")
|
|
255
|
+
# Needs: GOOGLE_API_KEY env var
|
|
256
|
+
|
|
257
|
+
# Vertex AI (project + location):
|
|
258
|
+
model = Gemini(model="gemini-2.5-flash") # same constructor
|
|
259
|
+
# Needs: GOOGLE_GENAI_USE_VERTEXAI=true, GOOGLE_CLOUD_PROJECT, GOOGLE_CLOUD_LOCATION env vars
|
|
260
|
+
# Or: google.auth default credentials (Application Default Credentials)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Key env vars:
|
|
264
|
+
| Env var | Purpose |
|
|
265
|
+
|---|---|
|
|
266
|
+
| `GOOGLE_API_KEY` | AI Studio API key (Express Mode) |
|
|
267
|
+
| `GOOGLE_GENAI_USE_VERTEXAI` | If `true`/`1`, routes to Vertex AI instead of AI Studio |
|
|
268
|
+
| `GOOGLE_CLOUD_PROJECT` | GCP project ID for Vertex AI |
|
|
269
|
+
| `GOOGLE_CLOUD_LOCATION` | GCP region for Vertex AI |
|
|
270
|
+
| `APIGEE_PROXY_URL` | Apigee proxy URL (for `ApigeeLlm`) |
|
|
271
|
+
|
|
272
|
+
### Memory service configuration
|
|
273
|
+
|
|
274
|
+
Services take explicit constructor params — no env var convention for memory backends:
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
# VertexAiMemoryBankService — requires agent engine ID
|
|
278
|
+
from google.adk.memory import VertexAiMemoryBankService
|
|
279
|
+
memory = VertexAiMemoryBankService(
|
|
280
|
+
project="my-project",
|
|
281
|
+
location="us-central1",
|
|
282
|
+
agent_engine_id="456",
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# VertexAiRagMemoryService — requires RAG corpus
|
|
286
|
+
from google.adk.memory import VertexAiRagMemoryService
|
|
287
|
+
memory = VertexAiRagMemoryService(
|
|
288
|
+
rag_corpus="projects/.../ragCorpora/my-corpus",
|
|
289
|
+
similarity_top_k=5,
|
|
290
|
+
)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Server wiring
|
|
294
|
+
|
|
295
|
+
In the ADK CLI web server (`adk web`), memory_service is injected at the server level:
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
```python
|
|
299
|
+
# WARNING: FastApiAdkRunner class name requires verification — not located in adk-python @ v1.32.0.
|
|
300
|
+
# The adk-python main branch has `Runner` and `InMemoryRunner` in `google/adk/runners.py`,
|
|
301
|
+
# and `adk_web_server.py` handles the HTTP entry point without a FastAPI-specific runner class.
|
|
302
|
+
# The memory_service injection pattern (runner → InvocationContext → ctx) is correct,
|
|
303
|
+
# but the specific class name may be internal, renamed, or from a different ADK version.
|
|
304
|
+
# Verified against: https://github.com/google/adk-python (main branch, 2026-05-05)
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
The runner pattern injects `memory_service` into each `InvocationContext`, making it available to all agents via `ctx.search_memory()`, `ctx.add_session_to_memory()`, etc.
|
|
308
|
+
|
|
309
|
+
### Session-scoped config
|
|
310
|
+
|
|
311
|
+
There is **no per-session memory config**. Memory is configured at the app/server level (one `BaseMemoryService` instance per runner). The `(app_name, user_id)` tuple in each call acts as the scope.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## Adapter Design Notes for `flair-adk-py`
|
|
316
|
+
|
|
317
|
+
Based on this investigation, a `flair-adk-py` adapter would:
|
|
318
|
+
|
|
319
|
+
1. **Subclass `BaseMemoryService`** — implement `add_session_to_memory()` and `search_memory()`. Optionally implement `add_events_to_memory()` and `add_memory()`.
|
|
320
|
+
|
|
321
|
+
2. **Handle `Event` → Flair event conversion** — ADK events use `google.genai.types.Content` (parts-based). Need to map `Content.parts[i].text` → Flair events.
|
|
322
|
+
|
|
323
|
+
3. **Handle Flair event → `MemoryEntry` conversion** — for `search_memory()`, return `SearchMemoryResponse(memories=[MemoryEntry(content=..., author=..., timestamp=...)])`.
|
|
324
|
+
|
|
325
|
+
4. **Inject via `InvocationContext`** — the adapter instance is passed as `memory_service=MyFlairMemoryService()` when constructing the runner.
|
|
326
|
+
|
|
327
|
+
5. **Scope mapping** — ADK scopes to `(app_name, user_id)`. Map `app_name` → Flair space, `user_id` → Flair user/agent.
|
|
328
|
+
|
|
329
|
+
6. **Session vs Memory** — ADK already handles session persistence separately via `BaseSessionService`. The adapter only needs to handle the memory/search side.
|
|
330
|
+
|
|
331
|
+
No implementation was done — this is a research report.
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# Headless agent-auth to MCP — the Flair/consumer half
|
|
2
|
+
|
|
3
|
+
> **Status:** complete against the published `@harperfast/oauth@2.2.0`, with
|
|
4
|
+
> one explicitly-drawn boundary: the full over-network CIMD fetch + token
|
|
5
|
+
> mint cannot be exercised end-to-end against a loopback Harper (the
|
|
6
|
+
> plugin's unconditional SSRF gate forbids it — by design, see
|
|
7
|
+
> [the SSRF/loopback boundary](#the-ssrfloopback-boundary) below). That last
|
|
8
|
+
> hop is deferred to a follow-up e2e against a real public HTTPS host;
|
|
9
|
+
> `bench.tps.harperfabric.com` is the planned venue.
|
|
10
|
+
|
|
11
|
+
This is the Flair-side consumer for RFC 7523 `client_credentials` +
|
|
12
|
+
`private_key_jwt` agent-auth: a headless Flair agent authenticating *as
|
|
13
|
+
itself* — no browser, no human — to a Harper MCP `/mcp` endpoint, using its
|
|
14
|
+
existing Ed25519 identity key. Plugin side:
|
|
15
|
+
[HarperFast/oauth#159](https://github.com/HarperFast/oauth/issues/159)
|
|
16
|
+
(parent issue, decomposed into 4 parts — all shipped, see below).
|
|
17
|
+
|
|
18
|
+
## Upstream state (as of 2026-07-12)
|
|
19
|
+
|
|
20
|
+
| # | What | State |
|
|
21
|
+
|---|------|-------|
|
|
22
|
+
| #160 / PR #165 | client-assertion primitives (strict EdDSA verify + `jti` replay store) | **SHIPPED** (merged @ `d48c3b2`) |
|
|
23
|
+
| #161 / #167 | CIMD-first client resolution & validation for `private_key_jwt` agents (SSRF-guarded fetch, validation, cache) | **SHIPPED** in 2.2.0 via PR #170 |
|
|
24
|
+
| #162 / PR #170 | token-endpoint grant — assertion + resolved client + RFC 8707 resource binding | **SHIPPED** in 2.2.0 |
|
|
25
|
+
| #163 / PR #171 | token-issuance rate limiting (per verified client_id, post-auth debit) | **SHIPPED** in 2.2.0 |
|
|
26
|
+
|
|
27
|
+
Every claim below about plugin behavior is read from the **published 2.2.0
|
|
28
|
+
package source** (`node_modules/@harperfast/oauth/dist/lib/mcp/*.js`) and,
|
|
29
|
+
where testable, proven against that real code — not mirrored, not guessed.
|
|
30
|
+
(History note: a 2026-07-09 revision of this doc corrected an earlier false
|
|
31
|
+
"#167 merged" claim; #167 has since genuinely shipped as part of 2.2.0.)
|
|
32
|
+
|
|
33
|
+
## 1. Assertion signing + live token mint — `flair mcp token`
|
|
34
|
+
|
|
35
|
+
`src/mcp-client-assertion.ts` builds + signs the `client_assertion` JWT:
|
|
36
|
+
header `{alg: "EdDSA", typ: "JWT"}`; claims `iss = sub = client_id`, `aud =
|
|
37
|
+
token endpoint`, `exp - iat ≤ 60s` (hard-capped, not just defaulted), `iat`,
|
|
38
|
+
random `jti`. Signed with `node:crypto` alone (no new dependency, matching
|
|
39
|
+
the plugin's own approach and this repo's existing `flair-client.mjs` /
|
|
40
|
+
`buildEd25519Auth` signing style). The claim shape is pinned to what PR #165
|
|
41
|
+
(`src/lib/mcp/clientAssertion.ts`) verifies, and
|
|
42
|
+
`test/unit/mcp-client-credentials-live-package.test.ts` proves assertions
|
|
43
|
+
this module signs pass the plugin's **real** `verifyClientAssertion` —
|
|
44
|
+
including negative cases (wrong signing key, tampered payload).
|
|
45
|
+
|
|
46
|
+
Key loading (`resolveAgentKeyPath` / `loadEd25519PrivateKeyFromFile`) mirrors
|
|
47
|
+
`src/cli.ts`'s existing `resolveKeyPath` / `buildEd25519Auth` (used by the
|
|
48
|
+
TPS-Ed25519 REST-auth path) exactly: same search order (`--keys-dir` /
|
|
49
|
+
`FLAIR_KEY_DIR` / `~/.flair/keys` / `~/.tps/secrets/flair`), same format
|
|
50
|
+
cascade (raw 32-byte seed, base64 seed, base64 PKCS8 DER, PEM fallback). No
|
|
51
|
+
new key material, no new on-disk format.
|
|
52
|
+
|
|
53
|
+
### The live round-trip (formerly stubbed — now real)
|
|
54
|
+
|
|
55
|
+
`requestMcpAccessToken` POSTs the `client_credentials` grant to the token
|
|
56
|
+
endpoint (`/oauth/mcp/token`) and returns the minted token. Its error
|
|
57
|
+
behavior is deliberate:
|
|
58
|
+
|
|
59
|
+
- **429 `slow_down`** (the #171/#163 issuance rate limit): honors the
|
|
60
|
+
`Retry-After` header with **full jitter** (sleep a random duration in
|
|
61
|
+
`[0, Retry-After]`), retrying up to `maxRetries` (default 3 — an
|
|
62
|
+
assertion is only valid ≤ 60s, so unbounded retry would outlive it).
|
|
63
|
+
A missing `Retry-After` falls back to capped exponential backoff — never
|
|
64
|
+
hammer, even a non-conformant server.
|
|
65
|
+
- **Any other non-2xx** throws `McpTokenRequestError` (status + OAuth error
|
|
66
|
+
code preserved) with **no retry** — those responses mean the request
|
|
67
|
+
itself is wrong, and retrying identically would just burn a fresh `jti`.
|
|
68
|
+
|
|
69
|
+
`getMcpAccessToken` wraps it with the consumer-side requirements the 2.2.0
|
|
70
|
+
rate limiter creates (pinned in flair#663's tracking thread): **mint
|
|
71
|
+
sparingly** — tokens are cached per `(clientId, tokenEndpoint, resource)`
|
|
72
|
+
and reused until within a refresh margin (default 30s) of expiry, because
|
|
73
|
+
the limiter debits per *mint*, not per use. `forceRefresh` bypasses the
|
|
74
|
+
cache (e.g. after an unexpected 401).
|
|
75
|
+
|
|
76
|
+
CLI: `flair mcp token --agent-id <id>` now performs the real mint by
|
|
77
|
+
default; `--dry-run` restores the old sign-and-print behavior.
|
|
78
|
+
`--client-id`/`--token-endpoint`/`--resource` default to this instance's own
|
|
79
|
+
oauth surface (derived from `FLAIR_MCP_ISSUER` / `FLAIR_PUBLIC_URL`,
|
|
80
|
+
matching `resources/mcp-oauth-flag.ts`) but are fully overridable — an
|
|
81
|
+
agent can authenticate to a **different** Harper MCP server; identity is
|
|
82
|
+
portable.
|
|
83
|
+
|
|
84
|
+
### RFC 8707 `resource`
|
|
85
|
+
|
|
86
|
+
`buildTokenRequestForm` carries an optional `resource` field, pass-through
|
|
87
|
+
only (it never invents a value). The CLI supplies the default:
|
|
88
|
+
`${FLAIR_MCP_ISSUER}/mcp`, the same canonical resource identifier
|
|
89
|
+
`resources/mcp-oauth-flag.ts`'s `mcpResource()` uses for this instance's own
|
|
90
|
+
`/mcp` audience binding — exact-match, fail-closed on the AS side. The
|
|
91
|
+
`resource` is part of the token-cache key: a token minted for one resource
|
|
92
|
+
never serves another.
|
|
93
|
+
|
|
94
|
+
## 2. CIMD publish — `resources/MCPClientMetadata.ts`
|
|
95
|
+
|
|
96
|
+
Each agent's Client ID Metadata Document is **served**, not just generated:
|
|
97
|
+
`GET /MCPClientMetadata/{agentId}` (public, unauthenticated — mirrors
|
|
98
|
+
`AgentCard.ts`'s A2A-card posture). `client_id` = that URL itself (derived
|
|
99
|
+
from `mcpIssuer()`, same env vars as above); `jwks` = the agent's existing
|
|
100
|
+
`Agent.publicKey` re-expressed as a JWK OKP.
|
|
101
|
+
|
|
102
|
+
Why served rather than exported for external hosting: Flair already
|
|
103
|
+
publishes public agent-discovery metadata this way (`AgentCard.ts`), CIMD is
|
|
104
|
+
explicitly the **stateless** registration path (no DCR row to replicate
|
|
105
|
+
across Fabric nodes), and the agent's public key is already Flair's source
|
|
106
|
+
of truth. Serving it keeps that single source of truth instead of
|
|
107
|
+
introducing a second, externally-hosted copy that could drift.
|
|
108
|
+
|
|
109
|
+
Field logic lives in `resources/mcp-client-metadata-fields.ts` (Harper-free,
|
|
110
|
+
mirrors `agentcard-fields.ts`'s pattern), shape-pinned to oauth#161:
|
|
111
|
+
|
|
112
|
+
- `grant_types: ["client_credentials"]`; `token_endpoint_auth_method:
|
|
113
|
+
"private_key_jwt"`.
|
|
114
|
+
- `jwks` = a JWK Set containing exactly one PUBLIC OKP/Ed25519 key.
|
|
115
|
+
`buildCimdDocument` rejects non-OKP/non-Ed25519 keys, a missing/malformed
|
|
116
|
+
`x`, and any JWK carrying a private `d` component (defensive runtime
|
|
117
|
+
check; the TS type has no `d` field).
|
|
118
|
+
- `redirect_uris` and `response_types` are BOTH omitted — neither exists on
|
|
119
|
+
the `CimdDocument` type, so neither can leak back in. This is the
|
|
120
|
+
conditional shape #161 specifies for client_credentials-only clients.
|
|
121
|
+
|
|
122
|
+
This document is proven against the plugin's **real published pipeline**:
|
|
123
|
+
`test/unit/mcp-client-credentials-live-package.test.ts` drives 2.2.0's
|
|
124
|
+
actual `resolveCimdClient` (fetch → SSRF gate → validate → cache) via its
|
|
125
|
+
exported `_setDnsLookup`/`_setFetch` test hooks and confirms the document
|
|
126
|
+
resolves end-to-end, plus the fail-closed negatives: no
|
|
127
|
+
`clientIdMetadataDocuments.allowedHosts` configured → rejected even though
|
|
128
|
+
the fetch succeeded, and a document with leaked private-key material →
|
|
129
|
+
rejected by the plugin's validator even if our own build-time guard were
|
|
130
|
+
bypassed.
|
|
131
|
+
|
|
132
|
+
## 3. Test evidence — three layers, strongest available at each boundary
|
|
133
|
+
|
|
134
|
+
1. **Mirror unit tests** (`test/unit/mcp-client-assertion.test.ts`) — our
|
|
135
|
+
signer vs. a local mirror of #165's verification contract, all negative
|
|
136
|
+
cases; plus the full `requestMcpAccessToken`/`getMcpAccessToken` behavior
|
|
137
|
+
matrix (429 backoff with and without `Retry-After`, retry exhaustion,
|
|
138
|
+
non-JSON responses, cache reuse/expiry/`forceRefresh`/per-resource
|
|
139
|
+
isolation) against an injected fetch.
|
|
140
|
+
2. **Live-package unit tests**
|
|
141
|
+
(`test/unit/mcp-client-credentials-live-package.test.ts`) — no mirrors:
|
|
142
|
+
the REAL published 2.2.0 `verifyClientAssertion`, `resolveCimdClient`,
|
|
143
|
+
and `createRateLimiter` composed in-process. Includes the
|
|
144
|
+
post-auth-debit ordering proof (#171/#163): a forged assertion never
|
|
145
|
+
reaches the rate limiter, so it cannot drain a real client's bucket; and
|
|
146
|
+
the exact-`Retry-After` handshake between the plugin's limiter and our
|
|
147
|
+
client backoff.
|
|
148
|
+
3. **Live-Harper e2e**
|
|
149
|
+
(`test/integration/mcp-client-credentials-e2e.test.ts`) — an ephemeral
|
|
150
|
+
Harper (via `test/helpers/harper-lifecycle.ts`) running
|
|
151
|
+
`@harperfast/oauth@2.2.0` as a real mounted component. Proves, over real
|
|
152
|
+
HTTP: the AS discovery document advertises
|
|
153
|
+
`client_credentials`/`private_key_jwt`/`EdDSA`; our CIMD document is
|
|
154
|
+
served correctly; a real grant naming our agent reaches CIMD resolution
|
|
155
|
+
and is rejected **only** by the SSRF/DNS gate (distinguishable from both
|
|
156
|
+
shape errors and the allowlist gate, which is proven live and distinct);
|
|
157
|
+
and our client helper surfaces the rejection as a typed error.
|
|
158
|
+
|
|
159
|
+
## The SSRF/loopback boundary
|
|
160
|
+
|
|
161
|
+
Read this before trying to "finish" the e2e locally — the wall is real and
|
|
162
|
+
it is the plugin working as designed, not a bug:
|
|
163
|
+
|
|
164
|
+
`@harperfast/oauth@2.2.0`'s CIMD document fetch (`dist/lib/mcp/cimd.js`)
|
|
165
|
+
enforces an **unconditional SSRF gate**: the `client_id` URL must be
|
|
166
|
+
`https://` (no loopback carve-out — contrast `mcp.issuer`'s explicit
|
|
167
|
+
loopback TLS exception, which has no analog here), and every DNS-resolved
|
|
168
|
+
address is checked against the full IANA private/loopback/link-local ranges
|
|
169
|
+
with **no override or allowlist knob**. A CIMD document served by a
|
|
170
|
+
loopback/private-network Harper can therefore never be fetched by the AS
|
|
171
|
+
over a real network hop. Consequences, all verified empirically while
|
|
172
|
+
building this slice:
|
|
173
|
+
|
|
174
|
+
- The plugin's `_setDnsLookup`/`_setFetch` injection hooks only help when
|
|
175
|
+
the test and the plugin run in the **same process**. A spawned Harper
|
|
176
|
+
(the `harper-lifecycle.ts` pattern) does not share the test's module
|
|
177
|
+
registry.
|
|
178
|
+
- Harper's component loader gives each `package:`-declared component an
|
|
179
|
+
**isolated module graph even within the same process** — a same-process
|
|
180
|
+
JS resource that deep-imports `cimd.js` and arms the hooks on its own
|
|
181
|
+
import instance does not affect the module instance the plugin's
|
|
182
|
+
`/oauth/mcp/token` route uses (confirmed by direct experiment: the
|
|
183
|
+
harness's own `resolveClient` call succeeded against the mocked
|
|
184
|
+
transport; the live HTTP endpoint, hit immediately after, still failed
|
|
185
|
+
with the identical SSRF rejection).
|
|
186
|
+
- Seeding the DCR client store is not a bypass either:
|
|
187
|
+
`MCPClientStore`'s `encodeRecord`/`decodeRecord`
|
|
188
|
+
(`dist/lib/mcp/clientStore.js`) persist no `jwks`/`_cimd` fields, so a
|
|
189
|
+
stored client can never satisfy `handleClientCredentialsGrant`'s
|
|
190
|
+
`client._cimd !== true` gate — confirmed by reading the published source.
|
|
191
|
+
|
|
192
|
+
So the local e2e drives the live grant as far as it can physically go —
|
|
193
|
+
into CIMD resolution, stopped only by the DNS gate — and the remaining
|
|
194
|
+
behavior (the plugin's real fetch of our served document, followed by a
|
|
195
|
+
200 token mint over the network) is covered in-process by the live-package
|
|
196
|
+
tests instead. **The genuine end-to-end over-network mint is deferred** to
|
|
197
|
+
an environment where this Flair instance's CIMD route is served from a
|
|
198
|
+
real, publicly-resolvable HTTPS host. Planned follow-up: run exactly
|
|
199
|
+
`test/integration/mcp-client-credentials-e2e.test.ts`'s happy path against
|
|
200
|
+
`bench.tps.harperfabric.com` (our public bench host) with
|
|
201
|
+
`clientIdMetadataDocuments.allowedHosts` pointed at it — at which point the
|
|
202
|
+
SSRF-gate rejection assertion flips into a 200-mint assertion. Until that
|
|
203
|
+
runs, "token mints over a real network hop" remains **inferred** from the
|
|
204
|
+
in-process proof, not measured; everything else above is measured.
|
|
205
|
+
|
|
206
|
+
## Deployment coordination note
|
|
207
|
+
|
|
208
|
+
Whoever owns the AS-side `@harperfast/oauth` config for a given Harper
|
|
209
|
+
instance MUST add this Flair instance's `MCPClientMetadata` host to
|
|
210
|
+
`clientIdMetadataDocuments.allowedHosts`. That host is derived from
|
|
211
|
+
`FLAIR_MCP_ISSUER` (or `FLAIR_PUBLIC_URL` as fallback) — the same env var
|
|
212
|
+
`mcpIssuer()` in `resources/mcp-oauth-flag.ts` and `defaultMcpClientId()` in
|
|
213
|
+
`src/mcp-client-assertion.ts` both read. If the allowlist isn't updated,
|
|
214
|
+
`/mcp` client_credentials auth for this agent fails closed (by design; the
|
|
215
|
+
e2e proves the "Unknown client" rejection live). This is an operational
|
|
216
|
+
step, not a code change; flag it explicitly when coordinating a rollout.
|
|
217
|
+
The AS must also be able to fetch that host over public HTTPS — see the
|
|
218
|
+
SSRF boundary above.
|
|
219
|
+
|
|
220
|
+
## What's NOT in this slice
|
|
221
|
+
|
|
222
|
+
- The over-network CIMD fetch + 200 token mint e2e — deferred to the
|
|
223
|
+
public-host follow-up described above.
|
|
224
|
+
- `flair agent register-mcp-client` — not needed for the CIMD path (#161
|
|
225
|
+
replaced the DCR-shaped registration gate with the AS-side host
|
|
226
|
+
allowlist); only worth revisiting if DCR back-compat ever matters.
|
|
227
|
+
- Wiring the minted access token into an actual MCP client session
|
|
228
|
+
(`Authorization: Bearer` against `/mcp`) — the token is minted and
|
|
229
|
+
cached; consuming it is the natural next slice.
|