@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.
- package/README.md +39 -11
- package/SECURITY.md +52 -20
- package/config.yaml +32 -5
- package/dist/cli.js +2235 -260
- package/dist/deploy.js +96 -15
- package/dist/doctor-client.js +312 -0
- package/dist/fleet-presence.js +98 -0
- package/dist/fleet-verify.js +291 -0
- package/dist/install/clients.js +18 -0
- package/dist/mcp-client-assertion.js +364 -0
- package/dist/probe.js +97 -0
- package/dist/rem/restore.js +1 -1
- package/dist/rem/runner.js +82 -12
- package/dist/resources/Admin.js +1 -1
- package/dist/resources/AdminConnectors.js +1 -1
- package/dist/resources/AdminDashboard.js +1 -1
- package/dist/resources/AdminIdp.js +1 -1
- package/dist/resources/AdminInstance.js +1 -1
- package/dist/resources/AdminMemory.js +2 -2
- package/dist/resources/AdminPrincipals.js +1 -1
- package/dist/resources/Agent.js +14 -0
- package/dist/resources/AgentCard.js +1 -1
- package/dist/resources/AgentSeed.js +1 -1
- package/dist/resources/AttentionQuery.js +356 -0
- package/dist/resources/Credential.js +11 -1
- package/dist/resources/Federation.js +121 -2
- package/dist/resources/Integration.js +1 -1
- package/dist/resources/MCPClientMetadata.js +88 -0
- package/dist/resources/Memory.js +127 -22
- package/dist/resources/MemoryBootstrap.js +441 -85
- package/dist/resources/MemoryGrant.js +1 -1
- package/dist/resources/MemoryReflect.js +105 -49
- package/dist/resources/MemoryUsage.js +104 -0
- package/dist/resources/OAuth.js +69 -5
- package/dist/resources/OrgEvent.js +11 -0
- package/dist/resources/OrgEventCatchup.js +1 -1
- package/dist/resources/Presence.js +268 -17
- package/dist/resources/RecordUsage.js +230 -0
- package/dist/resources/Relationship.js +108 -26
- package/dist/resources/SemanticSearch.js +88 -320
- package/dist/resources/SkillScan.js +15 -0
- package/dist/resources/Soul.js +14 -0
- package/dist/resources/WorkspaceLatest.js +1 -1
- package/dist/resources/WorkspaceState.js +12 -1
- package/dist/resources/agent-auth.js +61 -3
- package/dist/resources/agentcard-fields.js +2 -2
- package/dist/resources/auth-middleware.js +42 -14
- package/dist/resources/bm25-filter.js +1 -1
- package/dist/resources/bm25.js +13 -7
- package/dist/resources/collision-lib.js +157 -0
- package/dist/resources/dedup.js +2 -2
- package/dist/resources/ed25519-auth.js +2 -2
- 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/federation-nonce-store.js +195 -0
- package/dist/resources/health.js +97 -6
- package/dist/resources/instance-identity.js +53 -0
- 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-bootstrap-lib.js +1 -1
- package/dist/resources/memory-read-scope.js +58 -71
- package/dist/resources/memory-reflect-lib.js +289 -0
- package/dist/resources/memory-visibility.js +37 -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 +456 -0
- package/dist/resources/migrations/snapshot.js +127 -0
- package/dist/resources/migrations/source-fields.js +93 -0
- package/dist/resources/migrations/space.js +73 -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-check.js +167 -0
- package/dist/version-handshake.js +122 -0
- package/package.json +4 -5
- package/schemas/agent.graphql +7 -0
- package/schemas/event.graphql +5 -0
- package/schemas/federation.graphql +12 -0
- package/schemas/memory.graphql +82 -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
|
@@ -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
|
package/schemas/memory.graphql
CHANGED
|
@@ -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
|
package/schemas/schema.graphql
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
}
|