@tpsdev-ai/flair 0.21.0 → 0.22.1
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 -7
- package/SECURITY.md +24 -2
- package/config.yaml +32 -5
- package/dist/cli.js +1811 -221
- package/dist/deploy.js +3 -4
- package/dist/fleet-presence.js +98 -0
- package/dist/fleet-verify.js +291 -0
- package/dist/mcp-client-assertion.js +364 -0
- package/dist/probe.js +97 -0
- package/dist/rem/runner.js +82 -12
- package/dist/resources/AttentionQuery.js +356 -0
- package/dist/resources/Credential.js +11 -1
- package/dist/resources/Federation.js +23 -0
- package/dist/resources/MCPClientMetadata.js +88 -0
- package/dist/resources/Memory.js +52 -53
- package/dist/resources/MemoryBootstrap.js +422 -76
- package/dist/resources/MemoryReflect.js +105 -49
- package/dist/resources/MemoryUsage.js +104 -0
- package/dist/resources/OAuth.js +8 -1
- package/dist/resources/OrgEvent.js +11 -0
- package/dist/resources/Presence.js +218 -19
- package/dist/resources/RecordUsage.js +230 -0
- package/dist/resources/Relationship.js +98 -25
- package/dist/resources/SemanticSearch.js +85 -317
- package/dist/resources/SkillScan.js +15 -0
- package/dist/resources/WorkspaceState.js +11 -0
- package/dist/resources/agent-auth.js +60 -2
- package/dist/resources/auth-middleware.js +18 -9
- package/dist/resources/bm25.js +12 -6
- package/dist/resources/collision-lib.js +157 -0
- package/dist/resources/embeddings-boot.js +149 -0
- package/dist/resources/embeddings-provider.js +364 -106
- package/dist/resources/entity-vocab.js +139 -0
- package/dist/resources/health.js +97 -6
- package/dist/resources/mcp-client-metadata-fields.js +112 -0
- package/dist/resources/mcp-handler.js +1 -1
- package/dist/resources/mcp-tools.js +88 -10
- package/dist/resources/memory-reflect-lib.js +289 -0
- package/dist/resources/migration-boot.js +143 -0
- package/dist/resources/migrations/dir-safety.js +71 -0
- package/dist/resources/migrations/embedding-stamp.js +178 -0
- package/dist/resources/migrations/envelope.js +43 -0
- package/dist/resources/migrations/export.js +38 -0
- package/dist/resources/migrations/ledger.js +55 -0
- package/dist/resources/migrations/lock.js +154 -0
- package/dist/resources/migrations/progress.js +31 -0
- package/dist/resources/migrations/registry.js +36 -0
- package/dist/resources/migrations/risk-policy.js +23 -0
- package/dist/resources/migrations/runner.js +455 -0
- package/dist/resources/migrations/snapshot.js +127 -0
- package/dist/resources/migrations/source-fields.js +93 -0
- package/dist/resources/migrations/space.js +167 -0
- package/dist/resources/migrations/state.js +64 -0
- package/dist/resources/migrations/status.js +39 -0
- package/dist/resources/migrations/synthetic-test-migration.js +93 -0
- package/dist/resources/migrations/types.js +9 -0
- package/dist/resources/presence-internal.js +29 -0
- package/dist/resources/provenance.js +50 -0
- package/dist/resources/rate-limiter.js +8 -1
- package/dist/resources/scoring.js +124 -5
- package/dist/resources/semantic-retrieval-core.js +317 -0
- package/dist/version-handshake.js +122 -0
- package/package.json +4 -5
- package/schemas/event.graphql +5 -0
- package/schemas/memory.graphql +66 -0
- package/schemas/schema.graphql +5 -43
- package/schemas/workspace.graphql +3 -0
- package/dist/resources/IngestEvents.js +0 -162
- package/dist/resources/IssueTokens.js +0 -19
- package/dist/resources/ObsAgentSnapshot.js +0 -13
- package/dist/resources/ObsEventFeed.js +0 -13
- package/dist/resources/ObsOffice.js +0 -19
- package/dist/resources/ObservationCenter.js +0 -23
- package/ui/observation-center.html +0 -385
package/dist/resources/health.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { Resource, databases } from "@harperfast/harper";
|
|
2
|
-
import { promises as fsp } from "node:fs";
|
|
2
|
+
import { promises as fsp, existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { homedir, platform } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { getRerankStatus } from "./rerank-provider.js";
|
|
7
|
+
import { allowVerified, resolveAgentAuth } from "./agent-auth.js";
|
|
8
|
+
import { getMigrationStatusSnapshot } from "./migrations/status.js";
|
|
6
9
|
const db = databases;
|
|
7
10
|
const redactHome = (p) => {
|
|
8
11
|
const home = homedir();
|
|
@@ -17,6 +20,35 @@ const exists = async (path) => {
|
|
|
17
20
|
return false;
|
|
18
21
|
}
|
|
19
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the runtime package version from package.json (duplicated from
|
|
25
|
+
* admin-layout.ts / AdminInstance.ts — same "keep in sync" idiom as the
|
|
26
|
+
* federation-crypto helpers in src/cli.ts; see those two files' comments for
|
|
27
|
+
* why it's inlined rather than imported). `process.env.npm_package_version`
|
|
28
|
+
* is only populated inside `npm run`, so reading package.json relative to
|
|
29
|
+
* THIS running module is the only way to report the version of the code
|
|
30
|
+
* that's actually executing — which is exactly what `flair upgrade`'s
|
|
31
|
+
* post-restart verification (flair#635) needs: proof the NEW package is
|
|
32
|
+
* what's serving, not just what landed on disk before a restart.
|
|
33
|
+
*/
|
|
34
|
+
function resolveVersion() {
|
|
35
|
+
try {
|
|
36
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
const candidates = [
|
|
38
|
+
join(here, "..", "..", "package.json"),
|
|
39
|
+
join(here, "..", "package.json"),
|
|
40
|
+
];
|
|
41
|
+
for (const p of candidates) {
|
|
42
|
+
if (existsSync(p)) {
|
|
43
|
+
const pkg = JSON.parse(readFileSync(p, "utf-8"));
|
|
44
|
+
if (pkg.version)
|
|
45
|
+
return pkg.version;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch { /* fall through */ }
|
|
50
|
+
return process.env.npm_package_version ?? "dev";
|
|
51
|
+
}
|
|
20
52
|
/**
|
|
21
53
|
* Health endpoint — truly public, returns only { ok: true }.
|
|
22
54
|
*
|
|
@@ -45,25 +77,57 @@ export class Health extends Resource {
|
|
|
45
77
|
return true;
|
|
46
78
|
}
|
|
47
79
|
async get() {
|
|
48
|
-
|
|
80
|
+
// `version` (flair#695, the CLI↔server handshake — src/version-handshake.ts):
|
|
81
|
+
// exposed on the PUBLIC endpoint deliberately, so the check works even
|
|
82
|
+
// before an agent identity/key exists (fresh install, pre-`flair init`).
|
|
83
|
+
// Sherlock verdict: fine while locally bound; if /Health is ever fronted
|
|
84
|
+
// PUBLICLY over Fabric, the public surface must omit this field (the
|
|
85
|
+
// richer, auth-gated /HealthDetail already carried version pre-existing —
|
|
86
|
+
// this only adds it to the anonymous endpoint too).
|
|
87
|
+
return { ok: true, version: resolveVersion() };
|
|
49
88
|
}
|
|
50
89
|
}
|
|
51
90
|
/**
|
|
52
91
|
* Authenticated health detail — returns memory/agent/soul stats + process info.
|
|
53
92
|
* Requires Ed25519 agent auth or admin basic auth.
|
|
54
93
|
*
|
|
94
|
+
* allowRead()=allowVerified (authorizeLocal-escalation-class follow-up to
|
|
95
|
+
* #601/#604/#609/#612 — flair#614's backstop found this resource had NO
|
|
96
|
+
* allow* at all, despite this docstring's stated intent). Harper's own
|
|
97
|
+
* default (`user?.role.permission.super_user`, satisfiable only by a genuine
|
|
98
|
+
* admin OR authorizeLocal's forged loopback super_user) was silently
|
|
99
|
+
* standing in instead. Any verified agent may read — get() below still
|
|
100
|
+
* filters sensitive fields (peer/OAuth-client lists, absolute paths, full
|
|
101
|
+
* agent roster) down to admin-only.
|
|
102
|
+
*
|
|
55
103
|
* Every optional-subsystem lookup is wrapped so a missing table or absent
|
|
56
104
|
* schema downgrades to "not configured" rather than failing the whole call.
|
|
57
105
|
*/
|
|
58
106
|
export class HealthDetail extends Resource {
|
|
107
|
+
async allowRead() {
|
|
108
|
+
return allowVerified(this.getContext?.());
|
|
109
|
+
}
|
|
59
110
|
async get() {
|
|
60
111
|
const stats = { ok: true };
|
|
61
112
|
const nowMs = Date.now();
|
|
62
113
|
const warnings = [];
|
|
63
114
|
const ctx = this.getContext?.();
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
115
|
+
// #614 fix: resolve identity via the shared three-way verdict
|
|
116
|
+
// (internal/agent/anonymous — agent-auth.ts) instead of reading
|
|
117
|
+
// tpsAgent/tpsAgentIsAdmin off the raw request directly. The OLD
|
|
118
|
+
// computation was `request?.tpsAgentIsAdmin === true || !callerAgent` —
|
|
119
|
+
// the `|| !callerAgent` half meant an UNRESOLVED caller (no tpsAgent at
|
|
120
|
+
// all — i.e. anonymous) defaulted to isAdmin=TRUE, backwards from every
|
|
121
|
+
// other resource in this codebase and the opposite of fail-safe.
|
|
122
|
+
// allowRead() above already denies a genuine anonymous HTTP caller
|
|
123
|
+
// before get() ever runs; this fixes the internal computation to match
|
|
124
|
+
// (defense-in-depth, and correct semantics if get() is ever reached
|
|
125
|
+
// another way). Only a true "internal" verdict (no HTTP request at all —
|
|
126
|
+
// a programmatic/in-process call) or a verified admin agent is isAdmin;
|
|
127
|
+
// an unresolved/anonymous HTTP caller is never treated as admin.
|
|
128
|
+
const auth = await resolveAgentAuth(ctx);
|
|
129
|
+
const callerAgent = auth.kind === "agent" ? auth.agentId : undefined;
|
|
130
|
+
const isAdmin = auth.kind === "internal" || (auth.kind === "agent" && auth.isAdmin);
|
|
67
131
|
stats.caller = { agentId: callerAgent ?? null, isAdmin };
|
|
68
132
|
let memoriesList = [];
|
|
69
133
|
// ── Memory stats ──
|
|
@@ -435,6 +499,29 @@ export class HealthDetail extends Resource {
|
|
|
435
499
|
catch {
|
|
436
500
|
stats.rem = null;
|
|
437
501
|
}
|
|
502
|
+
// ── Migrations (flair#695: zero-touch boot-keyed auto-migration) ──
|
|
503
|
+
// `{ id, rowsDone, rowsRemaining, state }` per registered migration, per
|
|
504
|
+
// flair#695 §A. `cyclePhase: "pre-hash"` is the
|
|
505
|
+
// "pre-flight integrity check in progress" state the K&S verdict calls
|
|
506
|
+
// for; a halted migration surfaces here (with `reason`) AND as a
|
|
507
|
+
// warning below so it's visible without a separate lookup.
|
|
508
|
+
try {
|
|
509
|
+
const migrationsDataDir = process.env.HDB_ROOT ?? join(homedir(), ".flair", "data");
|
|
510
|
+
const snapshot = getMigrationStatusSnapshot(migrationsDataDir);
|
|
511
|
+
stats.migrations = {
|
|
512
|
+
cyclePhase: snapshot.cyclePhase,
|
|
513
|
+
lastCycleAt: snapshot.lastCycleAt ?? null,
|
|
514
|
+
migrations: snapshot.migrations,
|
|
515
|
+
};
|
|
516
|
+
for (const m of snapshot.migrations) {
|
|
517
|
+
if (m.state === "halted" || m.state === "failed") {
|
|
518
|
+
warnings.push({ level: "warn", message: `migration '${m.id}' ${m.state}${m.reason ? `: ${m.reason}` : ""} — see \`flair doctor\`` });
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
stats.migrations = null;
|
|
524
|
+
}
|
|
438
525
|
// ── Disk ──
|
|
439
526
|
try {
|
|
440
527
|
const dataDir = process.env.HDB_ROOT ?? join(homedir(), ".flair", "data");
|
|
@@ -535,6 +622,10 @@ export class HealthDetail extends Resource {
|
|
|
535
622
|
// ── Warnings ──
|
|
536
623
|
stats.warnings = warnings;
|
|
537
624
|
// ── Process info ──
|
|
625
|
+
// version: the RUNNING process's own package.json — used by `flair
|
|
626
|
+
// upgrade`'s post-restart verification (flair#635) to prove the new
|
|
627
|
+
// code is actually serving, not just installed on disk.
|
|
628
|
+
stats.version = resolveVersion();
|
|
538
629
|
stats.pid = process.pid;
|
|
539
630
|
stats.uptimeSeconds = Math.floor(process.uptime());
|
|
540
631
|
return stats;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client ID Metadata Document (CIMD) field logic for Flair agent identities.
|
|
3
|
+
*
|
|
4
|
+
* Produces the JSON metadata document an OAuth authorization server fetches
|
|
5
|
+
* when a client_id is an HTTPS URL — draft-ietf-oauth-client-id-metadata-
|
|
6
|
+
* document-00, adopted by the MCP draft authorization spec IN PLACE OF DCR
|
|
7
|
+
* (no registration row to create or replicate across nodes; the served
|
|
8
|
+
* document IS the registration).
|
|
9
|
+
*
|
|
10
|
+
* Shape is pinned to HarperFast/oauth **issue #161** ("client_credentials
|
|
11
|
+
* (2/4): CIMD-first client resolution for private_key_jwt agents") — the
|
|
12
|
+
* FORMAL shape spec for this document, shipped in @harperfast/oauth@2.2.0
|
|
13
|
+
* via PR #170 (with #167's CIMD resolution layer). #161 requires:
|
|
14
|
+
*
|
|
15
|
+
* - `grant_types: ["client_credentials"]`;
|
|
16
|
+
* `token_endpoint_auth_method: "private_key_jwt"`.
|
|
17
|
+
* - `jwks` = JWK Set of PUBLIC OKP/Ed25519 keys ONLY — reject any key
|
|
18
|
+
* carrying a private `d`, reject empty sets, reject non-OKP/non-Ed25519
|
|
19
|
+
* keys.
|
|
20
|
+
* - client_credentials-only clients carry NEITHER `redirect_uris` NOR
|
|
21
|
+
* `response_types` — #161 explicitly blesses this as a documented
|
|
22
|
+
* deviation from the CIMD draft's general required-fields list (which
|
|
23
|
+
* includes `redirect_uris` for interactive/DCR-style clients).
|
|
24
|
+
* - `client_credentials` combined with `refresh_token` is rejected.
|
|
25
|
+
* - Security gate (AS-side, enforced by the plugin, documented here for
|
|
26
|
+
* deployment coordination): `clientIdMetadataDocuments.allowedHosts`
|
|
27
|
+
* MUST be configured and this document's `client_id` host MUST be on
|
|
28
|
+
* it — merely hosting a reachable document must never be sufficient to
|
|
29
|
+
* mint tokens. Replaces the old DCR `initialAccessToken` gate.
|
|
30
|
+
*
|
|
31
|
+
* The AS-side machinery consuming this document (fetch/validate/cache —
|
|
32
|
+
* #167's CIMD resolution layer, extended by #170 for client_credentials +
|
|
33
|
+
* private_key_jwt) is SHIPPED in the published @harperfast/oauth@2.2.0.
|
|
34
|
+
* This module's output is proven against that real published code — not a
|
|
35
|
+
* mirror — in test/unit/mcp-client-credentials-live-package.test.ts, which
|
|
36
|
+
* drives 2.2.0's actual `resolveCimdClient` pipeline (via its exported
|
|
37
|
+
* `_setDnsLookup`/`_setFetch` test hooks) and confirms the document
|
|
38
|
+
* resolves and validates end-to-end, plus the fail-closed negatives (no
|
|
39
|
+
* `allowedHosts` configured → rejected; leaked private `d` in a JWK →
|
|
40
|
+
* rejected by the plugin even if our own build-time guard were bypassed).
|
|
41
|
+
*
|
|
42
|
+
* Kept free of any @harperfast/harper import (mirrors agentcard-fields.ts)
|
|
43
|
+
* so the document shape is unit-testable without spinning up Harper.
|
|
44
|
+
*/
|
|
45
|
+
/** The grant this agent's CIMD document targets — formalized by oauth#161, accepted by the published 2.2.0 validator. See module header. */
|
|
46
|
+
export const CIMD_TARGET_GRANT_TYPES = ["client_credentials"];
|
|
47
|
+
/** The auth method this agent's CIMD document targets — formalized by oauth#161, accepted by the published 2.2.0 validator. See module header. */
|
|
48
|
+
export const CIMD_TARGET_AUTH_METHOD = "private_key_jwt";
|
|
49
|
+
/** Build a Client ID Metadata Document for a headless Flair agent. */
|
|
50
|
+
export function buildCimdDocument(params) {
|
|
51
|
+
const { clientId, clientName, jwk } = params;
|
|
52
|
+
if (!clientId)
|
|
53
|
+
throw new Error("clientId is required");
|
|
54
|
+
if (!clientName)
|
|
55
|
+
throw new Error("clientName is required");
|
|
56
|
+
if (!jwk || jwk.kty !== "OKP" || jwk.crv !== "Ed25519" || typeof jwk.x !== "string" || !jwk.x) {
|
|
57
|
+
throw new Error("jwk must be a public Ed25519 JWK ({kty:'OKP', crv:'Ed25519', x:'...'})");
|
|
58
|
+
}
|
|
59
|
+
// Belt-and-suspenders (oauth#161: "reject any key carrying private `d`").
|
|
60
|
+
// Ed25519Jwk's TS type has no `d` field, so a well-typed caller can't
|
|
61
|
+
// construct one — but this function's argument still crosses a runtime
|
|
62
|
+
// boundary (a JWK read from disk/network, or a caller using `as any`), so
|
|
63
|
+
// check for it explicitly rather than trust the type alone. A CIMD
|
|
64
|
+
// document is fetched by an unauthenticated AS over the public internet;
|
|
65
|
+
// there is no scenario where leaking `d` here is acceptable.
|
|
66
|
+
if (typeof jwk.d !== "undefined") {
|
|
67
|
+
throw new Error("jwk must be a PUBLIC key — refusing to build a CIMD document from a JWK carrying a private 'd' component");
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
client_id: clientId,
|
|
71
|
+
client_name: clientName,
|
|
72
|
+
// Exactly one key, always non-empty and always OKP/Ed25519 — both are
|
|
73
|
+
// structurally guaranteed by the validation above (oauth#161 also
|
|
74
|
+
// requires rejecting an EMPTY jwks set and non-OKP/non-Ed25519 keys;
|
|
75
|
+
// this function's single-already-validated-jwk API makes both
|
|
76
|
+
// impossible to violate from here).
|
|
77
|
+
jwks: { keys: [jwk] },
|
|
78
|
+
// pending oauth#161 — rejected by today's open-draft #167 validator; see module header.
|
|
79
|
+
token_endpoint_auth_method: CIMD_TARGET_AUTH_METHOD,
|
|
80
|
+
grant_types: [...CIMD_TARGET_GRANT_TYPES],
|
|
81
|
+
// redirect_uris AND response_types are both deliberately omitted — #161
|
|
82
|
+
// blesses this explicitly for client_credentials-only CIMD clients (no
|
|
83
|
+
// redirect-based flow is ever possible for a headless agent). Neither
|
|
84
|
+
// field is declared on CimdDocument, so neither can slip back in via
|
|
85
|
+
// this object literal.
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Normalize a Flair `Agent.publicKey` value into a JWK OKP `x`. Accepts both
|
|
90
|
+
* encodings `Agent.publicKey` is written in across this repo (hex 64-char,
|
|
91
|
+
* or base64/base64url 44-char raw 32-byte key) — the same two forms
|
|
92
|
+
* resources/ed25519-auth.ts's `importEd25519Key` accepts, so a document
|
|
93
|
+
* built from this always matches what the verified-request path already
|
|
94
|
+
* treats as the agent's key.
|
|
95
|
+
*/
|
|
96
|
+
export function agentPublicKeyToJwk(publicKeyStr, kid) {
|
|
97
|
+
const trimmed = (publicKeyStr ?? "").trim();
|
|
98
|
+
let raw;
|
|
99
|
+
if (/^[0-9a-f]{64}$/i.test(trimmed)) {
|
|
100
|
+
raw = Buffer.from(trimmed, "hex");
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
raw = Buffer.from(trimmed.replace(/-/g, "+").replace(/_/g, "/"), "base64");
|
|
104
|
+
}
|
|
105
|
+
if (raw.length !== 32) {
|
|
106
|
+
throw new Error(`agent public key must decode to 32 bytes (got ${raw.length})`);
|
|
107
|
+
}
|
|
108
|
+
const jwk = { kty: "OKP", crv: "Ed25519", x: raw.toString("base64url") };
|
|
109
|
+
if (kid)
|
|
110
|
+
jwk.kid = kid;
|
|
111
|
+
return jwk;
|
|
112
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* mcp-handler.ts — the Model-2 custom MCP protocol handler.
|
|
3
3
|
*
|
|
4
|
-
* A minimal in-process MCP (JSON-RPC 2.0) handler serving the
|
|
4
|
+
* A minimal in-process MCP (JSON-RPC 2.0) handler serving the 12 curated flair
|
|
5
5
|
* tools over Streamable HTTP. It is wrapped by `@harperfast/oauth`'s
|
|
6
6
|
* `withMCPAuth` (see mcp-oauth.ts), which fails closed on any missing/invalid
|
|
7
7
|
* Bearer token BEFORE this handler runs and, on success, sets
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* mcp-tools.ts — the
|
|
2
|
+
* mcp-tools.ts — the 12 curated flair tools for the Model-2 custom /mcp handler.
|
|
3
3
|
*
|
|
4
|
-
* Curated BY CONSTRUCTION: this module implements
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `resolveAgentAuth(getContext())
|
|
10
|
-
*
|
|
11
|
-
*
|
|
4
|
+
* Curated BY CONSTRUCTION: this module implements a fixed set of tools, each a
|
|
5
|
+
* thin wrapper over the existing flair Resource handler. No business logic is
|
|
6
|
+
* re-implemented — the wrapped handlers (Memory / SemanticSearch /
|
|
7
|
+
* BootstrapMemories / Soul / WorkspaceState / OrgEvent / AttentionQuery /
|
|
8
|
+
* RecordUsage) enforce per-agent scoping/ownership via
|
|
9
|
+
* `resolveAgentAuth(getContext())` (or, for `attention`, AttentionQuery's own
|
|
10
|
+
* per-source scoping — see resources/AttentionQuery.ts's module doc), so the
|
|
11
|
+
* MCP surface inherits the SAME security model as the signed-REST path. There
|
|
12
|
+
* is no raw CRUD surface — the only way to reach the datastore through /mcp is
|
|
13
|
+
* via one of these 12 semantic tools.
|
|
12
14
|
*
|
|
13
15
|
* memory_search · memory_store · memory_update · memory_get · memory_delete ·
|
|
14
|
-
* bootstrap · soul_set · soul_get · flair_workspace_set · flair_orgevent
|
|
16
|
+
* bootstrap · soul_set · soul_get · flair_workspace_set · flair_orgevent ·
|
|
17
|
+
* attention · record_usage
|
|
15
18
|
*
|
|
16
19
|
* ── The scoping seam ────────────────────────────────────────────────────────
|
|
17
20
|
* The /mcp handler resolves the OAuth token's `sub` → a flair `Agent` id, then
|
|
@@ -21,6 +24,16 @@
|
|
|
21
24
|
* agent exactly as an Ed25519-signed REST call would. Identity ALWAYS comes from
|
|
22
25
|
* the resolved agent, never from the tool arguments — an agent can only act as
|
|
23
26
|
* itself (no forging of agentId / authorId in the body).
|
|
27
|
+
*
|
|
28
|
+
* NOTE (flair#677 scope call): the legacy `@tpsdev-ai/flair-mcp` stdio proxy
|
|
29
|
+
* (packages/flair-mcp) is a SEPARATE, independently-published package that
|
|
30
|
+
* talks to flair over HTTP via `FlairClient` — it is not wired through this
|
|
31
|
+
* registry at all (its own tool list is hardcoded in packages/flair-mcp/src/
|
|
32
|
+
* index.ts). Per the zero-install north star (retiring flair-mcp in favor of
|
|
33
|
+
* this native /mcp handler), `attention` is added HERE only, not mirrored into
|
|
34
|
+
* the legacy stdio proxy — adding it there would mean a separate package
|
|
35
|
+
* version bump + a new FlairClient method, out of scope for this query-only
|
|
36
|
+
* slice.
|
|
24
37
|
*/
|
|
25
38
|
const H = {};
|
|
26
39
|
const LOADERS = {
|
|
@@ -30,6 +43,8 @@ const LOADERS = {
|
|
|
30
43
|
Soul: async () => (await import("./Soul.js")).Soul,
|
|
31
44
|
WorkspaceState: async () => (await import("./WorkspaceState.js")).WorkspaceState,
|
|
32
45
|
OrgEvent: async () => (await import("./OrgEvent.js")).OrgEvent,
|
|
46
|
+
AttentionQuery: async () => (await import("./AttentionQuery.js")).AttentionQuery,
|
|
47
|
+
RecordUsage: async () => (await import("./RecordUsage.js")).RecordUsage,
|
|
33
48
|
};
|
|
34
49
|
/** Resolve a handler class — from the test override if set, else lazy-load + cache. */
|
|
35
50
|
async function handler(key) {
|
|
@@ -188,6 +203,7 @@ async function bootstrap(agent, args) {
|
|
|
188
203
|
channel: args?.channel,
|
|
189
204
|
surface: args?.surface,
|
|
190
205
|
subjects: args?.subjects,
|
|
206
|
+
entities: args?.entities,
|
|
191
207
|
}));
|
|
192
208
|
}
|
|
193
209
|
async function soulSet(agent, args) {
|
|
@@ -249,6 +265,28 @@ async function orgEvent(agent, args) {
|
|
|
249
265
|
body.targetIds = args.targets;
|
|
250
266
|
return unwrap(await h.post(body));
|
|
251
267
|
}
|
|
268
|
+
async function attention(agent, args) {
|
|
269
|
+
const Cls = await handler("AttentionQuery");
|
|
270
|
+
const h = new Cls(undefined, delegationContext(agent));
|
|
271
|
+
return unwrap(await h.post({ entity: args?.entity, days: args?.days }));
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* record_usage (flair#683) — report that memory(ies) were actually used
|
|
275
|
+
* (cited/grounded an answer or decision), driving the usage-feedback signal
|
|
276
|
+
* (Memory.usageCount → usageBoost → compositeScore). Distinct from search:
|
|
277
|
+
* calling memory_search does NOT count as usage — this tool is the explicit,
|
|
278
|
+
* verified-use report resources/RecordUsage.ts's module doc describes.
|
|
279
|
+
* Identity is the RESOLVED agent (delegationContext), never forgeable via
|
|
280
|
+
* args — same no-forge contract as every other write tool here.
|
|
281
|
+
*/
|
|
282
|
+
async function recordUsage(agent, args) {
|
|
283
|
+
const Cls = await handler("RecordUsage");
|
|
284
|
+
const h = new Cls(undefined, delegationContext(agent));
|
|
285
|
+
const memoryIds = Array.isArray(args?.memoryIds)
|
|
286
|
+
? args.memoryIds
|
|
287
|
+
: typeof args?.memoryId === "string" ? [args.memoryId] : undefined;
|
|
288
|
+
return unwrap(await h.post({ memoryIds, attribution: args?.attribution }));
|
|
289
|
+
}
|
|
252
290
|
export const TOOLS = {
|
|
253
291
|
memory_search: {
|
|
254
292
|
def: {
|
|
@@ -340,6 +378,11 @@ export const TOOLS = {
|
|
|
340
378
|
channel: { type: "string", description: "Channel name (discord, tps-mail, claude-code)" },
|
|
341
379
|
surface: { type: "string", description: "Surface name (tps-build, tps-review, cli-session)" },
|
|
342
380
|
subjects: { type: "array", items: { type: "string" }, description: "Entity names to preload context for" },
|
|
381
|
+
entities: {
|
|
382
|
+
type: "array",
|
|
383
|
+
items: { type: "string" },
|
|
384
|
+
description: "Your declared attention-plane vocabulary strings (e.g. \"issue:owner/repo#123\") for collision surfacing's 'Others in the room' block — teammates with overlapping active work. Falls back to your own most-recent workspace-state entities when omitted.",
|
|
385
|
+
},
|
|
343
386
|
},
|
|
344
387
|
},
|
|
345
388
|
},
|
|
@@ -410,6 +453,41 @@ export const TOOLS = {
|
|
|
410
453
|
},
|
|
411
454
|
impl: orgEvent,
|
|
412
455
|
},
|
|
456
|
+
attention: {
|
|
457
|
+
def: {
|
|
458
|
+
name: "attention",
|
|
459
|
+
description: "What's touching entity E in the last N days? A unified, grouped-by-source view across memories, " +
|
|
460
|
+
"relationships, active work (WorkspaceState), teammate presence, and org events. Entity must be a " +
|
|
461
|
+
"vocabulary string (e.g. 'repo:owner/name', 'issue:owner/repo#123', 'subsystem:embeddings').",
|
|
462
|
+
annotations: { readOnlyHint: true },
|
|
463
|
+
inputSchema: {
|
|
464
|
+
type: "object",
|
|
465
|
+
properties: {
|
|
466
|
+
entity: { type: "string", description: "Vocabulary string, exact match (type:value — e.g. 'repo:tpsdev-ai/flair')" },
|
|
467
|
+
days: { type: "number", description: "Window size in days (default 7)" },
|
|
468
|
+
},
|
|
469
|
+
required: ["entity"],
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
impl: attention,
|
|
473
|
+
},
|
|
474
|
+
record_usage: {
|
|
475
|
+
def: {
|
|
476
|
+
name: "record_usage",
|
|
477
|
+
description: "Report that one or more memories were actually USED — cited or relied on to ground an answer or decision. " +
|
|
478
|
+
"Distinct from search (surfacing a memory is not usage). Drives the recall-quality usage signal; dedup'd " +
|
|
479
|
+
"(you can only count once per memory) and rate-limited.",
|
|
480
|
+
inputSchema: {
|
|
481
|
+
type: "object",
|
|
482
|
+
properties: {
|
|
483
|
+
memoryIds: { type: "array", items: { type: "string" }, description: "IDs of the memories that were used (max 20 per call)" },
|
|
484
|
+
memoryId: { type: "string", description: "Convenience alias for a single memory id (use memoryIds for multiple)" },
|
|
485
|
+
attribution: { type: "string", description: "Optional free-text note on what used it (opaque — stored for audit only, max 500 chars)" },
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
impl: recordUsage,
|
|
490
|
+
},
|
|
413
491
|
};
|
|
414
492
|
/** The tool definitions for a tools/list response (exactly the 9 curated tools). */
|
|
415
493
|
export function listToolDefs() {
|