@tpsdev-ai/flair 0.51.1 → 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.
- package/README.md +10 -5
- package/dist/build-info.json +3 -3
- package/dist/cli.js +575 -547
- package/dist/doctor-client.js +35 -0
- package/dist/hook-install.js +74 -0
- package/dist/install/global-bin-path.js +14 -0
- package/dist/lib/auth-resolve.js +15 -0
- package/dist/lib/doctor-run.js +28 -15
- package/dist/lib/upgrade-exec-path.js +257 -0
- package/dist/lib/upgrade-plain-tree.js +558 -0
- package/dist/rem/promote-policy.js +204 -0
- package/dist/rem/restore.js +55 -15
- package/dist/rem/runner.js +203 -20
- package/dist/resources/AdminMemory.js +2 -1
- package/dist/resources/AgentSeed.js +26 -10
- package/dist/resources/Asset.js +203 -0
- package/dist/resources/AutoPromoteCandidates.js +2 -4
- package/dist/resources/Credential.js +14 -0
- package/dist/resources/Federation.js +80 -0
- package/dist/resources/Integration.js +12 -0
- package/dist/resources/Memory.js +158 -60
- package/dist/resources/MemoryBootstrap.js +63 -20
- package/dist/resources/MemoryCandidate.js +12 -0
- package/dist/resources/MemoryConsolidate.js +2 -1
- package/dist/resources/MemoryDedupStats.js +17 -2
- package/dist/resources/MemoryFeed.js +30 -0
- package/dist/resources/MemoryGrant.js +14 -0
- package/dist/resources/MemoryReflect.js +75 -17
- package/dist/resources/Message.js +190 -0
- package/dist/resources/OrgEvent.js +12 -0
- package/dist/resources/PromoteMemoryCandidate.js +76 -0
- package/dist/resources/RecordUsage.js +1 -1
- package/dist/resources/Relationship.js +12 -0
- package/dist/resources/SemanticSearch.js +45 -13
- package/dist/resources/Soul.js +54 -18
- package/dist/resources/WorkspaceState.js +12 -0
- package/dist/resources/auth-middleware.js +17 -44
- package/dist/resources/authority-field-guard.js +37 -0
- package/dist/resources/bm25-index-service.js +1 -1
- package/dist/resources/bm25-index.js +50 -11
- package/dist/resources/embedding-space-guard.js +238 -0
- package/dist/resources/embeddings-provider.js +32 -5
- package/dist/resources/federation-classify.js +23 -1
- package/dist/resources/health.js +11 -2
- package/dist/resources/hit-tracking.js +244 -0
- package/dist/resources/mcp-tools.js +272 -7
- package/dist/resources/memory-reflect-lib.js +111 -0
- package/dist/resources/migrations/embedding-stamp.js +22 -4
- package/dist/resources/owner-field-guard.js +62 -0
- package/dist/resources/promotion-stamp.js +29 -0
- package/dist/resources/record-owner-guard.js +71 -5
- package/dist/resources/record-types.js +30 -7
- package/dist/resources/relay-lib.js +205 -0
- package/dist/resources/relay-ops.js +294 -0
- package/dist/resources/skill-write.js +120 -0
- package/dist/resources/soul-adk-guard.js +68 -0
- package/dist/resources/soul-write-policy.js +63 -0
- package/dist/resources/table-helpers.js +2 -0
- package/dist/resources/usage-recording.js +3 -3
- package/dist/src/rem/promote-policy.js +204 -0
- package/docs/api-reference.md +374 -0
- package/docs/auth.md +52 -0
- package/docs/federation.md +4 -0
- package/docs/integrations.md +6 -6
- package/docs/mcp-clients.md +16 -1
- package/docs/releasing.md +11 -8
- package/docs/rem.md +20 -2
- package/docs/upgrade.md +47 -2
- package/package.json +13 -8
- package/schemas/memory.graphql +51 -2
- 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.
|
package/docs/federation.md
CHANGED
|
@@ -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
|