@tpsdev-ai/flair 0.51.2 → 0.52.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.
Files changed (71) hide show
  1. package/README.md +10 -5
  2. package/dist/build-info.json +3 -3
  3. package/dist/cli.js +575 -547
  4. package/dist/doctor-client.js +35 -0
  5. package/dist/hook-install.js +74 -0
  6. package/dist/install/global-bin-path.js +14 -0
  7. package/dist/lib/auth-resolve.js +15 -0
  8. package/dist/lib/doctor-run.js +28 -15
  9. package/dist/lib/upgrade-exec-path.js +257 -0
  10. package/dist/lib/upgrade-plain-tree.js +558 -0
  11. package/dist/rem/promote-policy.js +204 -0
  12. package/dist/rem/restore.js +55 -15
  13. package/dist/rem/runner.js +203 -20
  14. package/dist/resources/AdminMemory.js +2 -1
  15. package/dist/resources/AgentSeed.js +26 -10
  16. package/dist/resources/Asset.js +203 -0
  17. package/dist/resources/AutoPromoteCandidates.js +2 -4
  18. package/dist/resources/Credential.js +14 -0
  19. package/dist/resources/Federation.js +80 -0
  20. package/dist/resources/Integration.js +12 -0
  21. package/dist/resources/Memory.js +158 -60
  22. package/dist/resources/MemoryBootstrap.js +63 -20
  23. package/dist/resources/MemoryCandidate.js +12 -0
  24. package/dist/resources/MemoryConsolidate.js +2 -1
  25. package/dist/resources/MemoryDedupStats.js +17 -2
  26. package/dist/resources/MemoryFeed.js +30 -0
  27. package/dist/resources/MemoryGrant.js +14 -0
  28. package/dist/resources/MemoryReflect.js +75 -17
  29. package/dist/resources/Message.js +190 -0
  30. package/dist/resources/OrgEvent.js +12 -0
  31. package/dist/resources/PromoteMemoryCandidate.js +76 -0
  32. package/dist/resources/RecordUsage.js +1 -1
  33. package/dist/resources/Relationship.js +12 -0
  34. package/dist/resources/SemanticSearch.js +45 -13
  35. package/dist/resources/Soul.js +54 -18
  36. package/dist/resources/WorkspaceState.js +12 -0
  37. package/dist/resources/auth-middleware.js +17 -44
  38. package/dist/resources/authority-field-guard.js +37 -0
  39. package/dist/resources/bm25-index-service.js +1 -1
  40. package/dist/resources/bm25-index.js +50 -11
  41. package/dist/resources/embedding-space-guard.js +238 -0
  42. package/dist/resources/embeddings-provider.js +32 -5
  43. package/dist/resources/federation-classify.js +23 -1
  44. package/dist/resources/health.js +11 -2
  45. package/dist/resources/hit-tracking.js +244 -0
  46. package/dist/resources/mcp-tools.js +272 -7
  47. package/dist/resources/memory-reflect-lib.js +111 -0
  48. package/dist/resources/migrations/embedding-stamp.js +22 -4
  49. package/dist/resources/owner-field-guard.js +62 -0
  50. package/dist/resources/promotion-stamp.js +29 -0
  51. package/dist/resources/record-owner-guard.js +71 -5
  52. package/dist/resources/record-types.js +30 -7
  53. package/dist/resources/relay-lib.js +205 -0
  54. package/dist/resources/relay-ops.js +294 -0
  55. package/dist/resources/skill-write.js +120 -0
  56. package/dist/resources/soul-adk-guard.js +68 -0
  57. package/dist/resources/soul-write-policy.js +63 -0
  58. package/dist/resources/table-helpers.js +2 -0
  59. package/dist/resources/usage-recording.js +3 -3
  60. package/dist/src/rem/promote-policy.js +204 -0
  61. package/docs/api-reference.md +374 -0
  62. package/docs/auth.md +52 -0
  63. package/docs/federation.md +4 -0
  64. package/docs/integrations.md +6 -6
  65. package/docs/mcp-clients.md +16 -1
  66. package/docs/releasing.md +11 -8
  67. package/docs/rem.md +20 -2
  68. package/docs/upgrade.md +47 -2
  69. package/package.json +6 -5
  70. package/schemas/memory.graphql +51 -2
  71. package/schemas/message.graphql +74 -0
