@tpsdev-ai/flair 0.20.1 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +39 -11
  2. package/SECURITY.md +52 -20
  3. package/config.yaml +32 -5
  4. package/dist/cli.js +2235 -260
  5. package/dist/deploy.js +96 -15
  6. package/dist/doctor-client.js +312 -0
  7. package/dist/fleet-presence.js +98 -0
  8. package/dist/fleet-verify.js +291 -0
  9. package/dist/install/clients.js +18 -0
  10. package/dist/mcp-client-assertion.js +364 -0
  11. package/dist/probe.js +97 -0
  12. package/dist/rem/restore.js +1 -1
  13. package/dist/rem/runner.js +82 -12
  14. package/dist/resources/Admin.js +1 -1
  15. package/dist/resources/AdminConnectors.js +1 -1
  16. package/dist/resources/AdminDashboard.js +1 -1
  17. package/dist/resources/AdminIdp.js +1 -1
  18. package/dist/resources/AdminInstance.js +1 -1
  19. package/dist/resources/AdminMemory.js +2 -2
  20. package/dist/resources/AdminPrincipals.js +1 -1
  21. package/dist/resources/Agent.js +14 -0
  22. package/dist/resources/AgentCard.js +1 -1
  23. package/dist/resources/AgentSeed.js +1 -1
  24. package/dist/resources/AttentionQuery.js +356 -0
  25. package/dist/resources/Credential.js +11 -1
  26. package/dist/resources/Federation.js +121 -2
  27. package/dist/resources/Integration.js +1 -1
  28. package/dist/resources/MCPClientMetadata.js +88 -0
  29. package/dist/resources/Memory.js +127 -22
  30. package/dist/resources/MemoryBootstrap.js +441 -85
  31. package/dist/resources/MemoryGrant.js +1 -1
  32. package/dist/resources/MemoryReflect.js +105 -49
  33. package/dist/resources/MemoryUsage.js +104 -0
  34. package/dist/resources/OAuth.js +69 -5
  35. package/dist/resources/OrgEvent.js +11 -0
  36. package/dist/resources/OrgEventCatchup.js +1 -1
  37. package/dist/resources/Presence.js +268 -17
  38. package/dist/resources/RecordUsage.js +230 -0
  39. package/dist/resources/Relationship.js +108 -26
  40. package/dist/resources/SemanticSearch.js +88 -320
  41. package/dist/resources/SkillScan.js +15 -0
  42. package/dist/resources/Soul.js +14 -0
  43. package/dist/resources/WorkspaceLatest.js +1 -1
  44. package/dist/resources/WorkspaceState.js +12 -1
  45. package/dist/resources/agent-auth.js +61 -3
  46. package/dist/resources/agentcard-fields.js +2 -2
  47. package/dist/resources/auth-middleware.js +42 -14
  48. package/dist/resources/bm25-filter.js +1 -1
  49. package/dist/resources/bm25.js +13 -7
  50. package/dist/resources/collision-lib.js +157 -0
  51. package/dist/resources/dedup.js +2 -2
  52. package/dist/resources/ed25519-auth.js +2 -2
  53. package/dist/resources/embeddings-boot.js +149 -0
  54. package/dist/resources/embeddings-provider.js +364 -106
  55. package/dist/resources/entity-vocab.js +139 -0
  56. package/dist/resources/federation-nonce-store.js +195 -0
  57. package/dist/resources/health.js +97 -6
  58. package/dist/resources/instance-identity.js +53 -0
  59. package/dist/resources/mcp-client-metadata-fields.js +112 -0
  60. package/dist/resources/mcp-handler.js +1 -1
  61. package/dist/resources/mcp-tools.js +88 -10
  62. package/dist/resources/memory-bootstrap-lib.js +1 -1
  63. package/dist/resources/memory-read-scope.js +58 -71
  64. package/dist/resources/memory-reflect-lib.js +289 -0
  65. package/dist/resources/memory-visibility.js +37 -0
  66. package/dist/resources/migration-boot.js +143 -0
  67. package/dist/resources/migrations/dir-safety.js +71 -0
  68. package/dist/resources/migrations/embedding-stamp.js +178 -0
  69. package/dist/resources/migrations/envelope.js +43 -0
  70. package/dist/resources/migrations/export.js +38 -0
  71. package/dist/resources/migrations/ledger.js +55 -0
  72. package/dist/resources/migrations/lock.js +154 -0
  73. package/dist/resources/migrations/progress.js +31 -0
  74. package/dist/resources/migrations/registry.js +36 -0
  75. package/dist/resources/migrations/risk-policy.js +23 -0
  76. package/dist/resources/migrations/runner.js +456 -0
  77. package/dist/resources/migrations/snapshot.js +127 -0
  78. package/dist/resources/migrations/source-fields.js +93 -0
  79. package/dist/resources/migrations/space.js +73 -0
  80. package/dist/resources/migrations/state.js +64 -0
  81. package/dist/resources/migrations/status.js +39 -0
  82. package/dist/resources/migrations/synthetic-test-migration.js +93 -0
  83. package/dist/resources/migrations/types.js +9 -0
  84. package/dist/resources/presence-internal.js +29 -0
  85. package/dist/resources/provenance.js +50 -0
  86. package/dist/resources/rate-limiter.js +8 -1
  87. package/dist/resources/scoring.js +124 -5
  88. package/dist/resources/semantic-retrieval-core.js +317 -0
  89. package/dist/version-check.js +167 -0
  90. package/dist/version-handshake.js +122 -0
  91. package/package.json +4 -5
  92. package/schemas/agent.graphql +7 -0
  93. package/schemas/event.graphql +5 -0
  94. package/schemas/federation.graphql +12 -0
  95. package/schemas/memory.graphql +82 -0
  96. package/schemas/schema.graphql +5 -43
  97. package/schemas/workspace.graphql +3 -0
  98. package/dist/resources/IngestEvents.js +0 -162
  99. package/dist/resources/IssueTokens.js +0 -19
  100. package/dist/resources/ObsAgentSnapshot.js +0 -13
  101. package/dist/resources/ObsEventFeed.js +0 -13
  102. package/dist/resources/ObsOffice.js +0 -19
  103. package/dist/resources/ObservationCenter.js +0 -23
  104. package/ui/observation-center.html +0 -385
