@tpsdev-ai/flair 0.21.0 → 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 +10 -7
- package/SECURITY.md +24 -2
- package/config.yaml +32 -5
- package/dist/cli.js +1755 -213
- 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 +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-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
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* POST /MemoryReflect
|
|
3
3
|
*
|
|
4
|
-
* Gathers recent memories for an agent and returns a structured
|
|
5
|
-
* prompt
|
|
6
|
-
*
|
|
4
|
+
* Gathers recent memories for an agent and either (a) returns a structured
|
|
5
|
+
* reflection prompt for a human/agent to run through their own LLM, or (b)
|
|
6
|
+
* with execute:true, runs the distillation server-side via Harper's models
|
|
7
|
+
* facade and stages the results as MemoryCandidate rows for review. See
|
|
8
|
+
* specs/FLAIR-NIGHTLY-REM-SLICE-2-DISTILLATION.md §3A (issue #707).
|
|
7
9
|
*
|
|
8
10
|
* Request:
|
|
9
11
|
* agentId string — which agent to reflect on
|
|
@@ -12,22 +14,33 @@
|
|
|
12
14
|
* maxMemories number? — cap (default: 50)
|
|
13
15
|
* focus string? — "lessons_learned" | "patterns" | "decisions" | "errors" (default: "lessons_learned")
|
|
14
16
|
* tag string? — required when scope="tagged"
|
|
17
|
+
* execute boolean? — default false. When true, distill server-side and
|
|
18
|
+
* stage MemoryCandidate rows instead of returning a prompt.
|
|
15
19
|
*
|
|
16
|
-
* Response:
|
|
20
|
+
* Response (execute: false, default — unchanged from pre-#707 behavior):
|
|
17
21
|
* memories Memory[] — source memories included in the prompt
|
|
18
22
|
* prompt string — structured LLM prompt
|
|
19
23
|
* suggestedTags string[] — tags Flair detected in the source set
|
|
20
24
|
* count number — number of memories included
|
|
25
|
+
*
|
|
26
|
+
* Response (execute: true):
|
|
27
|
+
* candidates MemoryCandidate[] — staged rows (rationalePrompt omitted — see below)
|
|
28
|
+
* count number
|
|
29
|
+
* model string — resolved model id (see generatedBy note below)
|
|
30
|
+
*
|
|
31
|
+
* The pure logic behind execute mode (prompt building, actor resolution,
|
|
32
|
+
* generate+validate+retry, dedup) lives in ./memory-reflect-lib.ts — see that
|
|
33
|
+
* file's header for why: importing Resource/databases/models here pulls in
|
|
34
|
+
* the Harper runtime and can't be unit-tested directly (Harper injects
|
|
35
|
+
* `Resource` as a runtime global; bun's ESM linker rejects `import {
|
|
36
|
+
* Resource }` outright — see test/unit/resource-allow.test.ts). This
|
|
37
|
+
* resource is a thin orchestrator over the lib's tested functions.
|
|
21
38
|
*/
|
|
22
|
-
import { Resource, databases } from "@harperfast/harper";
|
|
39
|
+
import { Resource, databases, models, logger } from "@harperfast/harper";
|
|
40
|
+
import { randomBytes } from "node:crypto";
|
|
23
41
|
import { isAdmin, allowVerified } from "./agent-auth.js";
|
|
24
42
|
import { patchRecordSilent } from "./table-helpers.js";
|
|
25
|
-
|
|
26
|
-
lessons_learned: "Review these memories and identify concrete lessons learned. For each lesson: what happened, what you learned, and how it should change future behavior. Write atomic memories with durability=persistent.",
|
|
27
|
-
patterns: "Identify recurring patterns across these memories. What themes, approaches, or outcomes appear multiple times? Extract each pattern as a persistent memory.",
|
|
28
|
-
decisions: "Catalog the key decisions made and their outcomes. For each: what was decided, why, and what resulted. Promote important decisions to persistent.",
|
|
29
|
-
errors: "Extract errors, bugs, and failures. For each: what failed, root cause, and fix applied. These are high-value persistent memories.",
|
|
30
|
-
};
|
|
43
|
+
import { buildReflectionPrompt, buildExecutePrompt, resolveReflectActor, generateCandidates, dedupeCandidates, } from "./memory-reflect-lib.js";
|
|
31
44
|
export class ReflectMemories extends Resource {
|
|
32
45
|
// Self-authorize via the Ed25519 agent verify (auth reshape removes the gate's
|
|
33
46
|
// admin elevation). Any verified agent may reflect; the isAdmin checks in post()
|
|
@@ -36,7 +49,7 @@ export class ReflectMemories extends Resource {
|
|
|
36
49
|
return allowVerified(this.getContext?.());
|
|
37
50
|
}
|
|
38
51
|
async post(data) {
|
|
39
|
-
const { agentId: bodyAgentId, scope = "recent", since, maxMemories = 50, focus = "lessons_learned", tag, } = data || {};
|
|
52
|
+
const { agentId: bodyAgentId, scope = "recent", since, maxMemories = 50, focus = "lessons_learned", tag, execute = false, } = data || {};
|
|
40
53
|
// Authenticated identity comes from getContext().request, not this.request
|
|
41
54
|
// (see SemanticSearch / MemoryBootstrap for the same bug class). The prior
|
|
42
55
|
// check was silently bypassed — bob could reflect on alice's memories and
|
|
@@ -46,13 +59,13 @@ export class ReflectMemories extends Resource {
|
|
|
46
59
|
const actorId = request?.tpsAgent;
|
|
47
60
|
const callerIsAdmin = request?.tpsAgentIsAdmin === true
|
|
48
61
|
|| (actorId ? await isAdmin(actorId) : false);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
if (
|
|
53
|
-
return new Response(JSON.stringify(
|
|
62
|
+
// Same actor rules for both modes (spec §3A item 9) — resolved once via
|
|
63
|
+
// the shared, tested helper before either branch runs.
|
|
64
|
+
const actorResolution = resolveReflectActor({ bodyAgentId, actorId, callerIsAdmin });
|
|
65
|
+
if (actorResolution.error) {
|
|
66
|
+
return new Response(JSON.stringify(actorResolution.error.body), { status: actorResolution.error.status });
|
|
54
67
|
}
|
|
55
|
-
const agentId =
|
|
68
|
+
const agentId = actorResolution.agentId;
|
|
56
69
|
const sinceDate = since ? new Date(since) : new Date(Date.now() - 24 * 3600_000);
|
|
57
70
|
const memories = [];
|
|
58
71
|
for await (const record of databases.flair.Memory.search()) {
|
|
@@ -83,38 +96,81 @@ export class ReflectMemories extends Resource {
|
|
|
83
96
|
for (const t of m.tags ?? [])
|
|
84
97
|
tagSet.add(t);
|
|
85
98
|
}
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
.join("\n");
|
|
91
|
-
const prompt = `# Memory Reflection — ${agentId}
|
|
92
|
-
Focus: ${focus}
|
|
93
|
-
Scope: ${scope} (since ${sinceDate.toISOString()})
|
|
94
|
-
Memories: ${memories.length}
|
|
95
|
-
|
|
96
|
-
## Task
|
|
97
|
-
${focusText}
|
|
98
|
-
|
|
99
|
-
## Source Memories
|
|
100
|
-
${memorySummary || "(none)"}
|
|
101
|
-
|
|
102
|
-
## Instructions
|
|
103
|
-
For each insight:
|
|
104
|
-
1. Write a new memory with durability=persistent
|
|
105
|
-
2. Set derivedFrom=[<source memory ids>]
|
|
106
|
-
3. Set tags from the source memories where relevant
|
|
107
|
-
4. Keep each memory atomic — one insight per record`;
|
|
108
|
-
// Update lastReflected on source memories (read-modify-write to preserve embeddings)
|
|
109
|
-
const now = new Date().toISOString();
|
|
99
|
+
// Update lastReflected on source memories (read-modify-write to preserve
|
|
100
|
+
// embeddings). Unconditional for both modes — calling /ReflectMemories at
|
|
101
|
+
// all means these memories were considered, regardless of what happens next.
|
|
102
|
+
const nowISO = new Date().toISOString();
|
|
110
103
|
for (const m of memories) {
|
|
111
|
-
patchRecordSilent(databases.flair.Memory, m.id, { lastReflected:
|
|
104
|
+
patchRecordSilent(databases.flair.Memory, m.id, { lastReflected: nowISO });
|
|
105
|
+
}
|
|
106
|
+
const promptInputs = memories.map((m) => ({ id: m.id, createdAt: m.createdAt, content: m.content }));
|
|
107
|
+
if (!execute) {
|
|
108
|
+
const prompt = buildReflectionPrompt({ agentId, focus, scope, sinceISO: sinceDate.toISOString(), memories: promptInputs });
|
|
109
|
+
return {
|
|
110
|
+
memories,
|
|
111
|
+
prompt,
|
|
112
|
+
suggestedTags: [...tagSet].slice(0, 20),
|
|
113
|
+
count: memories.length,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// ── execute mode (spec §3A) ─────────────────────────────────────────────
|
|
117
|
+
const executePrompt = buildExecutePrompt({ agentId, focus, scope, sinceISO: sinceDate.toISOString(), memories: promptInputs });
|
|
118
|
+
const gatheredMemoryIds = new Set(promptInputs.map((m) => m.id));
|
|
119
|
+
const configuredModel = process.env.FLAIR_REM_MODEL || undefined;
|
|
120
|
+
const outcome = await generateCandidates({
|
|
121
|
+
prompt: executePrompt,
|
|
122
|
+
model: configuredModel,
|
|
123
|
+
gatheredMemoryIds,
|
|
124
|
+
generate: (input, opts) => models.generate(input, opts),
|
|
125
|
+
});
|
|
126
|
+
if (!outcome.ok) {
|
|
127
|
+
if (outcome.reason === "no_backend") {
|
|
128
|
+
// Static body (K&S) — never echo Harper version, backend lists, or endpoints.
|
|
129
|
+
return new Response(JSON.stringify({ error: "No generative backend configured. See the models configuration docs." }), { status: 503 });
|
|
130
|
+
}
|
|
131
|
+
return new Response(JSON.stringify({ error: "distillation_failed", detail: "model output did not validate after one retry" }), { status: 502 });
|
|
132
|
+
}
|
|
133
|
+
if (outcome.usedJsonFallback) {
|
|
134
|
+
logger.warn?.(`MemoryReflect: json-fallback path active for agent ${agentId} (schema-mode output failed validation)`);
|
|
135
|
+
}
|
|
136
|
+
// Dedup against this agent's existing pending candidates (spec §3A item 4).
|
|
137
|
+
const existingPendingClaims = [];
|
|
138
|
+
for await (const c of databases.flair.MemoryCandidate.search({})) {
|
|
139
|
+
if (c.agentId !== agentId)
|
|
140
|
+
continue;
|
|
141
|
+
if (c.status !== "pending")
|
|
142
|
+
continue;
|
|
143
|
+
existingPendingClaims.push(c.claim);
|
|
144
|
+
}
|
|
145
|
+
const toStage = dedupeCandidates(outcome.candidates, existingPendingClaims);
|
|
146
|
+
// generatedBy: GenerateResult in the pinned @harperfast/harper 5.1.17 has
|
|
147
|
+
// no model/backend-id field (content/finishReason/usage/toolCalls/trace
|
|
148
|
+
// only) — the "from the generate result if available" branch is
|
|
149
|
+
// unreachable in this version, so this always falls back to the
|
|
150
|
+
// configured logical name, matching Harper's own default routing name.
|
|
151
|
+
const resolvedModel = configuredModel ?? "default";
|
|
152
|
+
const generatedAt = new Date().toISOString();
|
|
153
|
+
const staged = [];
|
|
154
|
+
for (const c of toStage) {
|
|
155
|
+
const row = {
|
|
156
|
+
id: `cand_${randomBytes(8).toString("hex")}`,
|
|
157
|
+
agentId,
|
|
158
|
+
claim: c.claim,
|
|
159
|
+
sourceMemoryIds: c.sourceMemoryIds,
|
|
160
|
+
rationalePrompt: executePrompt,
|
|
161
|
+
generatedBy: resolvedModel,
|
|
162
|
+
generatedAt,
|
|
163
|
+
status: "pending",
|
|
164
|
+
};
|
|
165
|
+
await databases.flair.MemoryCandidate.put(row);
|
|
166
|
+
staged.push(row);
|
|
112
167
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
};
|
|
168
|
+
// Response omits rationalePrompt (spec §3A item 5: "no prompt field") —
|
|
169
|
+
// it's identical across every row in this batch and already persisted
|
|
170
|
+
// for audit on the MemoryCandidate row itself; echoing it back per
|
|
171
|
+
// candidate would just repeat the same large string N times. Matches
|
|
172
|
+
// `flair rem candidates`' own listing, which doesn't surface it either.
|
|
173
|
+
const responseCandidates = staged.map(({ rationalePrompt, ...rest }) => rest);
|
|
174
|
+
return { candidates: responseCandidates, count: responseCandidates.length, model: resolvedModel };
|
|
119
175
|
}
|
|
120
176
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resources/MemoryUsage.ts — the dedup ledger for the usage-feedback signal
|
|
3
|
+
* (flair#683). One row per (agentId, memoryId) contribution; see
|
|
4
|
+
* schemas/memory.graphql's MemoryUsage type doc for the full field/design
|
|
5
|
+
* rationale.
|
|
6
|
+
*
|
|
7
|
+
* This file is a LOCKED-DOWN table guard, not the public write surface: the
|
|
8
|
+
* actual "record that a memory was used" action lives in
|
|
9
|
+
* resources/RecordUsage.ts (POST /RecordUsage) — see that file's module doc
|
|
10
|
+
* for why the action lives on a SEPARATE, non-table-backed resource (a
|
|
11
|
+
* confirmed-live Harper gotcha: the base TableResource has no default
|
|
12
|
+
* `post()` for a static-style raw-table call outside an
|
|
13
|
+
* `isCollection`-instantiated resource — resources/Memory.ts documents the
|
|
14
|
+
* HTTP-facing shape of this same limitation for the Memory table).
|
|
15
|
+
* RecordUsage writes ledger rows via `.put()` on the RAW table object
|
|
16
|
+
* (`databases.flair.MemoryUsage` — an upsert against the deterministic
|
|
17
|
+
* composite id, not a `.post()`), the same "call the other table directly,
|
|
18
|
+
* bypass its resource class's own auth wrapper" pattern resources/Memory.ts
|
|
19
|
+
* already uses for MemoryGrant (hasWriteGrant()) — so nothing below is
|
|
20
|
+
* bypassed by that internal path; it only gates the DIRECT `/MemoryUsage`
|
|
21
|
+
* HTTP route.
|
|
22
|
+
*
|
|
23
|
+
* Why agents get READ but NOT UPDATE/DELETE here (mirrored in
|
|
24
|
+
* src/cli.ts's FLAIR_AGENT_PERMISSION native-role grant): the ledger IS the
|
|
25
|
+
* dedup/anti-gaming primitive — Sherlock's "(agent, memory) contributes ≤ 1"
|
|
26
|
+
* rule is enforced by RecordUsage checking for an EXISTING ledger row before
|
|
27
|
+
* bumping usageCount. If an agent could DELETE its own row over HTTP, it
|
|
28
|
+
* could re-trigger RecordUsage for the same memory indefinitely (create row
|
|
29
|
+
* → count once → delete row → count again → repeat), completely defeating
|
|
30
|
+
* the dedup cap. Locking put()/delete() to admin/internal here is therefore
|
|
31
|
+
* LOAD-BEARING, not just defense in depth — it's the only thing enforcing
|
|
32
|
+
* this in an environment where the native Harper role hasn't been
|
|
33
|
+
* (re-)provisioned yet (auth-middleware.ts's documented pre-migration
|
|
34
|
+
* admin-fallback — and every ephemeral test Harper spawned via
|
|
35
|
+
* test/helpers/harper-lifecycle.ts, which never runs `flair init`'s role
|
|
36
|
+
* provisioning at all).
|
|
37
|
+
*
|
|
38
|
+
* Read scope is deliberately narrower than Memory's "open-within-org" model:
|
|
39
|
+
* an agent sees only ITS OWN contributions (or admin sees everything). The
|
|
40
|
+
* ledger is an audit trail, not a shared surface — there is no product need
|
|
41
|
+
* to expose "which agent used which memory" cross-agent, and narrowing this
|
|
42
|
+
* costs nothing.
|
|
43
|
+
*/
|
|
44
|
+
import { databases } from "@harperfast/harper";
|
|
45
|
+
import { resolveAgentAuth, allowVerified } from "./agent-auth.js";
|
|
46
|
+
const FORBIDDEN = (msg) => new Response(JSON.stringify({ error: msg }), { status: 403, headers: { "Content-Type": "application/json" } });
|
|
47
|
+
const UNAUTH = () => new Response(JSON.stringify({ error: "authentication required" }), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
48
|
+
const NOT_FOUND = () => new Response(JSON.stringify({ error: "not found" }), { status: 404, headers: { "Content-Type": "application/json" } });
|
|
49
|
+
export class MemoryUsage extends databases.flair.MemoryUsage {
|
|
50
|
+
/** Self-authorize now that the global gate is non-rejecting — same pattern
|
|
51
|
+
* as every other table resource in this codebase (Memory.ts/MemoryGrant.ts
|
|
52
|
+
* etc.). Per-record scoping happens in get()/search() below. */
|
|
53
|
+
allowRead() { return allowVerified(this.getContext?.()); }
|
|
54
|
+
async get(target) {
|
|
55
|
+
if (!target || (typeof target === "object" && target.isCollection)) {
|
|
56
|
+
return this.search(target);
|
|
57
|
+
}
|
|
58
|
+
const auth = await resolveAgentAuth(this.getContext?.());
|
|
59
|
+
if (auth.kind === "anonymous")
|
|
60
|
+
return NOT_FOUND();
|
|
61
|
+
if (auth.kind === "internal" || (auth.kind === "agent" && auth.isAdmin))
|
|
62
|
+
return super.get(target);
|
|
63
|
+
const record = await super.get(target);
|
|
64
|
+
if (!record || record.agentId !== auth.agentId)
|
|
65
|
+
return NOT_FOUND();
|
|
66
|
+
return record;
|
|
67
|
+
}
|
|
68
|
+
async search(query) {
|
|
69
|
+
const auth = await resolveAgentAuth(this.getContext?.());
|
|
70
|
+
if (auth.kind === "anonymous")
|
|
71
|
+
return UNAUTH();
|
|
72
|
+
if (auth.kind === "internal" || (auth.kind === "agent" && auth.isAdmin))
|
|
73
|
+
return super.search(query);
|
|
74
|
+
const scope = { attribute: "agentId", comparator: "equals", value: auth.agentId };
|
|
75
|
+
if (query && typeof query === "object" && !Array.isArray(query)) {
|
|
76
|
+
const existing = query.conditions ?? [];
|
|
77
|
+
query.conditions = Array.isArray(existing) ? [scope, ...existing] : [scope, existing];
|
|
78
|
+
return super.search(query);
|
|
79
|
+
}
|
|
80
|
+
const conditions = Array.isArray(query) && query.length > 0 ? [scope, ...query] : [scope];
|
|
81
|
+
return super.search(conditions);
|
|
82
|
+
}
|
|
83
|
+
// Append-only ledger: rows are created via RecordUsage's RAW table call
|
|
84
|
+
// (bypasses this class entirely — see module doc), never via this
|
|
85
|
+
// instance-level HTTP route, for non-admin callers.
|
|
86
|
+
async post(content) {
|
|
87
|
+
const auth = await resolveAgentAuth(this.getContext?.());
|
|
88
|
+
if (auth.kind === "internal" || (auth.kind === "agent" && auth.isAdmin))
|
|
89
|
+
return super.post(content);
|
|
90
|
+
return FORBIDDEN("forbidden: MemoryUsage rows are written by the /RecordUsage endpoint, not directly");
|
|
91
|
+
}
|
|
92
|
+
async put(content) {
|
|
93
|
+
const auth = await resolveAgentAuth(this.getContext?.());
|
|
94
|
+
if (auth.kind === "internal" || (auth.kind === "agent" && auth.isAdmin))
|
|
95
|
+
return super.put(content);
|
|
96
|
+
return FORBIDDEN("forbidden: MemoryUsage rows are immutable once written");
|
|
97
|
+
}
|
|
98
|
+
async delete(id) {
|
|
99
|
+
const auth = await resolveAgentAuth(this.getContext?.());
|
|
100
|
+
if (auth.kind === "internal" || (auth.kind === "agent" && auth.isAdmin))
|
|
101
|
+
return super.delete(id);
|
|
102
|
+
return FORBIDDEN("forbidden: MemoryUsage rows cannot be deleted by non-admins (dedup-integrity invariant — see module doc)");
|
|
103
|
+
}
|
|
104
|
+
}
|
package/dist/resources/OAuth.js
CHANGED
|
@@ -216,7 +216,14 @@ ${scope.split(" ").map((s) => `<div class="scope">${s}</div>`).join("")}
|
|
|
216
216
|
status: 400, headers: { "content-type": "application/json" },
|
|
217
217
|
});
|
|
218
218
|
}
|
|
219
|
-
|
|
219
|
+
// flair#610 — mint an authorization code ONLY on an explicit approve. The
|
|
220
|
+
// consent form rendered by get() submits action=approve|deny and nothing
|
|
221
|
+
// else, so treating every non-"approve" value (deny, missing, empty, or
|
|
222
|
+
// garbage) as a denial is safe — and it closes the previous loose
|
|
223
|
+
// `action !== "deny"` check, which minted a code for a malformed or absent
|
|
224
|
+
// action. redirect_uri was already validated above, so this access_denied
|
|
225
|
+
// redirect can only target the allowed URI.
|
|
226
|
+
if (action !== "approve") {
|
|
220
227
|
const params = new URLSearchParams({ error: "access_denied", state });
|
|
221
228
|
return redirectTo(`${redirectUri}?${params}`);
|
|
222
229
|
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { databases } from "@harperfast/harper";
|
|
14
14
|
import { resolveAgentAuth, allowVerified } from "./agent-auth.js";
|
|
15
|
+
import { invalidEntitiesResponse } from "./entity-vocab.js";
|
|
15
16
|
const FORBIDDEN = (msg) => new Response(JSON.stringify({ error: msg }), { status: 403, headers: { "Content-Type": "application/json" } });
|
|
16
17
|
const UNAUTH = () => new Response(JSON.stringify({ error: "authentication required" }), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
17
18
|
export class OrgEvent extends databases.flair.OrgEvent {
|
|
@@ -39,6 +40,12 @@ export class OrgEvent extends databases.flair.OrgEvent {
|
|
|
39
40
|
if (!content.id)
|
|
40
41
|
content.id = `${content.authorId}-${new Date().toISOString()}`;
|
|
41
42
|
content.createdAt = new Date().toISOString();
|
|
43
|
+
// attention-plane vocabulary gate (flair#675): `entities`, if present,
|
|
44
|
+
// must be well-formed vocabulary strings — see resources/entity-vocab.ts.
|
|
45
|
+
// Field is additive/optional; absent entities is not an error.
|
|
46
|
+
const entitiesError = invalidEntitiesResponse(content.entities);
|
|
47
|
+
if (entitiesError)
|
|
48
|
+
return entitiesError;
|
|
42
49
|
// Harper 5: table resources use put() for create/upsert (post() removed).
|
|
43
50
|
return databases.flair.OrgEvent.put(content);
|
|
44
51
|
}
|
|
@@ -49,6 +56,10 @@ export class OrgEvent extends databases.flair.OrgEvent {
|
|
|
49
56
|
if (auth.kind === "agent" && !auth.isAdmin && content.authorId !== auth.agentId) {
|
|
50
57
|
return FORBIDDEN("forbidden: authorId must match authenticated agent");
|
|
51
58
|
}
|
|
59
|
+
// attention-plane vocabulary gate (flair#675) — same as post() above.
|
|
60
|
+
const entitiesError = invalidEntitiesResponse(content.entities);
|
|
61
|
+
if (entitiesError)
|
|
62
|
+
return entitiesError;
|
|
52
63
|
return databases.flair.OrgEvent.put(content);
|
|
53
64
|
}
|
|
54
65
|
async delete(id, context) {
|