@tpsdev-ai/flair 0.22.1 → 0.24.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/dist/cli.js +1836 -523
- package/dist/deploy.js +16 -1
- package/dist/doctor-client.js +176 -0
- package/dist/hook-install.js +324 -0
- package/dist/install/clients.js +46 -11
- package/dist/lib/auth-resolve.js +369 -0
- package/dist/lib/mcp-enable.js +787 -0
- package/dist/probe.js +15 -2
- package/dist/resources/Memory.js +98 -60
- package/dist/resources/OrgEvent.js +37 -26
- package/dist/resources/Presence.js +4 -4
- package/dist/resources/Relationship.js +52 -53
- package/dist/resources/Soul.js +16 -8
- package/dist/resources/WorkspaceState.js +58 -54
- package/dist/resources/agent-auth.js +4 -5
- package/dist/resources/auth-middleware.js +4 -4
- package/dist/resources/ed25519-auth.js +37 -0
- package/dist/resources/embeddings-boot.js +38 -1
- package/dist/resources/mcp-handler.js +29 -5
- package/dist/resources/mcp-tools.js +50 -3
- package/dist/resources/provenance.js +60 -8
- package/dist/resources/record-type-kit.js +253 -0
- package/dist/resources/record-types.js +363 -0
- package/package.json +3 -3
- package/schemas/memory.graphql +2 -2
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* record-type-kit.ts — shared, parameterized building blocks for a flair
|
|
3
|
+
* table's agent-identity / read-scope / no-forge-attribution machinery
|
|
4
|
+
* (record-types slice 1: kit extraction, flair#520).
|
|
5
|
+
*
|
|
6
|
+
* ─── What this is ────────────────────────────────────────────────────────
|
|
7
|
+
* Five flair-owned tables — Memory, Relationship, WorkspaceState, OrgEvent,
|
|
8
|
+
* Soul — each independently hand-copied ~150-250 lines of near-identical
|
|
9
|
+
* auth/scoping code: call resolveAgentAuth(getContext()), branch on the
|
|
10
|
+
* three-way verdict (internal / agent / anonymous), 404 a non-owner by-id
|
|
11
|
+
* read (never 403, so ids can't be enumerated), stamp agentId/authorId from
|
|
12
|
+
* the verified identity rather than the request body ("no-forge
|
|
13
|
+
* attribution"). Every allowRead() docstring in those five files literally
|
|
14
|
+
* says "same pattern as X.ts" — this module makes that pattern real code
|
|
15
|
+
* instead of a comment convention that can drift.
|
|
16
|
+
*
|
|
17
|
+
* ─── What this is NOT (scope discipline for slice 1) ─────────────────────
|
|
18
|
+
* This is a PURE REFACTOR extracting what is IDENTICAL across the five
|
|
19
|
+
* existing tables today. It is deliberately conservative, not the full
|
|
20
|
+
* `applyRecordTypeKit(policy)` composition sketched in the record-types
|
|
21
|
+
* design doc's §5 (that shape is for slice 2/3's registry, generating a
|
|
22
|
+
* COMPLETE {allowRead, get, search, post, put, delete} bundle for a
|
|
23
|
+
* brand-new consumer type with no bespoke logic). The five EXISTING tables
|
|
24
|
+
* genuinely diverge in real, load-bearing ways that must be preserved
|
|
25
|
+
* byte-for-byte, not papered over by a one-size bundle:
|
|
26
|
+
* - search()'s query-merge shape differs per table (Memory detaches the
|
|
27
|
+
* transaction and falls through differently on a bare query object than
|
|
28
|
+
* WorkspaceState's in-place mutation than Relationship's nested-wrap);
|
|
29
|
+
* - the no-forge attribution idiom differs not just per table but per
|
|
30
|
+
* METHOD within a table (WorkspaceState.post() unconditionally stamps
|
|
31
|
+
* with no mismatch check at all, while WorkspaceState.put() rejects a
|
|
32
|
+
* mismatch and never stamps — see stampAttribution's mode doc below);
|
|
33
|
+
* - delete() diverges the most: Memory.delete() doesn't even use
|
|
34
|
+
* resolveAgentAuth (raw tpsAgent + isAdmin(), gated on the `permanent`
|
|
35
|
+
* durability flag, no ownership check at all for non-permanent rows);
|
|
36
|
+
* Soul has no delete() override; the other three each fetch the
|
|
37
|
+
* pre-existing record with a different super.get() call signature.
|
|
38
|
+
* - OrgEvent and Soul have NO get()/search() overrides at all (org events
|
|
39
|
+
* and souls are readable by any verified agent, unscoped by owner) —
|
|
40
|
+
* this module does not force them into a scope they never had.
|
|
41
|
+
* Each of those divergences stays inline in its resource file as visible,
|
|
42
|
+
* type-specific business logic. What THIS module extracts is only the
|
|
43
|
+
* genuinely-identical primitives underneath: the auth-gate three-way
|
|
44
|
+
* dispatch, the two read-scope condition/predicate shapes, the no-forge
|
|
45
|
+
* attribution idioms (named and parameterized, not merged into one), and
|
|
46
|
+
* the canonical error-response builders. `buildProvenance` itself
|
|
47
|
+
* (./provenance.ts) is already table-agnostic and is reused as-is — this
|
|
48
|
+
* module does not wrap it, it re-exports it for a single import surface.
|
|
49
|
+
*
|
|
50
|
+
* Design lineage: flair#520 design draft (issue comment, 2026-07-13),
|
|
51
|
+
* Kern's DESIGN REVIEW ("the makeAuthGate/makeReadScope/buildProvenance
|
|
52
|
+
* as-is extraction is the right factoring... each class keeps its
|
|
53
|
+
* type-specific business logic visibly, loses only the copied
|
|
54
|
+
* boilerplate"). `stampEmbedding` (the design's 4th cut-line) is OUT of
|
|
55
|
+
* scope here — no table this module composes writes an embedding today
|
|
56
|
+
* (only Memory does, and Memory's embedding logic is dedup-gate-entangled,
|
|
57
|
+
* not part of the five-table auth/scope/provenance duplication this slice
|
|
58
|
+
* targets); it belongs to the registry/new-capability slice.
|
|
59
|
+
*/
|
|
60
|
+
import { resolveAgentAuth, allowVerified } from "./agent-auth.js";
|
|
61
|
+
import { resolveReadScope } from "./memory-read-scope.js";
|
|
62
|
+
import { buildProvenance } from "./provenance.js";
|
|
63
|
+
export { buildProvenance };
|
|
64
|
+
// ─── Canonical error responses ─────────────────────────────────────────────
|
|
65
|
+
// Byte-for-byte the same bodies every one of the five files hand-rolled
|
|
66
|
+
// (Memory/Relationship/WorkspaceState/OrgEvent each defined their own
|
|
67
|
+
// FORBIDDEN/UNAUTH/NOT_FOUND consts identically; Soul only needed
|
|
68
|
+
// FORBIDDEN/UNAUTH). Header-key casing is normalized here to "Content-Type"
|
|
69
|
+
// — HTTP header names are case-insensitive (and Harper/undici's Headers
|
|
70
|
+
// object normalizes on read), so this is not an observable behavior change
|
|
71
|
+
// from the two files (Memory.ts, Relationship.ts) whose inline copies used
|
|
72
|
+
// a lowercase "content-type" key.
|
|
73
|
+
export const FORBIDDEN = (msg) => new Response(JSON.stringify({ error: msg }), { status: 403, headers: { "Content-Type": "application/json" } });
|
|
74
|
+
export const UNAUTH = () => new Response(JSON.stringify({ error: "authentication required" }), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
75
|
+
export const NOT_FOUND = () => new Response(JSON.stringify({ error: "not found" }), { status: 404, headers: { "Content-Type": "application/json" } });
|
|
76
|
+
// ─── (a) Identity gating — makeAuthGate ────────────────────────────────────
|
|
77
|
+
/**
|
|
78
|
+
* Produces the `allowRead()` override every one of the five tables hand-
|
|
79
|
+
* wrote identically:
|
|
80
|
+
* `allowRead() { return allowVerified((this as any).getContext?.()); }`
|
|
81
|
+
* Self-authorizes now that the global gate is non-rejecting (the
|
|
82
|
+
* memory-soul-read-gate family fix): Harper routes `GET /<Table>/<id>` to
|
|
83
|
+
* get() and the collection describe (`GET /<Table>`) to a path outside
|
|
84
|
+
* search(), so neither was gated before that fix landed — closes the same
|
|
85
|
+
* P0 leak (an anonymous caller getting a 200 with full record content) for
|
|
86
|
+
* every table that composes this. Per-record/collection scoping is still
|
|
87
|
+
* each table's own get()/search() responsibility below this gate.
|
|
88
|
+
*
|
|
89
|
+
* `allowVerified()` itself (permit verified agents, admins/super_user, and
|
|
90
|
+
* trusted internal calls; deny anonymous HTTP) already lives in
|
|
91
|
+
* ./agent-auth.ts and is reused unmodified — this factory exists only to
|
|
92
|
+
* remove the identical one-line method body duplicated five times, not to
|
|
93
|
+
* change what it does.
|
|
94
|
+
*
|
|
95
|
+
* MUST be composed as a genuine prototype method, never a class-field
|
|
96
|
+
* assignment (`allowRead = makeAuthGate();`). Harper's Table.js has a code
|
|
97
|
+
* path (`relatedTable.prototype.allowRead.call(null, user, property,
|
|
98
|
+
* context)`, used when a query's `select` traverses a GraphQL relationship
|
|
99
|
+
* into one of these tables under Harper's native attribute-level RBAC) that
|
|
100
|
+
* reads `allowRead` off the CLASS PROTOTYPE directly, not off a resource
|
|
101
|
+
* instance. A class field is an own-instance property — invisible to that
|
|
102
|
+
* lookup — so a class-field assignment would silently fall back to Harper's
|
|
103
|
+
* default RBAC-based `allowRead` for that one relationship-traversal path
|
|
104
|
+
* while every direct/primary call (`this.allowRead(...)`, always
|
|
105
|
+
* instance-invoked) kept working, making the gap easy to miss. Every
|
|
106
|
+
* caller of this factory MUST wire it as `allowRead() { return
|
|
107
|
+
* gate.call(this); }` (see Memory.ts/Relationship.ts/WorkspaceState.ts/
|
|
108
|
+
* OrgEvent.ts/Soul.ts for the exact pattern), not `allowRead =
|
|
109
|
+
* makeAuthGate();`.
|
|
110
|
+
*/
|
|
111
|
+
export function makeAuthGate() {
|
|
112
|
+
return function allowRead() {
|
|
113
|
+
return allowVerified(this.getContext?.());
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export async function resolveAuthGate(ctx, denyResponse) {
|
|
117
|
+
const auth = await resolveAgentAuth(ctx);
|
|
118
|
+
if (auth.kind === "anonymous")
|
|
119
|
+
return { kind: "denied", response: denyResponse };
|
|
120
|
+
if (auth.kind === "internal" || (auth.kind === "agent" && auth.isAdmin))
|
|
121
|
+
return { kind: "unfiltered" };
|
|
122
|
+
return { kind: "scoped", agentId: auth.agentId };
|
|
123
|
+
}
|
|
124
|
+
/** owner-only: `agentId === authAgentId`, full stop — the model
|
|
125
|
+
* Relationship.ts and WorkspaceState.ts hand-rolled identically for their
|
|
126
|
+
* get()/search() overrides (`{attribute: ownerField, comparator: "equals",
|
|
127
|
+
* value: authAgentId}` / `record[ownerField] !== authAgentId`). No
|
|
128
|
+
* visibility field, no org-open exception — a real, legitimate read-scope
|
|
129
|
+
* shape in its own right (per Kern/Sherlock's review), not a lesser
|
|
130
|
+
* version of Memory's model. */
|
|
131
|
+
function ownerOnlyReadScope(ownerField) {
|
|
132
|
+
return async (authAgentId) => ({
|
|
133
|
+
condition: { attribute: ownerField, comparator: "equals", value: authAgentId },
|
|
134
|
+
isAllowed: (record) => !!record && record[ownerField] === authAgentId,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Resolve the read-scope resolver for a table, by mode:
|
|
139
|
+
* - "owner-only": own records only, scoped on `ownerField` (defaults to
|
|
140
|
+
* "agentId" — every current owner-only caller uses that field; the
|
|
141
|
+
* parameter exists so a future owner-only type using a different field,
|
|
142
|
+
* the way OrgEvent's WRITE path uses "authorId", isn't hardcoded out).
|
|
143
|
+
* - "open-within-org": delegates to the EXACT existing
|
|
144
|
+
* resolveReadScope()/PRIVATE_VISIBILITY semantics in
|
|
145
|
+
* ./memory-read-scope.ts — Memory's own module, untouched, not
|
|
146
|
+
* reimplemented here. `ownerField` is ignored for this mode:
|
|
147
|
+
* resolveReadScope() is hardcoded to "agentId" (the only table that
|
|
148
|
+
* uses this mode today), matching the task's instruction to delegate to
|
|
149
|
+
* that module "as the exact existing" implementation rather than
|
|
150
|
+
* generalizing it in this slice.
|
|
151
|
+
*
|
|
152
|
+
* The returned resolver is tagged with its own `.mode`/`.ownerField` (record-
|
|
153
|
+
* types slice 2, flair#520) — harmless own-properties on the function object,
|
|
154
|
+
* never read by any caller of the resolver itself in production. Pinned by
|
|
155
|
+
* test/unit/record-type-kit.test.ts directly (mock-free — this primitive
|
|
156
|
+
* touches no `databases` mock). The registry's own drift tripwire
|
|
157
|
+
* (test/unit/record-types-registry.test.ts) verifies the five resource files
|
|
158
|
+
* draw their kit parameters from RECORD_TYPES via a source-text scan instead
|
|
159
|
+
* of importing them (see that test file's header for why — importing
|
|
160
|
+
* multiple of Memory.ts/Relationship.ts/WorkspaceState.ts together risks a
|
|
161
|
+
* cross-file Harper-mock module-cache collision); this tagging exists so a
|
|
162
|
+
* FUTURE single-resource-file behavior test (the pattern every existing
|
|
163
|
+
* table's own read-gate test already uses in isolation) can also assert
|
|
164
|
+
* `<table>ReadScope.mode === RECORD_TYPES.<Table>.readScope` directly against
|
|
165
|
+
* a live, exported resolver without re-deriving it.
|
|
166
|
+
*/
|
|
167
|
+
export function makeReadScope(mode, ownerField = "agentId") {
|
|
168
|
+
const resolve = mode === "open-within-org"
|
|
169
|
+
? (authAgentId) => resolveReadScope(authAgentId)
|
|
170
|
+
: ownerOnlyReadScope(ownerField);
|
|
171
|
+
return Object.assign(resolve, { mode, ownerField });
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* By-id read-gate factory for get() overrides that scope a single-record
|
|
175
|
+
* read the same way search() scopes a collection read (Memory.ts,
|
|
176
|
+
* Relationship.ts, WorkspaceState.ts — NOT OrgEvent/Soul, which have no
|
|
177
|
+
* get() override at all and are intentionally left alone, see file header).
|
|
178
|
+
* Never distinguishes "doesn't exist" from "exists but not yours" — both
|
|
179
|
+
* return 404, never 403, so a denied caller can't use get() to enumerate
|
|
180
|
+
* other agents' record ids.
|
|
181
|
+
*
|
|
182
|
+
* `superGet` is a caller-supplied closure so the class's own `super.get()`
|
|
183
|
+
* (which cannot be referenced from outside the class body) stays exactly
|
|
184
|
+
* where it was.
|
|
185
|
+
*/
|
|
186
|
+
export function makeByIdReadGate(readScope) {
|
|
187
|
+
return async function byIdReadGate(target, superGet) {
|
|
188
|
+
// Collection / query reads arrive as a RequestTarget with
|
|
189
|
+
// `isCollection === true`, and are governed by search() (same owner
|
|
190
|
+
// scoping). Only a genuine by-id get is ownership-checked below.
|
|
191
|
+
if (!target || (typeof target === "object" && target.isCollection)) {
|
|
192
|
+
return this.search(target);
|
|
193
|
+
}
|
|
194
|
+
const ctx = this.getContext?.();
|
|
195
|
+
const auth = await resolveAgentAuth(ctx);
|
|
196
|
+
// Anonymous by-id read is already blocked at the allowRead() gate (403);
|
|
197
|
+
// this is defense-in-depth if get() is ever reached directly.
|
|
198
|
+
if (auth.kind === "anonymous")
|
|
199
|
+
return NOT_FOUND();
|
|
200
|
+
// Trusted internal call or admin agent — unfiltered, unchanged behavior.
|
|
201
|
+
if (auth.kind === "internal" || (auth.kind === "agent" && auth.isAdmin)) {
|
|
202
|
+
return superGet(target);
|
|
203
|
+
}
|
|
204
|
+
// Non-admin agent: scoped per the table's own read-scope model.
|
|
205
|
+
const record = await superGet(target);
|
|
206
|
+
if (!record)
|
|
207
|
+
return NOT_FOUND();
|
|
208
|
+
const scope = await readScope(auth.agentId);
|
|
209
|
+
if (!scope.isAllowed(record))
|
|
210
|
+
return NOT_FOUND();
|
|
211
|
+
return record;
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
export function stampAttribution(auth, content, field, mode, forbiddenMessage) {
|
|
215
|
+
if (auth.kind !== "agent")
|
|
216
|
+
return {}; // internal → always passthrough
|
|
217
|
+
if (auth.isAdmin) {
|
|
218
|
+
if (mode === "stamp-default")
|
|
219
|
+
content[field] ||= auth.agentId;
|
|
220
|
+
// every other mode: admin passthrough, untouched
|
|
221
|
+
return {};
|
|
222
|
+
}
|
|
223
|
+
// non-admin agent
|
|
224
|
+
switch (mode) {
|
|
225
|
+
case "validate-truthy":
|
|
226
|
+
if (content?.[field] && content[field] !== auth.agentId) {
|
|
227
|
+
return { denied: FORBIDDEN(forbiddenMessage) };
|
|
228
|
+
}
|
|
229
|
+
return {};
|
|
230
|
+
case "validate-strict":
|
|
231
|
+
if (content[field] !== auth.agentId) {
|
|
232
|
+
return { denied: FORBIDDEN(forbiddenMessage) };
|
|
233
|
+
}
|
|
234
|
+
return {};
|
|
235
|
+
case "stamp-default":
|
|
236
|
+
content[field] = auth.agentId;
|
|
237
|
+
return {};
|
|
238
|
+
case "stamp-strict":
|
|
239
|
+
if (content?.[field] && content[field] !== auth.agentId) {
|
|
240
|
+
return { denied: FORBIDDEN(forbiddenMessage) };
|
|
241
|
+
}
|
|
242
|
+
content[field] = auth.agentId;
|
|
243
|
+
return {};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// ─── (d) Provenance wiring ──────────────────────────────────────────────────
|
|
247
|
+
// buildProvenance itself is already table-agnostic (./provenance.ts) and is
|
|
248
|
+
// re-exported above, unmodified, for a single kit import surface. Per
|
|
249
|
+
// Kern/Sherlock's DESIGN REVIEW verdict (Q4): stamp the identical shape
|
|
250
|
+
// everywhere it's wired, never a table-specific format — and per this
|
|
251
|
+
// slice's explicit behavior-preservation bar, do NOT wire it onto
|
|
252
|
+
// WorkspaceState/OrgEvent, which don't stamp it today (only Memory and
|
|
253
|
+
// Relationship do). See Memory.ts/Relationship.ts for the call sites.
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* record-types.ts — the RecordType registry (record-types slice 2, flair#520).
|
|
3
|
+
*
|
|
4
|
+
* ─── What this is ────────────────────────────────────────────────────────
|
|
5
|
+
* The declared, PR-reviewed policy layer over resources/record-type-kit.ts
|
|
6
|
+
* (slice 1, flair#729): a static `RECORD_TYPES` map naming, for every
|
|
7
|
+
* flair-owned table, WHICH read-scope model it uses, WHICH no-forge
|
|
8
|
+
* attribution idiom it stamps on writes, whether it stamps provenance,
|
|
9
|
+
* whether it carries an embedding column, and whether it participates in
|
|
10
|
+
* federation — the five capabilities the flair#520 design draft's §4 laid
|
|
11
|
+
* out (identity / read-scope / provenance / semantic recall / REM), plus
|
|
12
|
+
* the attribution split slice 1 surfaced (four named idioms, not one) and
|
|
13
|
+
* the federation/MCP-shape refinements Kern and Sherlock's DESIGN REVIEW
|
|
14
|
+
* landed on the issue thread.
|
|
15
|
+
*
|
|
16
|
+
* This module is DATA, not a framework: `RecordTypePolicy` is a plain
|
|
17
|
+
* interface, `RECORD_TYPES` is a plain object literal. It does not derive
|
|
18
|
+
* MCP tools (that is slice 3 — see the `mcp` field's own doc below for why
|
|
19
|
+
* its SHAPE lands now without any consumer reading it yet), does not wire
|
|
20
|
+
* embeddings (Memory's embedding logic stays exactly where slice 1 left it
|
|
21
|
+
* — dedup-gate-entangled inline code in resources/Memory.ts, not routed
|
|
22
|
+
* through this registry), and does not change what Federation.ts/src/cli.ts
|
|
23
|
+
* actually sync (their hardcoded table lists are untouched — see the
|
|
24
|
+
* `federation` field's doc). Every field on every one of the five entries
|
|
25
|
+
* below documents CURRENT shipped behavior; none of them CHANGE it.
|
|
26
|
+
*
|
|
27
|
+
* ─── The static-registry invariant (non-negotiable) ───────────────────────
|
|
28
|
+
* Per §3 of the flair#520 design draft and Sherlock/Kern's unanimous
|
|
29
|
+
* verdict: this registry is STATIC, COMPILED, and PR-REVIEWED — never
|
|
30
|
+
* runtime-populated. A new entry lands via a PR to THIS file, same trust
|
|
31
|
+
* level as an entry in resources/mcp-tools.ts's TOOLS map. No component,
|
|
32
|
+
* consumer, or plugin can add itself to `RECORD_TYPES` at boot. This is the
|
|
33
|
+
* structural rejection of the exact failure mode Sherlock's review closed
|
|
34
|
+
* for cause on flair#541: Harper's native-MCP application profile
|
|
35
|
+
* auto-exposing every resource as a tool, with no allow/deny, at runtime.
|
|
36
|
+
* A dynamic `RECORD_TYPES` would reproduce that failure one layer up. If
|
|
37
|
+
* this ever needs to become an out-of-tree-extensible SDK (flair#520 §9
|
|
38
|
+
* calls that "a plausible later shape, not v1"), the review bar for that
|
|
39
|
+
* relaxation must be at least as high as the original #520/#541 decision.
|
|
40
|
+
*
|
|
41
|
+
* ─── Field-by-field rationale ─────────────────────────────────────────────
|
|
42
|
+
*
|
|
43
|
+
* `identity` — always "gated" for all five entries today. `makeAuthGate()`
|
|
44
|
+
* (record-type-kit.ts) only implements the "gated" posture (resolveAgentAuth
|
|
45
|
+
* + allowVerified — verified agents, admins, and trusted internal calls
|
|
46
|
+
* pass; anonymous HTTP is denied). "internal-only" is reserved for a future
|
|
47
|
+
* system table with no agent-facing surface at all — nothing wires it yet;
|
|
48
|
+
* a table setting it today would be a registry error, not a live capability.
|
|
49
|
+
*
|
|
50
|
+
* `readScope` — three values, not the two the original §4 draft proposed.
|
|
51
|
+
* "owner-only" and "open-within-org" are the two K&S-approved models
|
|
52
|
+
* (Relationship/WorkspaceState vs. Memory). The THIRD value, "none", names
|
|
53
|
+
* a real state that already ships and that record-type-kit.ts's own file
|
|
54
|
+
* header calls out explicitly: OrgEvent and Soul have NO get()/search()
|
|
55
|
+
* override at all — any verified agent (past the identity gate) reads
|
|
56
|
+
* every row, unscoped by owner, with no visibility field in play. That is
|
|
57
|
+
* neither "owner-only" (it is strictly broader — no ownership filter at
|
|
58
|
+
* all) nor "open-within-org" (there is no visibility-based private
|
|
59
|
+
* exception — nothing is excluded). Labeling OrgEvent/Soul "owner-only" to
|
|
60
|
+
* force them into the K&S-approved binary would be a FALSE registry entry —
|
|
61
|
+
* exactly the kind of drift a policy registry exists to prevent, and worse
|
|
62
|
+
* than just leaving the gap undocumented. This is a deliberate, disclosed
|
|
63
|
+
* extension of the flair#520 design draft's §4 shape (flagged in the
|
|
64
|
+
* slice-2 report for K&S to weigh in on), not a silent reinterpretation of
|
|
65
|
+
* their "owner-only | open-within-org" verdict.
|
|
66
|
+
*
|
|
67
|
+
* `readScope` narrowing is a BREAKING CHANGE, not covered by the
|
|
68
|
+
* additive-only discipline the design draft's §8 states for every other
|
|
69
|
+
* field. Per Sherlock's DESIGN REVIEW comment (explicitly concurred by
|
|
70
|
+
* Kern): flipping `open-within-org` → `owner-only`, or `none` → either
|
|
71
|
+
* narrower model, would retroactively hide rows other agents could already
|
|
72
|
+
* read — the same class of scrutiny as any other access-narrowing decision,
|
|
73
|
+
* called out separately here because it is easy to mis-file as "just
|
|
74
|
+
* editing a registry constant." WIDENING (owner-only → open-within-org, or
|
|
75
|
+
* either → none) is a normal, reviewable capability-addition PR. NARROWING
|
|
76
|
+
* requires the review bar of a genuine access-removal change: confirm
|
|
77
|
+
* nothing currently depends on the wider read, and treat it with the same
|
|
78
|
+
* weight as shipping a new private-visibility default.
|
|
79
|
+
*
|
|
80
|
+
* Review-gate tiering (Kern/Sherlock DESIGN REVIEW, Q1): a new entry with
|
|
81
|
+
* `readScope: "owner-only"` ships on the standard architecture-review bar.
|
|
82
|
+
* `readScope: "open-within-org"` (or, by the same logic, `"none"` — an even
|
|
83
|
+
* wider grant than open-within-org for records with no visibility concept
|
|
84
|
+
* at all) requires Sherlock's explicit security sign-off on the read-scope
|
|
85
|
+
* decision specifically, the same scrutiny Memory's own visibility work
|
|
86
|
+
* got. This registry's job is to make that decision a single reviewable
|
|
87
|
+
* line per type; the review-bar tiering itself is a process discipline for
|
|
88
|
+
* whoever reviews the PR, not something this module enforces in code.
|
|
89
|
+
*
|
|
90
|
+
* `attribution` — the four named `AttributionMode` idioms from
|
|
91
|
+
* record-type-kit.ts (`validate-truthy` / `validate-strict` /
|
|
92
|
+
* `stamp-default` / `stamp-strict`), keyed per WRITE METHOD
|
|
93
|
+
* (`post`/`put`), not one mode per type. This is the concrete thing slice 1
|
|
94
|
+
* surfaced that the original §4 draft did not anticipate: WorkspaceState
|
|
95
|
+
* and OrgEvent use a DIFFERENT idiom on post() (`stamp-default` — silent
|
|
96
|
+
* unconditional overwrite, never rejects) than on put()
|
|
97
|
+
* (`validate-strict` — rejects a mismatch, including an absent field).
|
|
98
|
+
* Collapsing that to one type-level `attributionMode` field would either be
|
|
99
|
+
* wrong for one of the two methods or would require silently converging
|
|
100
|
+
* post() onto put()'s stricter idiom — a real behavior change flagged but
|
|
101
|
+
* explicitly deferred by Flint's post-slice-1 comment on #520 ("Sherlock
|
|
102
|
+
* flagging stamp-default → stamp-strict convergence... Registry slice
|
|
103
|
+
* follows" — convergence is a FUTURE decision, not made here). Each
|
|
104
|
+
* sub-field is optional because not every table's write path calls
|
|
105
|
+
* `stampAttribution` on both methods: Relationship has no post() override
|
|
106
|
+
* at all (only `put`), so `attribution.post` is absent, not a placeholder.
|
|
107
|
+
* The stamped attribute name is always `ownerField` above — no type uses a
|
|
108
|
+
* different field for attribution vs. ownership scoping.
|
|
109
|
+
*
|
|
110
|
+
* `provenance` — true only for Memory and Relationship (the two tables that
|
|
111
|
+
* call `buildProvenance()` today and carry a nullable `provenance: String`
|
|
112
|
+
* schema field). WorkspaceState/OrgEvent/Soul are `false`: not an oversight
|
|
113
|
+
* — nobody wired it, and none of the three declare the schema field, so
|
|
114
|
+
* flipping this to `true` without a schema migration would be a lie the
|
|
115
|
+
* registry tells about what actually gets stamped.
|
|
116
|
+
*
|
|
117
|
+
* `embedding` — set only for Memory (`content` is the embedded field; the
|
|
118
|
+
* type has its own exposed semantic-search tool today — `memory_search` /
|
|
119
|
+
* resources/SemanticSearch.ts — hence `exposedSearch: true`). Declared for
|
|
120
|
+
* documentation ONLY in this slice: Memory's actual embedding logic stays
|
|
121
|
+
* exactly where slice 1's kit-extraction doc said it would stay — inline in
|
|
122
|
+
* resources/Memory.ts, dedup-gate-entangled, NOT routed through this
|
|
123
|
+
* registry or through a `stampEmbedding()` kit helper. Wiring is a later
|
|
124
|
+
* slice's job; this field exists so that slice doesn't need a breaking
|
|
125
|
+
* schema change to the policy shape when it lands (same reserved-flag
|
|
126
|
+
* pattern as `remEligible`).
|
|
127
|
+
*
|
|
128
|
+
* `remEligible` — MUST be `false` for every entry in v1 (typed as the
|
|
129
|
+
* literal `false`, not `boolean`, so a stray `true` is a compile error, not
|
|
130
|
+
* just a runtime policy mistake). REM's nightly gather step
|
|
131
|
+
* (specs/FLAIR-NIGHTLY-REM.md §11) is hardcoded to `GET /Memory?agentId=…`
|
|
132
|
+
* — single table, single agent, by construction. There is no multi-table
|
|
133
|
+
* distillation input path to opt into yet; the field exists so a future
|
|
134
|
+
* REM generalization doesn't require a breaking schema change to every
|
|
135
|
+
* existing entry.
|
|
136
|
+
*
|
|
137
|
+
* `federation` — "included" or "excluded", naming whether the type's rows
|
|
138
|
+
* currently CAN leave this instance via federation sync at all. This is
|
|
139
|
+
* DECLARATIVE ONLY in this slice, same discipline as `embedding` above:
|
|
140
|
+
* Federation.ts's `FederationSync.post()` table map and src/cli.ts's
|
|
141
|
+
* `runFederationSyncOnce`'s push table list are the actual, unchanged,
|
|
142
|
+
* hardcoded source of truth (`["Memory", "Soul", "Agent", "Relationship"]`)
|
|
143
|
+
* — this field documents what that hardcoded list already does per type,
|
|
144
|
+
* it does not drive it. Memory/Relationship/Soul are "included" because
|
|
145
|
+
* they are already in both lists (Memory's push additionally excludes
|
|
146
|
+
* `private`-visibility rows via classifyRecord — a finer-grained rule this
|
|
147
|
+
* boolean-ish field does not attempt to express). WorkspaceState/OrgEvent
|
|
148
|
+
* are "excluded" because they are in neither list today — org events and
|
|
149
|
+
* workspace state never leave the instance.
|
|
150
|
+
*
|
|
151
|
+
* For any FUTURE (non-core, consumer-defined) type, `federation` MUST be
|
|
152
|
+
* `"excluded"` — per Sherlock's DESIGN REVIEW (Q6, Kern concurring):
|
|
153
|
+
* DESIGN.md sequenced Memory's own open-within-org read AFTER federation
|
|
154
|
+
* hardening specifically so within-instance openness wouldn't outrun the
|
|
155
|
+
* one hard cross-instance boundary it depends on. A new type's registration
|
|
156
|
+
* must not silently inherit whatever Federation.ts's push filter does or
|
|
157
|
+
* doesn't already exclude — the filter does not know about new tables at
|
|
158
|
+
* all. Opting a new type into federation requires updating that filter
|
|
159
|
+
* FIRST, in its own reviewed PR, never the other order. This registry does
|
|
160
|
+
* not enforce that sequencing in code (there is no derivation from this
|
|
161
|
+
* field yet — see above); it is a review-time invariant for whoever adds
|
|
162
|
+
* the next entry.
|
|
163
|
+
*
|
|
164
|
+
* `mcp` — DECLARED AND ENFORCED as of slice 3 (flair#520 design review
|
|
165
|
+
* round 2, issue comment 2026-07-18 — Kern APPROVE all four asks, Sherlock
|
|
166
|
+
* APPROVE with one refinement, both unanimous). This field is the reviewed
|
|
167
|
+
* DECLARATION of the MCP surface, not a runtime generator: resources/
|
|
168
|
+
* mcp-tools.ts's hand-written `TOOLS` map stays the actual dispatch table.
|
|
169
|
+
* The slice-3 design round audited all 12 shipped tools and found only 5
|
|
170
|
+
* are simple table-verb wrappers (memory_get/store/delete, soul_get/set —
|
|
171
|
+
* and even the soul pair does bespoke `agentId:key` id synthesis); the rest
|
|
172
|
+
* are composite or bespoke (bootstrap, attention, memory_search,
|
|
173
|
+
* memory_update, record_usage) and cannot be generated from a registry
|
|
174
|
+
* entry without either losing schema/behavior specifics or duplicating the
|
|
175
|
+
* handler. Generating the 5 simple tools would also touch the
|
|
176
|
+
* security-critical `/mcp` dispatch path for zero behavior change, and
|
|
177
|
+
* runtime derivation from a registry is structurally the same failure
|
|
178
|
+
* flair#541 closed for cause (Harper's native-MCP auto-exposing every
|
|
179
|
+
* resource as a tool) one layer up. What declare-and-enforce buys instead:
|
|
180
|
+
* any PR that adds or removes an MCP tool must now also touch a policy
|
|
181
|
+
* chokepoint — either a `RECORD_TYPES.<Table>.mcp` declaration (table-verb
|
|
182
|
+
* tools) or the `COMPOSITE_MCP_TOOLS` allowlist below (everything else) —
|
|
183
|
+
* enforced bidirectionally by test/unit/mcp-surface-tripwire.test.ts, or CI
|
|
184
|
+
* fails. Declaring a verb here does not create a tool; it documents (and,
|
|
185
|
+
* via the tripwire, locks in) a tool resources/mcp-tools.ts already
|
|
186
|
+
* implements.
|
|
187
|
+
*
|
|
188
|
+
* Per Kern's DESIGN REVIEW refinement (Sherlock's Q5, Kern concurring): NOT
|
|
189
|
+
* a flat `verbs` array. `readVerbs` (get/search — same review bar as adding
|
|
190
|
+
* the type itself, since they exercise the same `readScope` the REST
|
|
191
|
+
* surface already exposes) is structurally separate from `writeVerbs`
|
|
192
|
+
* (store/delete/update — a write/delete/update surface over MCP, requiring
|
|
193
|
+
* its own explicit line-item sign-off). `"update"` was added to the
|
|
194
|
+
* `writeVerbs` union in slice 3 — APPROVED by both Kern and Sherlock as
|
|
195
|
+
* documenting `memory_update`'s already-shipped two-branch
|
|
196
|
+
* read-modify-write (in-place overwrite, or a `supersedes`-linked new
|
|
197
|
+
* version), not a new capability.
|
|
198
|
+
*
|
|
199
|
+
* Backfilled on FOUR of the five core entries below, documenting the
|
|
200
|
+
* CURRENT shipped surface exactly (slice-2 discipline: registration, not
|
|
201
|
+
* behavior change) — Memory (`get`/`search` reads; `store`/`delete`/
|
|
202
|
+
* `update` writes), Soul (`get` read; `store` write), WorkspaceState (no
|
|
203
|
+
* reads; `store` write), OrgEvent (no reads; `store` write). Relationship
|
|
204
|
+
* stays `mcp`-absent: it has no MCP tool today, and absent means no
|
|
205
|
+
* exposure, exactly as slice 2 defined.
|
|
206
|
+
*
|
|
207
|
+
* Verb→tool-name mapping (default `${toolPrefix}_${verb}`, with three
|
|
208
|
+
* overrides — `(Soul, store)` → `soul_set`, `(WorkspaceState, store)` →
|
|
209
|
+
* `flair_workspace_set`, `(OrgEvent, store)` → `flair_orgevent`) lives in
|
|
210
|
+
* resources/mcp-tools.ts's `TOOL_NAME_OVERRIDES`, not here. Per Kern/
|
|
211
|
+
* Sherlock's unanimous verdict: the registry declares WHAT is exposed
|
|
212
|
+
* (capability); mcp-tools.ts owns HOW (names, defaults, routing, input
|
|
213
|
+
* schema). Keeping presentation out of this file is what keeps
|
|
214
|
+
* `RecordTypeMcp` a pure capability declaration.
|
|
215
|
+
*
|
|
216
|
+
* See `COMPOSITE_MCP_TOOLS` below for the second — and only other —
|
|
217
|
+
* reviewed chokepoint: tools that are cross-table, aggregate, or otherwise
|
|
218
|
+
* cannot map to a single table + verb (`bootstrap`, `attention`,
|
|
219
|
+
* `record_usage`). Sherlock's slice-3 refinement (Kern concurring on the
|
|
220
|
+
* relocation) put that allowlist HERE, in record-types.ts, rather than in
|
|
221
|
+
* mcp-tools.ts — so the FULL MCP surface (table-verb tools and composites
|
|
222
|
+
* alike) is reviewable in this one file, not split across two.
|
|
223
|
+
*
|
|
224
|
+
* Design lineage: flair#520 design draft (issue comment, 2026-07-13) §4;
|
|
225
|
+
* Sherlock's Security Review (readScope narrowing, mandatory-vs-optional
|
|
226
|
+
* content-safety — NOT added here per Kern's Q3 verdict below —, structural
|
|
227
|
+
* readVerbs/writeVerbs split, federation-excluded default); Kern's DESIGN
|
|
228
|
+
* REVIEW (tiered review gate, readVerbs/writeVerbs, federation-excluded
|
|
229
|
+
* default, provenance-stamp-identical-shape); slice 1 kit extraction
|
|
230
|
+
* (flair#729, merged) whose file header first named the four attribution
|
|
231
|
+
* idioms and the OrgEvent/Soul unscoped-read state this module's `readScope`
|
|
232
|
+
* type makes explicit.
|
|
233
|
+
*
|
|
234
|
+
* Deliberately NOT added in this slice (Kern's DESIGN REVIEW Q3 verdict,
|
|
235
|
+
* explicit): a `contentSafety`/`scanFields` field. Sherlock proposed making
|
|
236
|
+
* content-safety scanning mandatory for any free-text field; Kern's verdict
|
|
237
|
+
* (which this implementation follows, per the task's explicit slice-2 scope)
|
|
238
|
+
* was NOT mandatory in v1 — generalizing `scanFields()` beyond Memory's
|
|
239
|
+
* hand-wired `content`/`summary` call needs its own design pass, and making
|
|
240
|
+
* it a blocking registration requirement would slow the easy win (data-layer
|
|
241
|
+
* -only registration with no MCP exposure). Left as a documented follow-up,
|
|
242
|
+
* not a silently-dropped field.
|
|
243
|
+
*/
|
|
244
|
+
// ─── The registry ───────────────────────────────────────────────────────────
|
|
245
|
+
//
|
|
246
|
+
// `as const satisfies Record<string, RecordTypePolicy>`: `satisfies` checks
|
|
247
|
+
// every entry against `RecordTypePolicy` (catching an invalid readScope
|
|
248
|
+
// value, a missing required field, etc. at compile time) WITHOUT widening
|
|
249
|
+
// the expression's inferred type the way a `: Record<string,
|
|
250
|
+
// RecordTypePolicy>` annotation would — so `RECORD_TYPES.Memory.readScope`
|
|
251
|
+
// keeps its literal type `"open-within-org"`, not the widened union
|
|
252
|
+
// `RecordTypeReadScopeMode`. That literal-preservation is what lets the five
|
|
253
|
+
// resource classes below pass `RECORD_TYPES.<Table>.<field>` straight into
|
|
254
|
+
// `makeReadScope()`/`stampAttribution()` (which expect the narrow literal
|
|
255
|
+
// unions, not the wide ones) with no runtime cast or non-null assertion.
|
|
256
|
+
export const RECORD_TYPES = {
|
|
257
|
+
Memory: {
|
|
258
|
+
table: "Memory",
|
|
259
|
+
ownerField: "agentId",
|
|
260
|
+
identity: "gated",
|
|
261
|
+
readScope: "open-within-org",
|
|
262
|
+
attribution: { post: "validate-truthy", put: "validate-truthy" },
|
|
263
|
+
provenance: true,
|
|
264
|
+
embedding: { field: "content", exposedSearch: true },
|
|
265
|
+
remEligible: false,
|
|
266
|
+
federation: "included",
|
|
267
|
+
mcp: { toolPrefix: "memory", readVerbs: ["get", "search"], writeVerbs: ["store", "delete", "update"] },
|
|
268
|
+
},
|
|
269
|
+
Relationship: {
|
|
270
|
+
table: "Relationship",
|
|
271
|
+
ownerField: "agentId",
|
|
272
|
+
identity: "gated",
|
|
273
|
+
readScope: "owner-only",
|
|
274
|
+
// No `post` — Relationship.ts has no post() override at all; only
|
|
275
|
+
// put() (upsert) calls stampAttribution.
|
|
276
|
+
attribution: { put: "stamp-strict" },
|
|
277
|
+
provenance: true,
|
|
278
|
+
remEligible: false,
|
|
279
|
+
federation: "included",
|
|
280
|
+
},
|
|
281
|
+
WorkspaceState: {
|
|
282
|
+
table: "WorkspaceState",
|
|
283
|
+
ownerField: "agentId",
|
|
284
|
+
identity: "gated",
|
|
285
|
+
readScope: "owner-only",
|
|
286
|
+
attribution: { post: "stamp-default", put: "validate-strict" },
|
|
287
|
+
provenance: false,
|
|
288
|
+
remEligible: false,
|
|
289
|
+
federation: "excluded",
|
|
290
|
+
mcp: { toolPrefix: "flair_workspace", readVerbs: [], writeVerbs: ["store"] },
|
|
291
|
+
},
|
|
292
|
+
OrgEvent: {
|
|
293
|
+
table: "OrgEvent",
|
|
294
|
+
ownerField: "authorId",
|
|
295
|
+
identity: "gated",
|
|
296
|
+
// No get()/search() override — any verified agent reads every org
|
|
297
|
+
// event, unscoped. See this module's header doc's `readScope` section.
|
|
298
|
+
readScope: "none",
|
|
299
|
+
attribution: { post: "stamp-default", put: "validate-strict" },
|
|
300
|
+
provenance: false,
|
|
301
|
+
remEligible: false,
|
|
302
|
+
federation: "excluded",
|
|
303
|
+
mcp: { toolPrefix: "flair_orgevent", readVerbs: [], writeVerbs: ["store"] },
|
|
304
|
+
},
|
|
305
|
+
Soul: {
|
|
306
|
+
table: "Soul",
|
|
307
|
+
ownerField: "agentId",
|
|
308
|
+
identity: "gated",
|
|
309
|
+
// No get()/search() override — souls are identity/discovery data,
|
|
310
|
+
// intentionally readable by any verified agent. See header doc.
|
|
311
|
+
readScope: "none",
|
|
312
|
+
attribution: { post: "validate-truthy", put: "validate-truthy" },
|
|
313
|
+
provenance: false,
|
|
314
|
+
remEligible: false,
|
|
315
|
+
federation: "included",
|
|
316
|
+
mcp: { toolPrefix: "soul", readVerbs: ["get"], writeVerbs: ["store"] },
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
// ─── Composite MCP tools (the second, and only other, reviewed chokepoint) ─
|
|
320
|
+
//
|
|
321
|
+
// Tools that do not map to a single table + verb — cross-table aggregates,
|
|
322
|
+
// bespoke resources, or multi-source composites. These cannot be expressed
|
|
323
|
+
// via `RECORD_TYPES.<Table>.mcp` (there is no single `table` to attach them
|
|
324
|
+
// to), so slice 3's design round (Sherlock's refinement, Kern concurring —
|
|
325
|
+
// see the `mcp` header doc above) makes this array the SECOND reviewed
|
|
326
|
+
// chokepoint for the MCP surface: test/unit/mcp-surface-tripwire.test.ts
|
|
327
|
+
// requires every tool name in resources/mcp-tools.ts's `TOOLS` map to be
|
|
328
|
+
// either derived from a declared `RECORD_TYPES.<Table>.mcp` verb OR listed
|
|
329
|
+
// here. A PR adding a new composite MCP tool (or renaming/removing one of
|
|
330
|
+
// these three) must touch this array — same review bar as touching a
|
|
331
|
+
// table's `mcp` field.
|
|
332
|
+
//
|
|
333
|
+
// bootstrap — Soul + Memory + predicted-context composite (BootstrapMemories)
|
|
334
|
+
// attention — cross-table aggregate query (AttentionQuery.ts), not a table verb
|
|
335
|
+
// record_usage — usage-signal resource (RecordUsage.ts), not a table verb
|
|
336
|
+
//
|
|
337
|
+
export const COMPOSITE_MCP_TOOLS = ["bootstrap", "attention", "record_usage"];
|
|
338
|
+
// ─── Runtime immutability ───────────────────────────────────────────────────
|
|
339
|
+
// Belt-and-suspenders backstop for the "static, compiled" invariant (see
|
|
340
|
+
// header doc). TypeScript's `as const satisfies` already gives compile-time
|
|
341
|
+
// readonly types, but that alone does not stop a runtime consumer (a
|
|
342
|
+
// resource file, a test, a hypothetical future dynamic-registration attempt)
|
|
343
|
+
// from mutating a nested object in place — `RECORD_TYPES.Memory.readScope =
|
|
344
|
+
// "owner-only"` is a type error at compile time but would silently SUCCEED
|
|
345
|
+
// at runtime without this. Deep-freezing every entry and every nested
|
|
346
|
+
// sub-object (attribution/embedding/mcp) makes an attempted mutation throw
|
|
347
|
+
// (every .ts file in this repo compiles as an ES module, which is always
|
|
348
|
+
// strict mode) instead of silently desyncing the registry from what a
|
|
349
|
+
// resource file already composed at its own module-load time. Same
|
|
350
|
+
// treatment for `COMPOSITE_MCP_TOOLS` — it is the second reviewed MCP-surface
|
|
351
|
+
// chokepoint (see its own doc above) and gets the same static-registry
|
|
352
|
+
// guarantee as RECORD_TYPES itself.
|
|
353
|
+
function deepFreeze(value) {
|
|
354
|
+
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
355
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
356
|
+
deepFreeze(value[key]);
|
|
357
|
+
}
|
|
358
|
+
Object.freeze(value);
|
|
359
|
+
}
|
|
360
|
+
return value;
|
|
361
|
+
}
|
|
362
|
+
deepFreeze(RECORD_TYPES);
|
|
363
|
+
deepFreeze(COMPOSITE_MCP_TOOLS);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/flair",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Identity, memory, and soul for AI agents. Cryptographic identity (Ed25519), semantic memory with local embeddings, and persistent personality — all in a single process.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -55,10 +55,10 @@
|
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@harperfast/harper": "5.1.17",
|
|
58
|
-
"@harperfast/oauth": "2.
|
|
58
|
+
"@harperfast/oauth": "2.4.0",
|
|
59
59
|
"@types/js-yaml": "4.0.9",
|
|
60
60
|
"commander": "14.0.3",
|
|
61
|
-
"harper-fabric-embeddings": "^0.
|
|
61
|
+
"harper-fabric-embeddings": "^0.5.0",
|
|
62
62
|
"jose": "6.2.2",
|
|
63
63
|
"js-yaml": "4.1.1",
|
|
64
64
|
"node-llama-cpp": "3.18.1",
|
package/schemas/memory.graphql
CHANGED
|
@@ -53,7 +53,7 @@ type Memory @table(database: "flair") {
|
|
|
53
53
|
validFrom: String @indexed # ISO timestamp — when this fact became true
|
|
54
54
|
validTo: String @indexed # ISO timestamp — when this fact stopped being true (null = still valid)
|
|
55
55
|
_safetyFlags: [String] # content safety flags from scanContent()
|
|
56
|
-
provenance: String # JSON blob (memory-provenance slice 1): { v, verified: { agentId, timestamp }, claimed?: { model } }
|
|
56
|
+
provenance: String # JSON blob (memory-provenance slice 1; claimed.client added by flair#718): { v, verified: { agentId, timestamp }, claimed?: { model, client } }
|
|
57
57
|
# Nullable by design — existing rows read back provenance = null, unchanged behavior (clean-upgrade-path gate).
|
|
58
58
|
# Same idiom as Soul.metadata below. Stamped server-side in resources/Memory.ts; never client-writable.
|
|
59
59
|
originatorInstanceId: String @indexed # federation-edge-hardening slice 1: WRITE-TIME instance identity, stamped server-side in
|
|
@@ -99,7 +99,7 @@ type Relationship @table(database: "flair") {
|
|
|
99
99
|
# for the full contract (write-time stamp, nullable, preserved across sync merges).
|
|
100
100
|
# Stamped server-side in resources/Relationship.ts's put().
|
|
101
101
|
provenance: String # JSON blob (relationship-write-path, folded K&S refinement): SAME shape as
|
|
102
|
-
# Memory.provenance above — { v, verified: { agentId, timestamp }, claimed?: { model } }
|
|
102
|
+
# Memory.provenance above — { v, verified: { agentId, timestamp }, claimed?: { model, client } }
|
|
103
103
|
# — via the shared resources/provenance.ts buildProvenance(), never a
|
|
104
104
|
# Relationship-specific format. Nullable by design — pre-existing rows read back
|
|
105
105
|
# provenance = null, unchanged behavior (migration-equivalence gate, same
|