@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.
Files changed (74) hide show
  1. package/README.md +10 -7
  2. package/SECURITY.md +24 -2
  3. package/config.yaml +32 -5
  4. package/dist/cli.js +1811 -221
  5. package/dist/deploy.js +3 -4
  6. package/dist/fleet-presence.js +98 -0
  7. package/dist/fleet-verify.js +291 -0
  8. package/dist/mcp-client-assertion.js +364 -0
  9. package/dist/probe.js +97 -0
  10. package/dist/rem/runner.js +82 -12
  11. package/dist/resources/AttentionQuery.js +356 -0
  12. package/dist/resources/Credential.js +11 -1
  13. package/dist/resources/Federation.js +23 -0
  14. package/dist/resources/MCPClientMetadata.js +88 -0
  15. package/dist/resources/Memory.js +52 -53
  16. package/dist/resources/MemoryBootstrap.js +422 -76
  17. package/dist/resources/MemoryReflect.js +105 -49
  18. package/dist/resources/MemoryUsage.js +104 -0
  19. package/dist/resources/OAuth.js +8 -1
  20. package/dist/resources/OrgEvent.js +11 -0
  21. package/dist/resources/Presence.js +218 -19
  22. package/dist/resources/RecordUsage.js +230 -0
  23. package/dist/resources/Relationship.js +98 -25
  24. package/dist/resources/SemanticSearch.js +85 -317
  25. package/dist/resources/SkillScan.js +15 -0
  26. package/dist/resources/WorkspaceState.js +11 -0
  27. package/dist/resources/agent-auth.js +60 -2
  28. package/dist/resources/auth-middleware.js +18 -9
  29. package/dist/resources/bm25.js +12 -6
  30. package/dist/resources/collision-lib.js +157 -0
  31. package/dist/resources/embeddings-boot.js +149 -0
  32. package/dist/resources/embeddings-provider.js +364 -106
  33. package/dist/resources/entity-vocab.js +139 -0
  34. package/dist/resources/health.js +97 -6
  35. package/dist/resources/mcp-client-metadata-fields.js +112 -0
  36. package/dist/resources/mcp-handler.js +1 -1
  37. package/dist/resources/mcp-tools.js +88 -10
  38. package/dist/resources/memory-reflect-lib.js +289 -0
  39. package/dist/resources/migration-boot.js +143 -0
  40. package/dist/resources/migrations/dir-safety.js +71 -0
  41. package/dist/resources/migrations/embedding-stamp.js +178 -0
  42. package/dist/resources/migrations/envelope.js +43 -0
  43. package/dist/resources/migrations/export.js +38 -0
  44. package/dist/resources/migrations/ledger.js +55 -0
  45. package/dist/resources/migrations/lock.js +154 -0
  46. package/dist/resources/migrations/progress.js +31 -0
  47. package/dist/resources/migrations/registry.js +36 -0
  48. package/dist/resources/migrations/risk-policy.js +23 -0
  49. package/dist/resources/migrations/runner.js +455 -0
  50. package/dist/resources/migrations/snapshot.js +127 -0
  51. package/dist/resources/migrations/source-fields.js +93 -0
  52. package/dist/resources/migrations/space.js +167 -0
  53. package/dist/resources/migrations/state.js +64 -0
  54. package/dist/resources/migrations/status.js +39 -0
  55. package/dist/resources/migrations/synthetic-test-migration.js +93 -0
  56. package/dist/resources/migrations/types.js +9 -0
  57. package/dist/resources/presence-internal.js +29 -0
  58. package/dist/resources/provenance.js +50 -0
  59. package/dist/resources/rate-limiter.js +8 -1
  60. package/dist/resources/scoring.js +124 -5
  61. package/dist/resources/semantic-retrieval-core.js +317 -0
  62. package/dist/version-handshake.js +122 -0
  63. package/package.json +4 -5
  64. package/schemas/event.graphql +5 -0
  65. package/schemas/memory.graphql +66 -0
  66. package/schemas/schema.graphql +5 -43
  67. package/schemas/workspace.graphql +3 -0
  68. package/dist/resources/IngestEvents.js +0 -162
  69. package/dist/resources/IssueTokens.js +0 -19
  70. package/dist/resources/ObsAgentSnapshot.js +0 -13
  71. package/dist/resources/ObsEventFeed.js +0 -13
  72. package/dist/resources/ObsOffice.js +0 -19
  73. package/dist/resources/ObservationCenter.js +0 -23
  74. package/ui/observation-center.html +0 -385
@@ -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
- return { ok: true };
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
- const request = ctx?.request ?? ctx;
65
- const callerAgent = request?.tpsAgent;
66
- const isAdmin = request?.tpsAgentIsAdmin === true || !callerAgent;
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 9 curated flair
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 10 curated flair tools for the Model-2 custom /mcp handler.
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 exactly the 10 tools that the
5
- * `@tpsdev-ai/flair-mcp` stdio proxy exposes, each a thin wrapper over the
6
- * existing flair Resource handler. No business logic is re-implemented — the
7
- * wrapped handlers (Memory / SemanticSearch / BootstrapMemories / Soul /
8
- * WorkspaceState / OrgEvent) enforce per-agent scoping/ownership via
9
- * `resolveAgentAuth(getContext())`, so the MCP surface inherits the SAME security
10
- * model as the signed-REST path. There is no raw CRUD surface — the only way to
11
- * reach the datastore through /mcp is via one of these 10 semantic tools.
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() {