@@ -0,0 +1,204 @@
1
+ // Pure promotion policy shared by the CLI and trusted server workflow.
2
+ export function validatePromoteOpts(opts) {
3
+ if (!opts.rationale || !opts.rationale.trim()) {
4
+ return "--rationale is required (per spec § 5: no rubber-stamp)";
5
+ }
6
+ if (!opts.to || (opts.to !== "soul" && opts.to !== "memory")) {
7
+ return "--to must be 'soul' or 'memory'";
8
+ }
9
+ if (opts.to === "soul" && (!opts.key || !opts.key.trim())) {
10
+ return "--key is required when --to=soul (gives the Soul entry a meaningful identifier)";
11
+ }
12
+ return null;
13
+ }
14
+ export function validateRejectOpts(opts) {
15
+ if (!opts.reason || !opts.reason.trim()) {
16
+ return "--reason is required";
17
+ }
18
+ return null;
19
+ }
20
+ /**
21
+ * Decide whether a promote/reject action can proceed against a candidate's
22
+ * current state, and what message to surface to the operator. Pure function;
23
+ * action side effects happen in the CLI body after this returns ok.
24
+ */
25
+ export function decideCandidateAction(candidate, action) {
26
+ if (!candidate)
27
+ return { ok: false, severity: "error", message: "candidate not found" };
28
+ const status = candidate.status;
29
+ if (status === "promoted") {
30
+ return action === "promote"
31
+ ? { ok: false, severity: "error", message: `already promoted (target=${candidate.target}, reviewer=${candidate.reviewerId})` }
32
+ : { ok: false, severity: "error", message: `already promoted; cannot reject after promotion` };
33
+ }
34
+ if (status === "rejected") {
35
+ return action === "reject"
36
+ ? { ok: false, severity: "info", message: `already rejected on ${candidate.decidedAt} by ${candidate.reviewerId}` }
37
+ : { ok: false, severity: "error", message: `already rejected; use a fresh candidate or reset status manually` };
38
+ }
39
+ return { ok: true };
40
+ }
41
+ // ─── ADK tag-lineage on promote (#1205 slice 1205a — Sherlock security req) ───
42
+ // ADK session records are written by adk-flair (memory_service.py) under a
43
+ // SHARED-namespace agentId, with per-user separation carried ENTIRELY by a
44
+ // compound scope tag `adk:<app>:<user>`. That tag is the access-control
45
+ // boundary. A candidate distilled from those records therefore MUST carry the
46
+ // scope tag when promoted, or the promoted claim lands in the shared agentId
47
+ // memory retrievable by every other user of the app — a cross-user leak.
48
+ //
49
+ // `rem promote` historically hard-coded `["nightly-rem-promoted", from:<id>]`
50
+ // and DROPPED the source tag. We now propagate the source scope tag for
51
+ // ADK-sourced candidates, and FAIL CLOSED (refuse) when a candidate is
52
+ // ADK-sourced but its scope tag can't be uniquely+completely determined.
53
+ //
54
+ // SCOPING (deliberate, per spec): fail-closed applies ONLY to ADK-sourced
55
+ // candidates. Non-ADK candidates carry no `adk:` tag and promote byte-for-byte
56
+ // as before — a transient/deleted source on a non-ADK candidate must NOT block
57
+ // its promotion.
58
+ //
59
+ // SEAM (foundation only; the distillation engine is slice #1205b): ADK-sourcing
60
+ // is detected here by re-reading the candidate's source memories and inspecting
61
+ // their tags. That leaves ONE residual fail-open: an ADK-sourced candidate all
62
+ // of whose source memories are unreadable (deleted/transient) yields no `adk:`
63
+ // evidence and is treated as non-ADK. Closing that corner without regressing
64
+ // non-ADK promotion requires the ENGINE to stamp the authoritative scope tag
65
+ // onto the MemoryCandidate row at distillation time (it distills per single
66
+ // scope:tagged tag, so it knows it authoritatively). `derivePromotedTags` is
67
+ // written so that override can be threaded in later without touching callers.
68
+ export const ADK_SCOPE_TAG_PREFIX = "adk:";
69
+ /**
70
+ * Decide the tag set for a promoted Memory given the candidate id and the
71
+ * result of fetching each of its source memories. Pure — no I/O; the action
72
+ * callback does the fetching and threads the results here so this is unit-
73
+ * testable and the fail-closed logic is exercised directly.
74
+ *
75
+ * `stampedScopeTag` (#1205b-1 — the engine slice the #1205a SEAM note below
76
+ * anticipated): the authoritative scope:"tagged" tag the distillation engine
77
+ * stamped onto the MemoryCandidate row (resources/MemoryReflect.ts →
78
+ * buildStagedCandidateRow). When present it is AUTHORITATIVE and short-circuits
79
+ * the source re-read entirely — the engine distilled under exactly this one
80
+ * tag, so it knows the per-user scope tag independent of whether the source
81
+ * memories are still readable. This closes the residual fail-open the SEAM
82
+ * note describes: a candidate all of whose sources are unreadable yields no
83
+ * `adk:` evidence and would otherwise be mis-classified NON-ADK and promoted
84
+ * tagless into the shared agentId namespace (a cross-user leak). Threading it
85
+ * in as an optional trailing arg keeps every pre-#1205b caller (and every
86
+ * candidate that never carried a stamp) on the unchanged source-re-read path.
87
+ *
88
+ * With NO stamp (undefined/empty) the source-re-read classification runs
89
+ * exactly as in #1205a:
90
+ * - No `adk:` scope tag across readable sources → NON-ADK candidate; return
91
+ * the provenance tags only (unchanged behavior).
92
+ * - Exactly one `adk:` scope tag AND every source readable → ADK-sourced;
93
+ * return [scopeTag, ...provenance].
94
+ * - `adk:` evidence present but the scope tag is ambiguous (>1 distinct tag)
95
+ * OR incomplete (some source unreadable) → REFUSE (fail-closed): a
96
+ * tagless/mis-tagged claim in a shared ADK namespace is a cross-user leak,
97
+ * not a benign miss.
98
+ */
99
+ export function derivePromotedTags(candidateId, sources, stampedScopeTag) {
100
+ const provenance = ["nightly-rem-promoted", `from:${candidateId}`];
101
+ // #1205b-1: a stamped scope tag is AUTHORITATIVE — consume it directly, never
102
+ // re-read sources. This is the seam closure: correctness no longer depends on
103
+ // source readability. `adkSourced` (which gates the Soul-promotion refusal in
104
+ // the promote action) tracks whether the stamped tag is an ADK scope tag.
105
+ if (typeof stampedScopeTag === "string" && stampedScopeTag.length > 0) {
106
+ return {
107
+ ok: true,
108
+ tags: [stampedScopeTag, ...provenance],
109
+ adkSourced: stampedScopeTag.startsWith(ADK_SCOPE_TAG_PREFIX),
110
+ };
111
+ }
112
+ const adkTags = new Set();
113
+ let anySourceUnreadable = false;
114
+ for (const s of sources) {
115
+ if (!s.ok) {
116
+ anySourceUnreadable = true;
117
+ continue;
118
+ }
119
+ for (const t of s.tags) {
120
+ if (typeof t === "string" && t.startsWith(ADK_SCOPE_TAG_PREFIX))
121
+ adkTags.add(t);
122
+ }
123
+ }
124
+ // No positive ADK evidence → non-ADK. An unreadable source with zero ADK
125
+ // evidence does NOT fail closed here (that would regress non-ADK promotion);
126
+ // see the SEAM note above.
127
+ if (adkTags.size === 0) {
128
+ return { ok: true, tags: provenance, adkSourced: false };
129
+ }
130
+ if (adkTags.size > 1) {
131
+ return {
132
+ ok: false,
133
+ reason: `ADK-sourced candidate spans multiple scope tags (${[...adkTags].sort().join(", ")}); refusing to promote — a merged cross-user claim would leak across users`,
134
+ };
135
+ }
136
+ if (anySourceUnreadable) {
137
+ return {
138
+ ok: false,
139
+ reason: `ADK-sourced candidate has unreadable source memories; the per-user scope tag cannot be confirmed — refusing to promote (fail-closed)`,
140
+ };
141
+ }
142
+ const scopeTag = [...adkTags][0];
143
+ return { ok: true, tags: [scopeTag, ...provenance], adkSourced: true };
144
+ }
145
+ // ─── Promoted-row visibility (flair#1257 slice 3 — default-private-unless) ────
146
+ // Continuity-journal scope tag prefix. Canonical string duplicated in
147
+ // resources/memory-reflect-lib.ts / resources/auto-promote-lib.ts and
148
+ // packages/flair-mcp/src/continuity.ts — this file sits on the CLI side of the
149
+ // npm-packaging boundary (see this file's header) and cannot import them; kept
150
+ // in sync by the shared canonical string, same discipline as
151
+ // MACHINE_REVIEWER_* below.
152
+ export const CONTINUITY_SCOPE_TAG_PREFIX = "adk:continuity:";
153
+ /**
154
+ * Decide a promoted Memory row's visibility for the HUMAN `rem promote` path
155
+ * (flair#1257 slice 3). Mirror of resources/auto-promote-lib.ts
156
+ * decidePromotedVisibility (the server-side auto-promote half) — Sherlock's
157
+ * default-private-unless ruling covers BOTH promotion paths: the sources of a
158
+ * continuity candidate are the most sensitive tier (ephemeral+private journal
159
+ * rows), so leaving visibility unset here would let Memory's durability-keyed
160
+ * default widen it to shared ("persistent" defaults shared) — a silent
161
+ * visibility escalation. "shared" only when the candidate is continuity-scoped
162
+ * AND carries the distiller's affirmative ruling WITH its recorded
163
+ * team-relevance justification; every other case — including every
164
+ * uncertainty — is "private".
165
+ *
166
+ * Returns undefined for NON-continuity candidates: their visibility behavior
167
+ * (durability-keyed default) is byte-for-byte the pre-slice-3 contract and is
168
+ * deliberately not changed here.
169
+ */
170
+ export function derivePromotedVisibility(candidate) {
171
+ const scopeTag = candidate.scopeTag;
172
+ const isContinuity = typeof scopeTag === "string" &&
173
+ scopeTag.length > CONTINUITY_SCOPE_TAG_PREFIX.length &&
174
+ scopeTag.startsWith(CONTINUITY_SCOPE_TAG_PREFIX);
175
+ if (!isContinuity)
176
+ return undefined;
177
+ if (candidate.visibilityRuling !== "shared")
178
+ return "private";
179
+ const rationale = typeof candidate.visibilityRationale === "string" ? candidate.visibilityRationale.trim() : "";
180
+ return rationale.length > 0 ? "shared" : "private";
181
+ }
182
+ // ─── Machine reviewer namespace (#1205 slice 1205a — Sherlock security req 4) ─
183
+ // A promotion records a reviewerId that feeds audit/attribution
184
+ // (schemas/memory.graphql:209). An automated (machine-driven) promotion path
185
+ // must record a reviewerId that can NEVER be mistaken for a human/agent
186
+ // reviewer, so attribution isn't laundered. Reserve the `machine:` namespace
187
+ // for that, and forbid the human `--reviewer` path from claiming it.
188
+ export const MACHINE_REVIEWER_PREFIX = "machine:";
189
+ /** Canonical machine reviewerId for the ADK auto-promote consumer (#1205b). */
190
+ export const MACHINE_REVIEWER_ADK_AUTO_PROMOTE = "machine:adk-auto-promote";
191
+ /** True iff `id` is in the reserved machine-reviewer namespace — i.e. it
192
+ * denotes an automated path, not a human or agent reviewer. */
193
+ export function isMachineReviewerId(id) {
194
+ return typeof id === "string" && id.startsWith(MACHINE_REVIEWER_PREFIX);
195
+ }
196
+ /** The human `flair rem promote` path must not record a reviewerId in the
197
+ * reserved machine namespace — that would launder automated attribution onto
198
+ * a human-operated promotion. Returns an error string, or null if allowed. */
199
+ export function validateHumanReviewerId(reviewerId) {
200
+ if (isMachineReviewerId(reviewerId)) {
201
+ return `--reviewer '${reviewerId}' uses the reserved '${MACHINE_REVIEWER_PREFIX}' namespace (reserved for automated promotion); use a human/agent reviewer id`;
202
+ }
203
+ return null;
204
+ }
@@ -0,0 +1,374 @@
1
+ # API & schema reference
2
+
3
+ Consolidated catalog of Flair's HTTP surface, per-resource auth, and GraphQL
4
+ table schemas. Narrative lives in the topic docs; this page is the map so an
5
+ adopter does not have to read `resources/` or `schemas/` to see what is
6
+ callable.
7
+
8
+ - **Auth model:** [docs/auth.md](auth.md), [SECURITY.md](../SECURITY.md)
9
+ - **Access invariant:** [DESIGN.md](../DESIGN.md) — open within the org, closed
10
+ at the federation edge
11
+ - **Federation pairing and sync:** [docs/federation.md](federation.md)
12
+ - **Source of truth for tables:** [`schemas/*.graphql`](../schemas/)
13
+ - **Source of truth for policy:** [`resources/record-types.ts`](../resources/record-types.ts)
14
+
15
+ The [docs-freshness gate](../scripts/docs-freshness-check.mjs) (`api-reference-schema-coverage`)
16
+ fails when a GraphQL `@table` type is missing from this file, so a new table
17
+ cannot land undocumented.
18
+
19
+ Default REST port is `19926` (`DEFAULT_PORT` in `src/cli.ts`). Override with
20
+ `--port` / `HTTP_PORT`. Sign remote requests as
21
+ `agentId:timestamp:nonce:METHOD:/path?query` and send
22
+ `Authorization: TPS-Ed25519 <agentId>:<ts>:<nonce>:<sig>`. Protocol detail is
23
+ in [SECURITY.md](../SECURITY.md).
24
+
25
+ ## How Harper maps resources to URLs
26
+
27
+ Flair is a Harper application. Two things become HTTP paths:
28
+
29
+ 1. **`@table @export` in GraphQL** — Harper generates REST CRUD at
30
+ `/<TypeName>` and `/<TypeName>/<id>` (GET collection / GET by id / POST /
31
+ PUT / PATCH / DELETE). Flair resource classes override those verbs to add
32
+ identity gates, read-scope, and write policy.
33
+ 2. **`export class Foo extends Resource`** in `resources/` — Harper mounts the
34
+ class name as `/Foo`. Custom verbs are whatever the class implements
35
+ (`post()` for actions, `get()` for reads).
36
+
37
+ In-process callers use `server.resources.get("Memory")` (no leading slash).
38
+ See [docs/embedding-in-a-harper-app.md](embedding-in-a-harper-app.md).
39
+
40
+ `@table` types **without** `@export` have no REST surface. They are listed in
41
+ the schema section so the catalog is complete.
42
+
43
+ ## Auth classes
44
+
45
+ | Class | Credential | Typical grant |
46
+ |-------|------------|---------------|
47
+ | **Public** | none | Discovery, health, OAuth well-known, Presence roster (field-allowlisted) |
48
+ | **Ed25519 agent** | `TPS-Ed25519` header | Default agent path. Writes as self only. Reads follow the resource's read-scope (below). |
49
+ | **Admin Basic** | Harper `HDB_ADMIN_PASSWORD` / `FLAIR_ADMIN_PASSWORD` | Whole-instance operator. Bypasses agent scoping, including `private` memory. Used by the web admin and `n8n-nodes-flair`. |
50
+ | **Operator / internal** | Admin Basic, or a deliberate `internalContext()` call inside the process | Soul mutations and `AgentSeed`. Agent Ed25519 keys — including admin-agent keys — cannot author Soul. |
51
+ | **Federation body-sig** | Ed25519 over the request body + timestamp/nonce; pairing uses a one-time token | `/FederationPair`, `/FederationSync`. Harper role gate is open; the handler is the auth boundary. |
52
+ | **OAuth bearer** | Access token from Flair's AS or `@harperfast/oauth` | `/mcp` only, and only when `FLAIR_MCP_OAUTH=true` plus a public issuer. Off by default (path 404s). |
53
+
54
+ Anonymous HTTP is denied on every agent-facing table. A by-id miss and a
55
+ by-id deny both return **404**, never 403, so ids are not an existence oracle.
56
+
57
+ ### Read-scope vocabulary
58
+
59
+ From `RECORD_TYPES` in `resources/record-types.ts`:
60
+
61
+ | Scope | Meaning | Tables |
62
+ |-------|---------|--------|
63
+ | **open-within-org** | Own rows (any visibility) plus every other agent's non-private rows | Memory |
64
+ | **owner-only** | Only the owning agent (plus admin / internal) | Relationship, WorkspaceState, Asset, MemoryCandidate |
65
+ | **none** | Any verified agent reads every row; no visibility field | Soul, OrgEvent |
66
+
67
+ These three scopes are **not** in `RECORD_TYPES`. Each is hand-implemented on its
68
+ own resource:
69
+
70
+ | Scope | Meaning | Table | Enforced in |
71
+ |-------|---------|-------|-------------|
72
+ | **party** | Sender or recipient only | Message | `resources/Message.ts` |
73
+ | **own-ledger** | Only the contributing agent's rows | MemoryUsage | `resources/MemoryUsage.ts` |
74
+ | **owner-or-grantee** | Either party on the grant | MemoryGrant | `resources/MemoryGrant.ts` |
75
+
76
+ Writes stamp `agentId` (or `authorId` / `from`) from the authenticated
77
+ principal. A body that names a different owner is rejected or overwritten
78
+ per the table's attribution mode — never trusted.
79
+
80
+ Federation sync currently pushes **Memory** (non-`private`), **Soul**,
81
+ **Agent**, and **Relationship**. `Message` has a classifier policy for a later
82
+ cross-host slice; it is not in today's spoke push list. Everything else is
83
+ instance-local.
84
+
85
+ ---
86
+
87
+ ## Endpoints
88
+
89
+ Paths are the Harper class / table name. Collection GET is `GET /Name`; by-id
90
+ is `GET /Name/<id>` unless noted.
91
+
92
+ ### Public and health
93
+
94
+ | Method | Path | Auth | Notes |
95
+ |--------|------|------|-------|
96
+ | GET | `/Health`, `/health` | Public | Liveness. `searchReady` is always present; HTTP 503 / `ok: false` when search cannot be served. |
97
+ | GET | `/HealthDetail` | Ed25519 | Rich stats (counts, agents, migration). |
98
+ | GET | `/AgentCard/<agentId>` | Public | A2A agent-card; field-allowlisted. |
99
+ | GET | `/a2a`, `/A2AAdapter` | Public | A2A discovery. |
100
+ | POST | `/a2a`, `/A2AAdapter` | Ed25519 | JSON-RPC actions (writes OrgEvents, reads tasks). GET-only is public; POST is not. |
101
+
102
+ ### Identity — Agent, Presence, Soul
103
+
104
+ | Method | Path | Auth | Read / write |
105
+ |--------|------|------|--------------|
106
+ | GET | `/Agent`, `/Agent/<id>` | Ed25519 | Any verified agent may read principals (discovery). |
107
+ | POST | `/Agent` | Admin Basic | Create principal. Also `POST /AgentSeed` (operator/internal only — not an admin-agent key). |
108
+ | PUT / PATCH | `/Agent/<id>` | Ed25519 | An agent updates **only its own** record. |
109
+ | DELETE | `/Agent/<id>` | Admin Basic | Deprovision. |
110
+ | GET | `/Presence` | Public | Roster, field-allowlisted. `currentTask` is null for anonymous callers; verified agents see the text. |
111
+ | POST | `/Presence` | Ed25519 | Heartbeat. Agent writes only its own row (403 cross-agent). Stamps `flairVersion` / `harperVersion`. |
112
+ | PUT / DELETE | `/Presence/<id>` | Ed25519 | Own row only. Collection PUT is not a public bypass. |
113
+ | GET | `/Soul`, `/Soul/<id>` | Ed25519 | Any verified agent; unscoped (identity/discovery). |
114
+ | POST / PUT / PATCH / DELETE | `/Soul` | **Operator / internal** | Not Ed25519. Learned Memory text cannot be copied in as Soul. See [docs/auth.md](auth.md#soul-authorship). |
115
+ | POST | `/FeedSouls` | Ed25519 | Soul change feed (verified). |
116
+
117
+ `Credential` (GET/POST verified; extra credentials for a principal) and
118
+ `Integration` (legacy 0.x platform rows; verified, prefer Credential) sit on
119
+ the same identity plane.
120
+
121
+ ### Memory, search, bootstrap
122
+
123
+ | Method | Path | Auth | Read / write |
124
+ |--------|------|------|--------------|
125
+ | GET | `/Memory`, `/Memory/<id>` | Ed25519 | open-within-org. By-id deny = 404. |
126
+ | POST / PUT / PATCH | `/Memory` | Ed25519 | Own `agentId` only. Auto-embed on write. Visibility defaults from durability (`permanent`/`persistent` → `shared`, `standard`/`ephemeral` → `private`). |
127
+ | DELETE | `/Memory/<id>` | Ed25519 | Owner or admin. `permanent` owner-delete is allowed. |
128
+ | POST | `/SemanticSearch` | Ed25519 | Hybrid semantic + lexical. Same read-scope as Memory. Default scoring is `raw`. |
129
+ | POST | `/BootstrapMemories` | Ed25519 | Cold-start context (soul + predicted memories + optional org events). |
130
+ | POST | `/RecordUsage` | Ed25519 | Cross-agent usage signal (`Memory.usageCount`). No ownership requirement; no existence oracle in the response. Prefer this over writing `/MemoryUsage` directly. |
131
+ | GET | `/MemoryUsage` | Ed25519 | Own ledger rows only. PUT/DELETE are admin/internal — agents must not delete their row to re-count. |
132
+ | GET / write | `/MemoryGrant` | Ed25519 | Read: owner or grantee. Write/delete: owner only (you share your own memories). |
133
+ | GET / write | `/Asset` | Ed25519 | Owner-only blobs linked by `memoryId`. No MCP and no federation in this slice. |
134
+ | GET / write | `/MemoryCandidate` | Ed25519 | Owner-only REM drafts. Never auto-promoted except the narrow ADK path. |
135
+ | POST | `/PromoteMemoryCandidate` | Ed25519 | Promote/reject with required rationale. |
136
+ | POST | `/AutoPromoteCandidates` | Ed25519 | ADK per-user auto-promote only. |
137
+ | POST | `/FeedMemories` | Ed25519 | Ingest path; `agentId` stamped from the caller (`stamp-strict`). |
138
+ | POST | `/MemoryArchive` | Ed25519 | Basement / restore (`memory_basement` / `memory_restore` on `/mcp`). |
139
+ | POST | `/MemoryMaintenance` | Ed25519 or admin | Hygiene: expire ephemeral, archive old standard. Agent-scoped unless admin. |
140
+ | POST | `/MemoryReindex` | Admin / verified per handler | Embedding / HNSW rebuild. |
141
+ | POST | `/MemoryConsolidate` | Ed25519 | Dedup / consolidate. |
142
+ | POST | `/MemoryReflect` | Ed25519 | REM distill → MemoryCandidate. |
143
+ | POST | `/MemoryDedupStats` | Admin Basic | Dedup diagnostics. Fleet-wide sweep; `allowCreate` is `allowAdmin`. |
144
+ | POST | `/SkillScan` | Ed25519 | Skill-tag scan on Memory writes. |
145
+
146
+ Skill-tagged Memory rows embed from `trigger` (the recall signal), not
147
+ `content`. MCP tools: `skill_store`, `skill_search`, `skill_get`.
148
+
149
+ ### Relationships, workspace, org events, attention
150
+
151
+ | Method | Path | Auth | Read / write |
152
+ |--------|------|------|--------------|
153
+ | GET / PUT | `/Relationship` | Ed25519 | Owner-only. Upsert via PUT; provenance stamped server-side. |
154
+ | GET / POST / PUT | `/WorkspaceState` | Ed25519 | Owner-only. POST stamps `agentId`; PUT rejects a mismatch. |
155
+ | GET | `/WorkspaceLatest` | Ed25519 | Latest workspace row for the caller. |
156
+ | GET / POST / PUT | `/OrgEvent` | Ed25519 | Any verified agent reads every event. Writes stamp `authorId`. |
157
+ | GET | `/OrgEventCatchup` | Ed25519 | Catch-up feed for the caller. |
158
+ | POST | `/OrgEventMaintenance` | Ed25519 / admin | Expire / sweep org events. |
159
+ | POST | `/AttentionQuery` | Ed25519 | Cross-table “what touches entity E”. Entity strings: [docs/entity-vocabulary.md](entity-vocabulary.md). |
160
+
161
+ ### Federation
162
+
163
+ | Method | Path | Auth | Notes |
164
+ |--------|------|------|-------|
165
+ | GET | `/FederationInstance` | Admin Basic | Local instance identity (CLI / admin). Peers do not call this during pair. |
166
+ | POST | `/FederationPair` | Pairing token + body-sig | Public at the Harper role gate. Handler validates token, signature, anti-replay. Fabric uses the bootstrap-user triple from `flair federation token`. |
167
+ | POST | `/FederationSync` | Peer body-sig | Public at the role gate. Merge Memory / Soul / Agent / Relationship (and classifier-ready Message). Originator + per-record signature checks. |
168
+ | GET | `/FederationPeers` | Admin Basic | Known peers. |
169
+ | GET / write | `/Instance` | Read: Ed25519. Write: admin | Instance row (`flair_…` id, role hub/spoke). |
170
+ | GET / write | `/Peer` | Admin Basic | Pinned peer keys and sync cursors. |
171
+ | GET / write | `/PairingToken` | Admin Basic | One-time tokens; default TTL 1 hour. |
172
+
173
+ `Nonce` and `SyncLog` are **not** `@export` — no agent REST. Nonce is the
174
+ anti-replay store; SyncLog is the operator audit trail.
175
+
176
+ ### Messaging (Flair Relay)
177
+
178
+ | Method | Path | Auth | Notes |
179
+ |--------|------|------|-------|
180
+ | GET / POST | `/Message` | Ed25519 | POST sends (signed envelope). GET is party-scoped (`from` or `to`). Direct PUT is admin/internal. |
181
+ | GET | `/MessageInbox` | Ed25519 | Inbox for the caller. |
182
+ | POST | `/MessageAck` | Ed25519 | Consume a delivered message. |
183
+ | GET | `/MessageDeadLetter` | Ed25519 | Visible failures for the sender (`deadline`, `inbox_full`, …). |
184
+ | GET / POST | `/MessageSweep` | Ed25519 / admin | Deadline sweep. Messages never arrive at a silent drop. |
185
+
186
+ ### OAuth, MCP, XAA
187
+
188
+ | Method | Path | Auth | Notes |
189
+ |--------|------|------|-------|
190
+ | GET | `/.well-known/oauth-authorization-server`, `/OAuthMetadata` | Public | RFC 8414. CORS `*`. |
191
+ | GET | `/.well-known/oauth-protected-resource`, `…/mcp` | Public | RFC 9728. |
192
+ | POST | `/OAuthRegister` | `X-Flair-Initial-Access-Token` | DCR. **Off** unless `FLAIR_OAUTH_DCR_TOKEN` is set (32–508 chars). Rate-limited. |
193
+ | GET / POST | `/OAuthAuthorize` | Public (user consent) | Authorization code + PKCE. |
194
+ | POST | `/OAuthToken` | Public (client + code/assertion) | Token + `jwt-bearer` (XAA). |
195
+ | POST | `/OAuthRevoke` | Public (token) | Revocation. |
196
+ | GET / POST | `/mcp` | OAuth bearer | Curated tools. **Unmounted** until `FLAIR_MCP_OAUTH=true` and an issuer. |
197
+ | GET / write | `/OAuthClient` | Admin Basic | Durable client rows. |
198
+ | GET / write | `/IdpConfig` | Admin Basic | XAA IdP registration (`flair idp add`). |
199
+ | GET | `/MCPClientMetadata` | Public / handler-gated | CIMD documents for allowed hosts. |
200
+
201
+ `OAuthAuthCode`, `OAuthToken`, and `IdJagReplay` are internal tables (no
202
+ `@export`). They hold codes, hashed tokens, and used `jti` values.
203
+
204
+ OAuth rate limits and env vars: [docs/auth.md](auth.md#rate-limiting).
205
+
206
+ ### Admin UI
207
+
208
+ All `/Admin*` routes require **Admin Basic**.
209
+
210
+ | Path | Purpose |
211
+ |------|---------|
212
+ | `/Admin`, `/AdminDashboard` | Server-rendered console |
213
+ | `/AdminPrincipals` | Agents / users: view, promote, disable |
214
+ | `/AdminConnectors` | OAuth clients and sessions |
215
+ | `/AdminIdp` | IdP configuration |
216
+ | `/AdminMemory` | Browse / search memory as operator |
217
+ | `/AdminInstance` | Federation status, peers, instance |
218
+
219
+ ---
220
+
221
+ ## `/mcp` tools
222
+
223
+ Mounted only when OAuth MCP is on. Each tool wraps a resource above; identity
224
+ comes from the token `sub`, never from tool arguments.
225
+
226
+ | Tool | Wraps |
227
+ |------|-------|
228
+ | `memory_search` | `POST /SemanticSearch` |
229
+ | `memory_store` | `POST /Memory` |
230
+ | `memory_update` | Memory read-modify-write (in-place or `supersedes` version) |
231
+ | `memory_get` | `GET /Memory/<id>` |
232
+ | `memory_delete` | `DELETE /Memory/<id>` |
233
+ | `memory_basement` | `POST /MemoryArchive` (archive) |
234
+ | `memory_restore` | `POST /MemoryArchive` (restore) |
235
+ | `skill_store` / `skill_search` / `skill_get` | Skill-tagged Memory |
236
+ | `bootstrap` | `POST /BootstrapMemories` |
237
+ | `soul_get` | `GET /Soul` |
238
+ | `soul_set` | Soul write — still operator-gated on the resource |
239
+ | `flair_workspace_set` | `POST /WorkspaceState` |
240
+ | `flair_orgevent` | `POST /OrgEvent` |
241
+ | `attention` | `POST /AttentionQuery` |
242
+ | `record_usage` | `POST /RecordUsage` |
243
+
244
+ The stdio package `@tpsdev-ai/flair-mcp` is a separate HTTP client, not this
245
+ handler. It talks Ed25519 REST via `flair-client`.
246
+
247
+ ---
248
+
249
+ ## Schema
250
+
251
+ Fields below are the GraphQL attributes. Server-stamped columns are **not**
252
+ client-writable even if a client sends them. Full comments live in
253
+ `schemas/*.graphql`.
254
+
255
+ ### Presence (`schemas/schema.graphql`)
256
+
257
+ | Field | Type | Notes |
258
+ |-------|------|-------|
259
+ | `agentId` | ID PK | One row per agent |
260
+ | `lastHeartbeatAt` | BigInt | Unix ms; refreshed every heartbeat |
261
+ | `currentTask` | String | Free text; verified-agent read only |
262
+ | `activity` | String | `coding` \| `reviewing` \| `planning` \| `debugging` \| `idle` |
263
+ | `activityUpdatedAt` | BigInt | When activity/task were asserted |
264
+ | `flairVersion` | String | Serving `@tpsdev-ai/flair` version |
265
+ | `harperVersion` | String | Serving Harper version |
266
+
267
+ ### Memory (`schemas/memory.graphql`)
268
+
269
+ | Field | Type | Notes |
270
+ |-------|------|-------|
271
+ | `id` | ID PK | |
272
+ | `agentId` | String! | Owner; no-forge |
273
+ | `content` | String! | Embedded text (non-skill) |
274
+ | `contentHash` | String | Near-duplicate key |
275
+ | `trigger` | String | Skill “when to use”; skill rows embed from this |
276
+ | `visibility` | String | `shared` \| `private` (durability default if omitted) |
277
+ | `embedding` | [Float] | HNSW, M:16 |
278
+ | `embeddingModel` | String | Stamp of the model that produced the vector |
279
+ | `tags` | [String] | |
280
+ | `durability` | String | `permanent` \| `persistent` \| `standard` \| `ephemeral` |
281
+ | `source` | String | |
282
+ | `createdAt` / `updatedAt` | String | |
283
+ | `expiresAt` | String | Ephemeral TTL |
284
+ | `retrievalCount` / `lastRetrieved` | | Search-hit counters (weak signal). Incremented on `MemoryHitStat` and overlaid on Memory reads — search no longer rewrites the Memory row. |
285
+ | `usageCount` | Int | Verified-use signal; only `RecordUsage` / citations increment |
286
+ | `promotionStatus` / `promotedAt` / `promotedBy` | | REM promotion |
287
+ | `archived` / `archivedAt` / `archivedBy` | | Basement |
288
+ | `parentId` / `derivedFrom` / `sessionId` / `lastReflected` | | Learning pipeline |
289
+ | `supersedes` | String | Version chain |
290
+ | `subject` / `summary` | String | Compression: subject → summary → content |
291
+ | `validFrom` / `validTo` | String | Temporal validity; expired rows drop out of search |
292
+ | `_safetyFlags` | [String] | Content-safety scan |
293
+ | `provenance` | String | Server JSON `{ v, verified, claimed? }` |
294
+ | `originatorInstanceId` | String | Write-time instance id; preserved across sync |
295
+ | `metadata` | String | Client JSON blob; opaque to the server |
296
+ | `entities` | [String] | Attention-plane `type:value` strings |
297
+
298
+ ### Soul
299
+
300
+ | Field | Type | Notes |
301
+ |-------|------|-------|
302
+ | `id` | ID PK | Typically `agentId:key` |
303
+ | `agentId` | String! | Owner |
304
+ | `key` / `value` | String | Personality / procedure entry |
305
+ | `priority` | String | `critical` \| `high` \| `standard` \| `low` |
306
+ | `metadata` | String | JSON (skill governance, etc.) |
307
+ | `provenance` | String | Operator/internal author + `sourceClass` |
308
+ | `durability` | String | Default `permanent` |
309
+ | `createdAt` / `updatedAt` | String | |
310
+ | `originatorInstanceId` | String | Federation origin |
311
+
312
+ ### Agent (Principal)
313
+
314
+ The Agent table **is** the Principal table. Pre-1.0 rows without `kind` are
315
+ agents.
316
+
317
+ | Field | Type | Notes |
318
+ |-------|------|-------|
319
+ | `id` | ID PK | Agent id |
320
+ | `name` | String! | |
321
+ | `role` / `type` | String | Legacy; `role` reconciles with `admin` |
322
+ | `kind` | String | `human` \| `agent` |
323
+ | `displayName` | String | |
324
+ | `status` | String | `active` \| `deactivated` |
325
+ | `publicKey` | String! | Ed25519 public key |
326
+ | `defaultTrustTier` | String | `endorsed` \| `corroborated` \| `unverified` |
327
+ | `admin` | Boolean | Principal-table admin bit |
328
+ | `runtime` / `runtimeEndpoint` | String | How to reach the principal |
329
+ | `subjects` | [String] | Soul-level interests |
330
+ | `createdAt` / `updatedAt` | String | |
331
+ | `originatorInstanceId` | String | Federation origin |
332
+
333
+ Related: **Credential** (`principalId`, `kind` webauthn / bearer-token /
334
+ ed25519 / idp) and **Integration** (legacy platform connection).
335
+
336
+ ### Federation tables (`schemas/federation.graphql`)
337
+
338
+ | Type | REST? | Purpose |
339
+ |------|-------|---------|
340
+ | **Instance** | yes | One row per Flair instance (`id`, `publicKey`, `role` hub/spoke, `fabricEndpoint`, `status`) |
341
+ | **PairingToken** | yes | One-time token (`expiresAt`, `consumedBy`) |
342
+ | **Peer** | yes | Pinned peer (`publicKey`, `endpoint`, `status`, `lastSyncAt` / `lastMergeAt`, `lastSyncCursor`, `relayOnly`) |
343
+ | **Nonce** | no | Body-sig anti-replay; PK is the nonce string |
344
+ | **SyncLog** | no | Per-sync audit (`peerId`, `direction`, counts, `skippedReasons`, `status`) |
345
+
346
+ ### Other tables
347
+
348
+ | Type | File | REST? | Role |
349
+ |------|------|-------|------|
350
+ | **Relationship** | memory.graphql | yes | `subject` / `predicate` / `object` + temporal bounds + provenance |
351
+ | **MemoryGrant** | memory.graphql | yes | `ownerId`, `granteeId`, `scope`, `filter` |
352
+ | **MemoryUsage** | memory.graphql | yes | Dedup ledger; PK `${agentId}:${memoryId}` |
353
+ | **MemoryHitStat** | memory.graphql | no | Search-hit ledger (`retrievalCount`, `lastRetrieved`); overlaid onto Memory reads |
354
+ | **MemoryCandidate** | memory.graphql | yes | REM draft (`claim`, `status`, `scopeTag`, visibility ruling) |
355
+ | **Asset** | memory.graphql | yes | Blob (`contentType`, `data`) owned by `agentId`, linked by `memoryId` |
356
+ | **WorkspaceState** | workspace.graphql | yes | Current work (`ref`, `provider`, `phase`, `entities`) |
357
+ | **OrgEvent** | event.graphql | yes | Org-visible event (`authorId`, `kind`, `summary`, `entities`) |
358
+ | **Message** | message.graphql | yes | Signed envelope (`from`, `to`, `threadId`, `seq`, `state`, `signature`) |
359
+ | **OAuthClient** | oauth.graphql | yes | Registered OAuth clients |
360
+ | **OAuthAuthCode** | oauth.graphql | no | Single-use codes + PKCE |
361
+ | **OAuthToken** | oauth.graphql | no | Hashed access/refresh tokens |
362
+ | **IdpConfig** | oauth.graphql | yes | XAA IdP (`issuer`, `jwksUri`, `requiredDomain`) |
363
+ | **IdJagReplay** | oauth.graphql | no | Used ID-JAG `jti` values |
364
+
365
+ ---
366
+
367
+ ## See also
368
+
369
+ - [docs/auth.md](auth.md) — Ed25519, OAuth 2.1, XAA, Soul authorship
370
+ - [docs/federation.md](federation.md) — pairing, sync, CLI
371
+ - [docs/rem.md](rem.md) — MemoryCandidate promote/reject
372
+ - [docs/entity-vocabulary.md](entity-vocabulary.md) — `entities` grammar
373
+ - [docs/embedding-in-a-harper-app.md](embedding-in-a-harper-app.md) — in-process API
374
+ - [`packages/flair-client/README.md`](../packages/flair-client/README.md) — typed HTTP client
package/docs/auth.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # Authentication & Authorization
2
2
 
3
3
  Flair supports three authentication methods, from simplest to most enterprise-ready.
4
+ HTTP paths and per-resource auth are catalogued in **[docs/api-reference.md](api-reference.md)**.
4
5
 
5
6
  ## Auth across surfaces (read this first)
6
7
 
@@ -298,3 +299,54 @@ The web admin at `/AdminDashboard` provides a UI for managing:
298
299
  - **Instance:** federation status, peer connections
299
300
 
300
301
  Access requires admin-level authentication (Basic auth with the Harper admin password).
302
+
303
+ ## Soul authorship
304
+
305
+ Soul mutations require verified Harper administrator Basic credentials on the
306
+ REST API, or a deliberate `internalContext()` call inside the server. Agent
307
+ Ed25519 keys, including admin-agent keys, and MCP/OAuth delegation cannot create,
308
+ update, patch or delete Soul. A missing context does not grant Soul authority.
309
+ This distinguishes credential classes; an administrator password is still a
310
+ privileged secret, not proof that a human typed the request. Keep it out of
311
+ agent-runtime environments. The n8n adapter currently uses admin Basic credentials
312
+ and therefore retains operator-level access; it needs separate runtime credentials
313
+ to receive the runtime restriction. Existing verified Soul reads are unchanged.
314
+
315
+ For an explicit operator edit:
316
+
317
+ ```sh
318
+ flair soul set --agent mybot --key role --value "Security reviewer" --admin-pass-file ~/.flair/admin-pass
319
+ flair rem restore <date> --agent mybot --apply --admin-pass-file ~/.flair/admin-pass
320
+ ```
321
+
322
+ `--admin-user` selects a non-default Harper administrator. The server overwrites
323
+ Soul's `provenance` with the authenticated author, timestamp, and verified
324
+ `sourceClass` (`operator` or `internal`); body fields cannot choose that class.
325
+
326
+ `AgentSeed` uses the same operator/internal gate on purpose: minting a principal
327
+ and its identity is a trust-root act, so an admin-agent key cannot provision.
328
+ This is an intended provisioning change, not only a Soul-write restriction.
329
+ `flair agent add` and the setup wizard already seed through administrator Basic
330
+ credentials (ops API); they do not call `AgentSeed` with an Ed25519 agent key.
331
+ Deliberate `internalContext()` provisioning still passes. Do not widen the gate
332
+ to admin-agent keys.
333
+
334
+ Federation remains an authenticated instance-to-instance replication path and
335
+ preserves the originating record; it does not reclassify a runtime request as
336
+ an operator edit. Raw Harper OPS access remains administrator infrastructure.
337
+
338
+ An operator write is refused if its value exactly matches stored Memory or
339
+ MemoryCandidate text for the target agent, including legacy or untagged records.
340
+ The owner-scoped lookup prevents another agent from blocking edits by copying
341
+ known Soul text into its own memories. Learned artifacts are not an operator-authored Soul source;
342
+ claimed or missing provenance does not exempt them. Lookup failure aborts the
343
+ write. This is an exact-match backstop, not semantic detection of paraphrases.
344
+ Existing Soul records remain readable without a migration.
345
+
346
+ The legacy `adk:` body-tag and stored-tag refusal remains a compatibility
347
+ bridge until **2026-10-31** (`ADK_SOUL_REFUSE_KILL_DATE` in
348
+ `resources/soul-adk-guard.ts`). Runtime denial itself never depends on
349
+ connector names — Soul writes are deny-by-default, and Flair stamps
350
+ `sourceClass` from the authenticated credential, not from a body field.
351
+ Removal of the vendor-string bridge is #1540; do not grow a per-connector
352
+ blacklist in Soul.
@@ -239,6 +239,10 @@ flair federation status
239
239
  flair federation unpin <instanceId>
240
240
  ```
241
241
 
242
+ HTTP paths (`/FederationPair`, `/FederationSync`, `/FederationInstance`,
243
+ `/FederationPeers`) and the Instance / Peer / PairingToken / Nonce / SyncLog
244
+ schemas: **[docs/api-reference.md](api-reference.md#federation)**.
245
+
242
246
  ## Limitations (1.0)
243
247
 
244
248
  - **HTTP push only** — no persistent WebSocket connections or real-time sync