@@ -39,6 +39,18 @@ type Peer @table(database: "flair") @export {
39
39
  updatedAt: String
40
40
  }
41
41
 
42
+ # Anti-replay nonce store (federation-edge-hardening slice 4).
43
+ # Persists nonces recorded by verifyBodySignatureFresh (federation-crypto.ts)
44
+ # so an instance restart doesn't wipe recently-seen nonces — closes the
45
+ # ±30s freshness-window replay gap a restart would otherwise reopen. Nonces
46
+ # are already globally unique (128-bit random, generated per request by
47
+ # signBodyFresh), so the nonce string itself is the primary key — no scope
48
+ # prefix needed. Internal bookkeeping only: never served, never replicated.
49
+ type Nonce @table(database: "flair") {
50
+ id: ID @primaryKey # the nonce string (base64url, 128-bit random)
51
+ seenAt: Int! @indexed # ms-epoch when first recorded — eviction cutoff
52
+ }
53
+
42
54
  # Sync log — audit trail for federation operations.
43
55
  type SyncLog @table(database: "flair") {
44
56
  id: ID @primaryKey
@@ -5,6 +5,16 @@ type Memory @table(database: "flair") {
5
5
  contentHash: String @indexed
6
6
  visibility: String
7
7
  embedding: [Float] @indexed(type: "HNSW")
8
+ embeddingModel: String @indexed # ops-1l18: previously written (resources/Memory.ts stamps every
9
+ # write) but never DECLARED in this schema, so it was storable but not
10
+ # queryable — Harper's Table.search({conditions}) rejects a condition on
11
+ # an undeclared attribute ("X is not a defined attribute"), which the
12
+ # zero-touch migration runner's bounded detect()/countPending()/run()
13
+ # queries (resources/migrations/embedding-stamp.ts: `embeddingModel
14
+ # not_equal <current>`) need in order to stay O(1)-bounded rather than a
15
+ # full-corpus scan on every boot. Purely additive — existing rows already
16
+ # carry this value on disk; declaring it queryable changes nothing about
17
+ # what's written or how embeddings/prefixes behave (#689's gate untouched).
8
18
  tags: [String] @indexed
9
19
  durability: String @indexed
10
20
  source: String
@@ -13,6 +23,18 @@ type Memory @table(database: "flair") {
13
23
  expiresAt: String @indexed
14
24
  retrievalCount: Int
15
25
  lastRetrieved: String
26
+ usageCount: Int # flair#683: verified-USE signal, distinct from retrievalCount (a
27
+ # search HIT). Additive/nullable — absent reads as 0, existing rows
28
+ # unaffected. NEVER auto-incremented on search/retrieval; the ONLY
29
+ # writer is the dedicated usage-feedback endpoint (resources/
30
+ # MemoryUsage.ts), which does a targeted patchRecord increment on
31
+ # this field alone (never through Memory.put() — see that
32
+ # resource's module doc for why: usage-feedback writes to ANOTHER
33
+ # agent's memory, and Memory's ownership check would 403 every
34
+ # legit call). Drives usageBoost in resources/scoring.ts, which
35
+ # replaces retrievalBoost in compositeScore (retrievalCount was the
36
+ # contaminated "retrieval counted as usage" signal root-caused in
37
+ # flair#623).
16
38
  promotionStatus: String # null | "pending" | "approved" | "rejected"
17
39
  promotedAt: String
18
40
  promotedBy: String # authenticated agentId who approved
@@ -31,11 +53,36 @@ type Memory @table(database: "flair") {
31
53
  validFrom: String @indexed # ISO timestamp — when this fact became true
32
54
  validTo: String @indexed # ISO timestamp — when this fact stopped being true (null = still valid)
33
55
  _safetyFlags: [String] # content safety flags from scanContent()
56
+ provenance: String # JSON blob (memory-provenance slice 1): { v, verified: { agentId, timestamp }, claimed?: { model } }
57
+ # Nullable by design — existing rows read back provenance = null, unchanged behavior (clean-upgrade-path gate).
58
+ # Same idiom as Soul.metadata below. Stamped server-side in resources/Memory.ts; never client-writable.
59
+ originatorInstanceId: String @indexed # federation-edge-hardening slice 1: WRITE-TIME instance identity, stamped server-side in
60
+ # resources/Memory.ts from resources/instance-identity.ts's localInstanceId() — never client-writable.
61
+ # Nullable (existing rows read null = clean upgrade). Distinct from the legacy _originatorInstanceId
62
+ # (Federation.ts's mergeRecord — stamped by the RECEIVER at merge time, forgeable/lossy); this field is
63
+ # stamped by the ORIGINATING instance itself and preserved (never re-stamped) as the record flows through
64
+ # sync merges. @indexed for the later sync push-query filter (per-record signature verification and the
65
+ # classifier org-gate are separate, later slices — not built here).
66
+ entities: [String] @indexed # attention-plane vocabulary strings (flair#675). Added in v1 (not v2) per K&S
67
+ # verdict on FLAIR-ATTENTION-PLANE.md — gives the future attention query
68
+ # uniform index pushdown across Memory/WorkspaceState/OrgEvent instead of a
69
+ # partial one. Optional/derivable-on-write (no automatic tagging built here —
70
+ # that producer is a follow-up); additive/nullable, existing rows carry no
71
+ # entities, readers tolerate absent (same pattern as Presence.activityUpdatedAt).
72
+ # See docs/entity-vocabulary.md.
34
73
  }
35
74
 
36
75
  # Explicit entity-to-entity relationships with temporal validity.
37
76
  # Enables queries like "who was team lead in Q1?" or "what changed about X on date Y?"
38
77
  # Inspired by knowledge graph triples with time-bounded validity windows.
78
+ #
79
+ # attention-plane vocabulary note (flair#675): Relationship gets NO `entities`
80
+ # field. `subject`/`object` already hold free-form entity-reference strings
81
+ # and are already @indexed — they're the vocabulary carrier for this table.
82
+ # (Today they're lowercased on write in resources/Relationship.ts's put() but
83
+ # not yet validated against resources/entity-vocab.ts — wiring that
84
+ # validation, so `subject`/`object` are enforced attention-plane vocabulary
85
+ # strings, is a follow-up, not this foundation slice.)
39
86
  type Relationship @table(database: "flair") {
40
87
  id: ID @primaryKey
41
88
  agentId: String! @indexed # who created this relationship
@@ -48,6 +95,16 @@ type Relationship @table(database: "flair") {
48
95
  source: String # where this was learned (memory ID, conversation, etc.)
49
96
  createdAt: String! @indexed
50
97
  updatedAt: String
98
+ originatorInstanceId: String @indexed # federation-edge-hardening slice 1 — see Memory.originatorInstanceId's doc above
99
+ # for the full contract (write-time stamp, nullable, preserved across sync merges).
100
+ # Stamped server-side in resources/Relationship.ts's put().
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 } }
103
+ # — via the shared resources/provenance.ts buildProvenance(), never a
104
+ # Relationship-specific format. Nullable by design — pre-existing rows read back
105
+ # provenance = null, unchanged behavior (migration-equivalence gate, same
106
+ # discipline as Memory.usageCount/flair#684). Stamped server-side in
107
+ # resources/Relationship.ts's put(); never client-writable.
51
108
  }
52
109
 
53
110
  type Soul @table(database: "flair") {
@@ -60,6 +117,9 @@ type Soul @table(database: "flair") {
60
117
  durability: String @indexed
61
118
  createdAt: String!
62
119
  updatedAt: String
120
+ originatorInstanceId: String @indexed # federation-edge-hardening slice 1 — see Memory.originatorInstanceId's doc above
121
+ # for the full contract (write-time stamp, nullable, preserved across sync merges).
122
+ # Stamped server-side in resources/Soul.ts's post()/put().
63
123
  }
64
124
 
65
125
  type MemoryGrant @table(database: "flair") @export {
@@ -71,6 +131,28 @@ type MemoryGrant @table(database: "flair") @export {
71
131
  createdAt: String
72
132
  }
73
133
 
134
+ # MemoryUsage — the dedup ledger for the usage-feedback signal (flair#683).
135
+ # One row per (agentId, memoryId) contribution: `id` is the deterministic
136
+ # composite key `${agentId}:${memoryId}`, so "has this agent already
137
+ # contributed to this memory's usageCount" is a single point-lookup, not a
138
+ # query. This is what makes the anti-gaming dedup rule ("each (agent, memory)
139
+ # pair contributes at most 1 to usageCount" — Sherlock) enforceable: the
140
+ # dedicated endpoint (resources/MemoryUsage.ts) only bumps Memory.usageCount
141
+ # when a NEW row is created here; a repeat call from the same agent for the
142
+ # same memory is a no-op against a row that already exists.
143
+ #
144
+ # `attribution` is OPAQUE — never parsed, never fed to an LLM, never rendered
145
+ # — a free-text "what used this" hint the caller supplies, sanitized
146
+ # (control-character-stripped, length-capped) and stored as-is for
147
+ # analytics/audit only. Never trust it as anything more than a label.
148
+ type MemoryUsage @table(database: "flair") {
149
+ id: ID @primaryKey # `${agentId}:${memoryId}` — the dedup key itself
150
+ agentId: String! @indexed # the contributing (verified) agent — never client-forgeable
151
+ memoryId: String! @indexed # the memory this contribution counted toward
152
+ attribution: String # opaque/sanitized free-text hint (optional)
153
+ createdAt: String!
154
+ }
155
+
74
156
  # MemoryCandidate — staged distillations from the FLAIR-NIGHTLY-REM cycle.
75
157
  # Per specs/FLAIR-NIGHTLY-REM.md § 7. Slice 1 of ops-2qq adds the schema +
76
158
  # `flair rem candidates` listing command. Future slices wire the nightly
@@ -3,48 +3,10 @@
3
3
 
4
4
  type Presence @table(database: "flair") @export {
5
5
  agentId: ID @primaryKey
6
- lastHeartbeatAt: BigInt # unix ms timestamp
6
+ lastHeartbeatAt: BigInt # unix ms timestamp — pure liveness beacon; every heartbeat refreshes it
7
7
  currentTask: String # short human string, e.g. "Reviewing flair#467"
8
- activity: String @indexed # "coding" | "reviewing" | "planning" | "idle"
9
- }
10
-
11
- # ─── Observatory tables ───────────────────────────────────────────────────────
12
-
13
- type ObsOffice @table(database: "flair") @export {
14
- id: ID @primaryKey # e.g. "office1"
15
- name: String!
16
- publicKey: String! # Ed25519 hex public key for auth verification
17
- status: String @indexed # "online" | "offline" | "degraded"
18
- lastSeen: String
19
- agentCount: Int
20
- createdAt: String!
21
- updatedAt: String
22
- }
23
-
24
- type ObsAgentSnapshot @table(database: "flair") @export {
25
- id: ID @primaryKey # "{officeId}:{agentId}"
26
- officeId: String! @indexed
27
- agentId: String! @indexed
28
- name: String
29
- role: String
30
- type: String # "agent" | "human"
31
- model: String
32
- status: String @indexed # "active" | "idle" | "offline"
33
- currentTask: String
34
- lastActivity: String
35
- lastHeartbeat: String
36
- updatedAt: String!
37
- }
38
-
39
- type ObsEventFeed @table(database: "flair") @export {
40
- id: ID @primaryKey # "{officeId}:{eventId}"
41
- officeId: String! @indexed
42
- kind: String! @indexed
43
- authorId: String! @indexed
44
- summary: String!
45
- refId: String @indexed
46
- scope: String @indexed
47
- createdAt: String! @indexed
48
- receivedAt: String!
49
- expiresAt: String @indexed
8
+ activity: String @indexed # "coding" | "reviewing" | "planning" | "debugging" | "idle"
9
+ activityUpdatedAt: BigInt # unix ms — when activity/currentTask were last ASSERTED (natural-presence); nullable/additive, absent on records written before this feature (readers fall back to lastHeartbeatAt)
10
+ flairVersion: String # @tpsdev-ai/flair version serving this heartbeat (flair#639); absent on pre-#639 records
11
+ harperVersion: String # @harperfast/harper version serving this heartbeat (flair#639); absent on pre-#639 records
50
12
  }
@@ -11,4 +11,7 @@ type WorkspaceState @table(database: "flair") @export {
11
11
  summary: String
12
12
  filesChanged: [String]
13
13
  createdAt: String!
14
+ entities: [String] @indexed # attention-plane vocabulary strings (flair#675); additive/nullable —
15
+ # existing rows carry no entities, readers tolerate absent (same
16
+ # pattern as Presence.activityUpdatedAt). See docs/entity-vocabulary.md.
14
17
  }
@@ -1,162 +0,0 @@
1
- /**
2
- * IngestEvents.ts — Observatory ingestion endpoint.
3
- *
4
- * POST /IngestEvents
5
- * Auth: TPS-Ed25519 signed by the office's private key (verified against
6
- * ObsOffice.publicKey stored at registration time).
7
- *
8
- * Body: {
9
- * officeId: string;
10
- * events: OrgEventRecord[];
11
- * agents: AgentStatus[];
12
- * syncedAt: string;
13
- * }
14
- *
15
- * Actions:
16
- * 1. Verify Ed25519 signature against stored office public key
17
- * 2. Upsert ObsAgentSnapshot for each agent
18
- * 3. Insert ObsEventFeed for each new event (30-day TTL)
19
- * 4. Update ObsOffice.lastSeen + agentCount
20
- *
21
- * Rate limit: 1 call / 10s per office (enforced by createdAt delta check)
22
- * Batch limit: 100 events per call
23
- */
24
- import { Resource, databases } from "@harperfast/harper";
25
- import { createPublicKey, verify } from "node:crypto";
26
- const BATCH_LIMIT = 100;
27
- const RATE_LIMIT_MS = 10_000;
28
- const EVENT_TTL_DAYS = 30;
29
- function verifyEd25519Signature(publicKeyHex, authHeader, officeId) {
30
- try {
31
- // Header format: TPS-Ed25519 officeId:ts:nonce:sig
32
- const prefix = "TPS-Ed25519 ";
33
- if (!authHeader.startsWith(prefix))
34
- return false;
35
- const parts = authHeader.slice(prefix.length).split(":");
36
- if (parts.length < 4)
37
- return false;
38
- const [id, ts, nonce, ...sigParts] = parts;
39
- const sig = sigParts.join(":");
40
- if (id !== officeId)
41
- return false;
42
- // Replay protection: reject signatures older than 5 minutes
43
- const age = Date.now() - Number(ts);
44
- if (age > 5 * 60 * 1000 || age < -30_000)
45
- return false;
46
- const pubKeyBytes = Buffer.from(publicKeyHex.replace(/=\s*/g, ""), "hex");
47
- const spkiHeader = Buffer.from("302a300506032b6570032100", "hex");
48
- const pubKey = createPublicKey({ key: Buffer.concat([spkiHeader, pubKeyBytes]), format: "der", type: "spki" });
49
- const payload = Buffer.from(`${id}:${ts}:${nonce}:POST:/IngestEvents`);
50
- const sigBuf = Buffer.from(sig, "base64");
51
- return verify(null, payload, pubKey, sigBuf);
52
- }
53
- catch {
54
- return false;
55
- }
56
- }
57
- export class IngestEvents extends Resource {
58
- // Public POST by design: the handler self-verifies the OFFICE's Ed25519
59
- // signature against ObsOffice.publicKey (not an agent identity), so it can't use
60
- // agent auth. allowCreate=true lets the request reach post() where that
61
- // verification happens — same self-gating pattern as FederationPair/FederationSync.
62
- allowCreate() {
63
- return true;
64
- }
65
- async post(body, context) {
66
- // Harper v5 does not populate this.request on Resource subclasses —
67
- // getContext() is the only reliable path (ops-sal4: the previous
68
- // `(this as any).request` read was always undefined, so authHeader was
69
- // always undefined and every request 401'd before reaching the office
70
- // Ed25519 signature check below).
71
- const ctx = this.getContext?.();
72
- const request = ctx?.request ?? ctx;
73
- const authHeader = request?.headers?.get?.("authorization") ?? request?.headers?.authorization;
74
- // Parse and validate body
75
- let payload;
76
- try {
77
- payload = (typeof body === "string" ? JSON.parse(body) : body);
78
- }
79
- catch {
80
- return new Response(JSON.stringify({ error: "invalid JSON" }), { status: 400, headers: { "Content-Type": "application/json" } });
81
- }
82
- const { officeId, events = [], agents = [], syncedAt } = payload;
83
- if (!officeId) {
84
- return new Response(JSON.stringify({ error: "officeId required" }), { status: 400, headers: { "Content-Type": "application/json" } });
85
- }
86
- // Look up the office
87
- const office = await databases.flair.ObsOffice.get(officeId).catch(() => null);
88
- if (!office) {
89
- return new Response(JSON.stringify({ error: "office not registered — POST /ObsOffice first" }), { status: 403, headers: { "Content-Type": "application/json" } });
90
- }
91
- // Verify Ed25519 signature
92
- if (!authHeader || !verifyEd25519Signature(String(office.publicKey), authHeader, officeId)) {
93
- return new Response(JSON.stringify({ error: "invalid signature" }), { status: 401, headers: { "Content-Type": "application/json" } });
94
- }
95
- // Rate limit check
96
- if (office.lastSeen) {
97
- const msSinceLastSync = Date.now() - new Date(office.lastSeen).getTime();
98
- if (msSinceLastSync < RATE_LIMIT_MS) {
99
- return new Response(JSON.stringify({ error: "rate limit: 1 call per 10s" }), { status: 429, headers: { "Content-Type": "application/json" } });
100
- }
101
- }
102
- // Batch limit
103
- if (events.length > BATCH_LIMIT) {
104
- return new Response(JSON.stringify({ error: `batch limit: max ${BATCH_LIMIT} events` }), { status: 400, headers: { "Content-Type": "application/json" } });
105
- }
106
- const now = new Date().toISOString();
107
- const expiresAt = new Date(Date.now() + EVENT_TTL_DAYS * 24 * 60 * 60 * 1000).toISOString();
108
- // Upsert agent snapshots
109
- for (const agent of agents) {
110
- if (!agent.agentId)
111
- continue;
112
- const snapshotId = `${officeId}:${agent.agentId}`;
113
- await databases.flair.ObsAgentSnapshot.put({
114
- id: snapshotId,
115
- officeId,
116
- agentId: agent.agentId,
117
- name: agent.name ?? agent.agentId,
118
- role: agent.role,
119
- status: agent.status ?? "unknown",
120
- model: agent.model,
121
- lastActivity: agent.lastSeen ?? now,
122
- lastHeartbeat: now,
123
- updatedAt: now,
124
- }).catch((e) => console.warn(`[IngestEvents] snapshot upsert failed for ${snapshotId}: ${e.message}`));
125
- }
126
- // Insert event feed entries (skip duplicates)
127
- let inserted = 0;
128
- for (const ev of events) {
129
- if (!ev.id || !ev.kind)
130
- continue;
131
- const feedId = `${officeId}:${ev.id}`;
132
- const existing = await databases.flair.ObsEventFeed.get(feedId).catch(() => null);
133
- if (existing)
134
- continue;
135
- await databases.flair.ObsEventFeed.put({
136
- id: feedId,
137
- officeId,
138
- kind: ev.kind,
139
- authorId: ev.authorId,
140
- summary: ev.summary,
141
- refId: ev.refId,
142
- scope: ev.scope,
143
- createdAt: ev.createdAt,
144
- receivedAt: now,
145
- expiresAt,
146
- }).catch((e) => console.warn(`[IngestEvents] event insert failed for ${feedId}: ${e.message}`));
147
- inserted++;
148
- }
149
- // Update office lastSeen + agentCount
150
- await databases.flair.ObsOffice.put({
151
- ...office,
152
- status: "online",
153
- lastSeen: now,
154
- agentCount: agents.length,
155
- updatedAt: now,
156
- }).catch(() => { });
157
- return new Response(JSON.stringify({ ok: true, events: inserted, agents: agents.length }), {
158
- status: 200,
159
- headers: { "Content-Type": "application/json" },
160
- });
161
- }
162
- }
@@ -1,19 +0,0 @@
1
- import { Resource, databases } from "@harperfast/harper";
2
- export class IssueTokens extends Resource {
3
- static loadAsInstance = false;
4
- async get(_target) {
5
- const { refresh_token: refreshToken, operation_token: jwt } = await databases.system.hdb_user.operation({ operation: "create_authentication_tokens" }, this.getContext());
6
- return { refreshToken, jwt };
7
- }
8
- async post(_target, data) {
9
- if (!data?.username || !data?.password) {
10
- throw new Error("username and password are required");
11
- }
12
- const { refresh_token: refreshToken, operation_token: jwt } = await databases.system.hdb_user.operation({
13
- operation: "create_authentication_tokens",
14
- username: data.username,
15
- password: data.password,
16
- });
17
- return { refreshToken, jwt };
18
- }
19
- }
@@ -1,13 +0,0 @@
1
- import { databases } from "@harperfast/harper";
2
- import { allowVerified, allowAdmin } from "./agent-auth.js";
3
- /**
4
- * ObsAgentSnapshot — observatory per-agent snapshot read-model. Writes are
5
- * system-driven (internal sync); agents may read. See ObsOffice for the
6
- * allowRead=allowVerified security default (flag for Sherlock).
7
- */
8
- export class ObsAgentSnapshot extends databases.flair.ObsAgentSnapshot {
9
- allowRead() { return allowVerified(this.getContext?.()); }
10
- allowCreate() { return allowAdmin(this.getContext?.()); }
11
- allowUpdate() { return allowAdmin(this.getContext?.()); }
12
- allowDelete() { return allowAdmin(this.getContext?.()); }
13
- }
@@ -1,13 +0,0 @@
1
- import { databases } from "@harperfast/harper";
2
- import { allowVerified, allowAdmin } from "./agent-auth.js";
3
- /**
4
- * ObsEventFeed — observatory event-feed read-model. Writes are system-driven
5
- * (internal sync); agents may read. See ObsOffice for the allowRead=allowVerified
6
- * security default (flag for Sherlock).
7
- */
8
- export class ObsEventFeed extends databases.flair.ObsEventFeed {
9
- allowRead() { return allowVerified(this.getContext?.()); }
10
- allowCreate() { return allowAdmin(this.getContext?.()); }
11
- allowUpdate() { return allowAdmin(this.getContext?.()); }
12
- allowDelete() { return allowAdmin(this.getContext?.()); }
13
- }
@@ -1,19 +0,0 @@
1
- import { databases } from "@harperfast/harper";
2
- import { allowVerified, allowAdmin } from "./agent-auth.js";
3
- /**
4
- * ObsOffice — observatory office-layout read-model. Writes are system-driven
5
- * (internal sync); agents may read. Self-authorizes now that the global gate is
6
- * non-rejecting.
7
- *
8
- * SECURITY DEFAULT (flag for Sherlock): allowRead is allowVerified (anonymous
9
- * denied). The ObservationCenter HTML shell is public but its data API calls
10
- * authenticate (admin-pass). If the public Office Space renderer needs anonymous
11
- * Obs reads, relax to allowRead=true WITH field-allowlisting (as Presence.get does)
12
- * — do NOT expose raw rows publicly.
13
- */
14
- export class ObsOffice extends databases.flair.ObsOffice {
15
- allowRead() { return allowVerified(this.getContext?.()); }
16
- allowCreate() { return allowAdmin(this.getContext?.()); }
17
- allowUpdate() { return allowAdmin(this.getContext?.()); }
18
- allowDelete() { return allowAdmin(this.getContext?.()); }
19
- }
@@ -1,23 +0,0 @@
1
- import { Resource } from "@harperfast/harper";
2
- import { readFileSync } from "node:fs";
3
- import { fileURLToPath } from "node:url";
4
- import { dirname, resolve } from "node:path";
5
- const HERE = dirname(fileURLToPath(import.meta.url));
6
- const HTML_PATH = resolve(HERE, "..", "..", "ui", "observation-center.html");
7
- const HTML = readFileSync(HTML_PATH, "utf8");
8
- /**
9
- * GET /ObservationCenter — serves the read-only dashboard shell.
10
- * The HTML handles its own Basic-auth prompt and polls authenticated JSON endpoints.
11
- */
12
- export class ObservationCenter extends Resource {
13
- // The dashboard shell is just HTML + inline JS; the JS prompts the user
14
- // for admin-pass and authenticates each API call. The shell itself is
15
- // intentionally public so it can render before auth happens.
16
- allowRead() { return true; }
17
- async get() {
18
- return new Response(HTML, {
19
- status: 200,
20
- headers: { "content-type": "text/html; charset=utf-8" },
21
- });
22
- }
23
- }