@tpsdev-ai/flair 0.11.0 → 0.12.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/dist/cli.js +199 -16
- package/dist/resources/Agent.js +20 -7
- package/dist/resources/AgentCard.js +6 -0
- package/dist/resources/AgentSeed.js +7 -1
- package/dist/resources/Credential.js +24 -13
- package/dist/resources/IngestEvents.js +7 -0
- package/dist/resources/Instance.js +13 -0
- package/dist/resources/Integration.js +64 -0
- package/dist/resources/Memory.js +53 -8
- package/dist/resources/MemoryBootstrap.js +8 -0
- package/dist/resources/MemoryConsolidate.js +7 -1
- package/dist/resources/MemoryFeed.js +8 -0
- package/dist/resources/MemoryGrant.js +79 -0
- package/dist/resources/MemoryMaintenance.js +1 -1
- package/dist/resources/MemoryReflect.js +7 -1
- package/dist/resources/MemoryReindex.js +7 -1
- package/dist/resources/OAuthClient.js +15 -0
- package/dist/resources/ObsAgentSnapshot.js +13 -0
- package/dist/resources/ObsEventFeed.js +13 -0
- package/dist/resources/ObsOffice.js +19 -0
- package/dist/resources/OrgEvent.js +37 -21
- package/dist/resources/OrgEventCatchup.js +8 -0
- package/dist/resources/OrgEventMaintenance.js +7 -0
- package/dist/resources/PairingToken.js +14 -0
- package/dist/resources/Peer.js +15 -0
- package/dist/resources/Presence.js +30 -0
- package/dist/resources/Relationship.js +13 -7
- package/dist/resources/SemanticSearch.js +25 -8
- package/dist/resources/Soul.js +18 -9
- package/dist/resources/SoulFeed.js +7 -0
- package/dist/resources/WorkspaceLatest.js +7 -0
- package/dist/resources/WorkspaceState.js +38 -28
- package/dist/resources/XAA.js +8 -0
- package/dist/resources/agent-auth.js +209 -0
- package/dist/resources/auth-middleware.js +83 -55
- package/package.json +1 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { databases } from "@harperfast/harper";
|
|
2
|
+
import { resolveAgentAuth } from "./agent-auth.js";
|
|
3
|
+
const FORBIDDEN = (msg) => new Response(JSON.stringify({ error: msg }), { status: 403, headers: { "Content-Type": "application/json" } });
|
|
4
|
+
const UNAUTH = () => new Response(JSON.stringify({ error: "authentication required" }), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
5
|
+
/**
|
|
6
|
+
* MemoryGrant — an agent (ownerId) grants another (granteeId) scoped access to its
|
|
7
|
+
* memories. Self-authorizes now that the global gate is non-rejecting (previously a
|
|
8
|
+
* pure @table protected only by the gate → anonymous could read/write any grant).
|
|
9
|
+
*
|
|
10
|
+
* Write: non-admin agents may only create/modify/delete grants they OWN
|
|
11
|
+
* (ownerId === self) — you can only share your own memories.
|
|
12
|
+
* Read: non-admin agents see grants where they are owner OR grantee.
|
|
13
|
+
* Internal calls (e.g. Memory.search's grant lookup) and admins pass unfiltered.
|
|
14
|
+
*/
|
|
15
|
+
export class MemoryGrant extends databases.flair.MemoryGrant {
|
|
16
|
+
_auth() {
|
|
17
|
+
return resolveAgentAuth(this.getContext?.());
|
|
18
|
+
}
|
|
19
|
+
async search(query) {
|
|
20
|
+
const auth = await this._auth();
|
|
21
|
+
if (auth.kind === "anonymous")
|
|
22
|
+
return UNAUTH();
|
|
23
|
+
if (auth.kind === "internal" || (auth.kind === "agent" && auth.isAdmin)) {
|
|
24
|
+
return super.search(query);
|
|
25
|
+
}
|
|
26
|
+
// owner OR grantee
|
|
27
|
+
const scope = {
|
|
28
|
+
operator: "or",
|
|
29
|
+
conditions: [
|
|
30
|
+
{ attribute: "ownerId", comparator: "equals", value: auth.agentId },
|
|
31
|
+
{ attribute: "granteeId", comparator: "equals", value: auth.agentId },
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
if (query && typeof query === "object" && !Array.isArray(query)) {
|
|
35
|
+
const existing = query.conditions ?? [];
|
|
36
|
+
query.conditions = Array.isArray(existing) ? [scope, ...existing] : [scope, existing];
|
|
37
|
+
return super.search(query);
|
|
38
|
+
}
|
|
39
|
+
const conditions = Array.isArray(query) && query.length > 0 ? [scope, ...query] : [scope];
|
|
40
|
+
return super.search(conditions);
|
|
41
|
+
}
|
|
42
|
+
async post(content, context) {
|
|
43
|
+
const denied = await this._enforceOwnerWrite(content);
|
|
44
|
+
if (denied)
|
|
45
|
+
return denied;
|
|
46
|
+
content.createdAt ||= new Date().toISOString();
|
|
47
|
+
return super.post(content, context);
|
|
48
|
+
}
|
|
49
|
+
async put(content, context) {
|
|
50
|
+
const denied = await this._enforceOwnerWrite(content);
|
|
51
|
+
if (denied)
|
|
52
|
+
return denied;
|
|
53
|
+
return super.put(content, context);
|
|
54
|
+
}
|
|
55
|
+
async delete(id, context) {
|
|
56
|
+
const auth = await this._auth();
|
|
57
|
+
if (auth.kind === "anonymous")
|
|
58
|
+
return UNAUTH();
|
|
59
|
+
if (auth.kind === "internal" || (auth.kind === "agent" && auth.isAdmin)) {
|
|
60
|
+
return super.delete(id, context);
|
|
61
|
+
}
|
|
62
|
+
const record = await this.get(id);
|
|
63
|
+
if (!record)
|
|
64
|
+
return super.delete(id, context);
|
|
65
|
+
if (record.ownerId !== auth.agentId) {
|
|
66
|
+
return FORBIDDEN("forbidden: cannot delete a grant owned by another agent");
|
|
67
|
+
}
|
|
68
|
+
return super.delete(id, context);
|
|
69
|
+
}
|
|
70
|
+
async _enforceOwnerWrite(content) {
|
|
71
|
+
const auth = await this._auth();
|
|
72
|
+
if (auth.kind === "anonymous")
|
|
73
|
+
return UNAUTH();
|
|
74
|
+
if (auth.kind === "agent" && !auth.isAdmin && content?.ownerId && content.ownerId !== auth.agentId) {
|
|
75
|
+
return FORBIDDEN("forbidden: cannot grant access to another agent's memories");
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* to gate auth correctly.
|
|
18
18
|
*/
|
|
19
19
|
import { Resource, databases } from "@harperfast/harper";
|
|
20
|
-
import { isAdmin } from "./auth
|
|
20
|
+
import { isAdmin } from "./agent-auth.js";
|
|
21
21
|
export class MemoryMaintenance extends Resource {
|
|
22
22
|
/** POST requires auth — either an agent acting on its own memories, or admin. */
|
|
23
23
|
allowCreate() {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* count number — number of memories included
|
|
21
21
|
*/
|
|
22
22
|
import { Resource, databases } from "@harperfast/harper";
|
|
23
|
-
import { isAdmin } from "./auth
|
|
23
|
+
import { isAdmin, allowVerified } from "./agent-auth.js";
|
|
24
24
|
import { patchRecordSilent } from "./table-helpers.js";
|
|
25
25
|
const FOCUS_PROMPTS = {
|
|
26
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.",
|
|
@@ -29,6 +29,12 @@ const FOCUS_PROMPTS = {
|
|
|
29
29
|
errors: "Extract errors, bugs, and failures. For each: what failed, root cause, and fix applied. These are high-value persistent memories.",
|
|
30
30
|
};
|
|
31
31
|
export class ReflectMemories extends Resource {
|
|
32
|
+
// Self-authorize via the Ed25519 agent verify (auth reshape removes the gate's
|
|
33
|
+
// admin elevation). Any verified agent may reflect; the isAdmin checks in post()
|
|
34
|
+
// handle finer-grained authorization.
|
|
35
|
+
async allowCreate() {
|
|
36
|
+
return allowVerified(this.getContext?.());
|
|
37
|
+
}
|
|
32
38
|
async post(data) {
|
|
33
39
|
const { agentId: bodyAgentId, scope = "recent", since, maxMemories = 50, focus = "lessons_learned", tag, } = data || {};
|
|
34
40
|
// Authenticated identity comes from getContext().request, not this.request
|
|
@@ -24,8 +24,14 @@
|
|
|
24
24
|
* This endpoint is idempotent: running it again finds zero drift.
|
|
25
25
|
*/
|
|
26
26
|
import { Resource, databases } from "@harperfast/harper";
|
|
27
|
-
import { isAdmin } from "./auth
|
|
27
|
+
import { isAdmin, allowAdmin } from "./agent-auth.js";
|
|
28
28
|
export class MemoryReindex extends Resource {
|
|
29
|
+
// Admin-only: permit verified ADMIN agents (Basic-admin is super_user and
|
|
30
|
+
// bypasses allow*); non-admin agents denied. Mirrors the handler's admin gate
|
|
31
|
+
// but is the real authorization now that the gate no longer elevates agents.
|
|
32
|
+
async allowCreate() {
|
|
33
|
+
return allowAdmin(this.getContext?.());
|
|
34
|
+
}
|
|
29
35
|
async post(data) {
|
|
30
36
|
const ctx = this.getContext?.();
|
|
31
37
|
const request = ctx?.request ?? ctx ?? this.request;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { databases } from "@harperfast/harper";
|
|
2
|
+
import { allowAdmin } from "./agent-auth.js";
|
|
3
|
+
/**
|
|
4
|
+
* OAuthClient holds registered OAuth client records (incl. secrets) — admin/system
|
|
5
|
+
* only, no agent grant. OAuth endpoints (OAuthRegister/Authorize/Token) read+write
|
|
6
|
+
* clients via internal calls (allowAdmin permits internal), so the OAuth flows keep
|
|
7
|
+
* working; direct anonymous table access is denied. Self-authorizes now that the
|
|
8
|
+
* global gate is non-rejecting.
|
|
9
|
+
*/
|
|
10
|
+
export class OAuthClient extends databases.flair.OAuthClient {
|
|
11
|
+
allowRead() { return allowAdmin(this.getContext?.()); }
|
|
12
|
+
allowCreate() { return allowAdmin(this.getContext?.()); }
|
|
13
|
+
allowUpdate() { return allowAdmin(this.getContext?.()); }
|
|
14
|
+
allowDelete() { return allowAdmin(this.getContext?.()); }
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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,42 +1,58 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OrgEvent.ts — Harper table resource for org-wide activity events.
|
|
3
3
|
*
|
|
4
|
-
* Auth
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Auth (self-enforced now that the global gate is non-rejecting):
|
|
5
|
+
* Read — any verified agent/admin (org-scoped); anonymous denied.
|
|
6
|
+
* Write — authorId must match the authenticated agent (or admin); anonymous denied.
|
|
7
|
+
*
|
|
8
|
+
* The previous version read context.request.tpsAgent and treated a MISSING agent
|
|
9
|
+
* as trusted (`if (agentId && …)` / `if (!agentId) super.delete`), so anonymous
|
|
10
|
+
* requests slipped through once the gate stopped rejecting. resolveAgentAuth
|
|
11
|
+
* distinguishes internal/agent/anonymous explicitly.
|
|
7
12
|
*/
|
|
8
13
|
import { databases } from "@harperfast/harper";
|
|
14
|
+
import { resolveAgentAuth, allowVerified } from "./agent-auth.js";
|
|
15
|
+
const FORBIDDEN = (msg) => new Response(JSON.stringify({ error: msg }), { status: 403, headers: { "Content-Type": "application/json" } });
|
|
16
|
+
const UNAUTH = () => new Response(JSON.stringify({ error: "authentication required" }), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
9
17
|
export class OrgEvent extends databases.flair.OrgEvent {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
allowRead() { return allowVerified(this.getContext?.()); }
|
|
19
|
+
_auth() {
|
|
20
|
+
return resolveAgentAuth(this.getContext?.());
|
|
21
|
+
}
|
|
22
|
+
async post(content) {
|
|
23
|
+
const auth = await this._auth();
|
|
24
|
+
if (auth.kind === "anonymous")
|
|
25
|
+
return UNAUTH();
|
|
26
|
+
if (auth.kind === "agent" && !auth.isAdmin && content.authorId !== auth.agentId) {
|
|
27
|
+
return FORBIDDEN("forbidden: authorId must match authenticated agent");
|
|
19
28
|
}
|
|
29
|
+
if (!content.id)
|
|
30
|
+
content.id = `${content.authorId}-${new Date().toISOString()}`;
|
|
20
31
|
content.createdAt = new Date().toISOString();
|
|
21
|
-
// Harper 5: table resources use put() for create/upsert (post() removed)
|
|
32
|
+
// Harper 5: table resources use put() for create/upsert (post() removed).
|
|
22
33
|
return databases.flair.OrgEvent.put(content);
|
|
23
34
|
}
|
|
24
|
-
async put(content
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
27
|
-
return
|
|
35
|
+
async put(content) {
|
|
36
|
+
const auth = await this._auth();
|
|
37
|
+
if (auth.kind === "anonymous")
|
|
38
|
+
return UNAUTH();
|
|
39
|
+
if (auth.kind === "agent" && !auth.isAdmin && content.authorId !== auth.agentId) {
|
|
40
|
+
return FORBIDDEN("forbidden: authorId must match authenticated agent");
|
|
28
41
|
}
|
|
29
42
|
return databases.flair.OrgEvent.put(content);
|
|
30
43
|
}
|
|
31
44
|
async delete(id, context) {
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
45
|
+
const auth = await this._auth();
|
|
46
|
+
if (auth.kind === "anonymous")
|
|
47
|
+
return UNAUTH();
|
|
48
|
+
if (auth.kind === "internal" || (auth.kind === "agent" && auth.isAdmin)) {
|
|
34
49
|
return super.delete(id, context);
|
|
50
|
+
}
|
|
35
51
|
const record = await this.get(id);
|
|
36
52
|
if (!record)
|
|
37
53
|
return super.delete(id, context);
|
|
38
|
-
if (
|
|
39
|
-
return
|
|
54
|
+
if (record.authorId !== auth.agentId) {
|
|
55
|
+
return FORBIDDEN("forbidden: cannot delete events authored by another agent");
|
|
40
56
|
}
|
|
41
57
|
return super.delete(id, context);
|
|
42
58
|
}
|
|
@@ -10,7 +10,15 @@
|
|
|
10
10
|
* Limit 50 events max.
|
|
11
11
|
*/
|
|
12
12
|
import { Resource, databases } from "@harperfast/harper";
|
|
13
|
+
import { allowVerified } from "./agent-auth.js";
|
|
13
14
|
export class OrgEventCatchup extends Resource {
|
|
15
|
+
// Self-authorize via the Ed25519 agent verify (auth reshape removes the gate's
|
|
16
|
+
// admin elevation). Any verified agent may catch up; participant scoping is in
|
|
17
|
+
// get(). Uses getContext().request — the reliable v5 path (this.request is not
|
|
18
|
+
// populated on Harper v5 Resources).
|
|
19
|
+
async allowRead() {
|
|
20
|
+
return allowVerified(this.getContext?.());
|
|
21
|
+
}
|
|
14
22
|
// HarperDB calls get(pathInfo, context) where pathInfo is the URL segment after /OrgEventCatchup/
|
|
15
23
|
async get(pathInfo) {
|
|
16
24
|
const request = this.request;
|
|
@@ -5,7 +5,14 @@
|
|
|
5
5
|
* Auth: admin only.
|
|
6
6
|
*/
|
|
7
7
|
import { Resource, databases } from "@harperfast/harper";
|
|
8
|
+
import { allowAdmin } from "./agent-auth.js";
|
|
8
9
|
export class OrgEventMaintenance extends Resource {
|
|
10
|
+
// Admin-only: permit verified ADMIN agents (Basic-admin is super_user and
|
|
11
|
+
// bypasses allow*); non-admin agents denied. Real authorization now that the
|
|
12
|
+
// gate no longer elevates agents to admin.
|
|
13
|
+
async allowCreate() {
|
|
14
|
+
return allowAdmin(this.getContext?.());
|
|
15
|
+
}
|
|
9
16
|
async post(_data, context) {
|
|
10
17
|
// Admin-only
|
|
11
18
|
if (!context?.request?.tpsAgentIsAdmin) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { databases } from "@harperfast/harper";
|
|
2
|
+
import { allowAdmin } from "./agent-auth.js";
|
|
3
|
+
/**
|
|
4
|
+
* PairingToken holds one-time federation pairing secrets — admin/system only,
|
|
5
|
+
* no agent grant. The FederationPair endpoint consumes tokens via internal calls
|
|
6
|
+
* (allowAdmin permits internal), so dedicated flows keep working; direct anonymous
|
|
7
|
+
* table access is denied. Self-authorizes now that the global gate is non-rejecting.
|
|
8
|
+
*/
|
|
9
|
+
export class PairingToken extends databases.flair.PairingToken {
|
|
10
|
+
allowRead() { return allowAdmin(this.getContext?.()); }
|
|
11
|
+
allowCreate() { return allowAdmin(this.getContext?.()); }
|
|
12
|
+
allowUpdate() { return allowAdmin(this.getContext?.()); }
|
|
13
|
+
allowDelete() { return allowAdmin(this.getContext?.()); }
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { databases } from "@harperfast/harper";
|
|
2
|
+
import { allowAdmin } from "./agent-auth.js";
|
|
3
|
+
/**
|
|
4
|
+
* Peer holds federation peer records (URLs + credentials) — system/admin data,
|
|
5
|
+
* no agent grant. Before the auth flip the global gate was its only protection;
|
|
6
|
+
* the non-rejecting gate means it must self-authorize. Admin/internal only on
|
|
7
|
+
* every path. (Federation pairing/sync use dedicated endpoints — FederationPair,
|
|
8
|
+
* FederationSync — not direct Peer table access.)
|
|
9
|
+
*/
|
|
10
|
+
export class Peer extends databases.flair.Peer {
|
|
11
|
+
allowRead() { return allowAdmin(this.getContext?.()); }
|
|
12
|
+
allowCreate() { return allowAdmin(this.getContext?.()); }
|
|
13
|
+
allowUpdate() { return allowAdmin(this.getContext?.()); }
|
|
14
|
+
allowDelete() { return allowAdmin(this.getContext?.()); }
|
|
15
|
+
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* - currentTask is agent-authored free text → cap length, escape on render.
|
|
18
18
|
*/
|
|
19
19
|
import { databases } from "@harperfast/harper";
|
|
20
|
+
import { resolveAgentAuth } from "./agent-auth.js";
|
|
20
21
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
21
22
|
const WINDOW_MS = 30_000;
|
|
22
23
|
const CURRENT_TASK_MAX_LENGTH = 200;
|
|
@@ -267,4 +268,33 @@ export class Presence extends databases.flair.Presence {
|
|
|
267
268
|
return new Response(JSON.stringify({ error: "presence_write_failed", detail: e?.message }), { status: 500, headers: { "Content-Type": "application/json" } });
|
|
268
269
|
}
|
|
269
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* PUT/DELETE are not part of the agent heartbeat contract (writes go through
|
|
273
|
+
* POST, which carries its own Ed25519 auth). GET is intentionally public and
|
|
274
|
+
* allowCreate=true lets POST self-auth — but those bypasses must NOT extend to
|
|
275
|
+
* PUT/DELETE. The non-rejecting gate would otherwise let anonymous PUT/DELETE
|
|
276
|
+
* straight to Harper's default handler (the leak this closes). Require a verified
|
|
277
|
+
* non-anonymous principal, scoped to the agent's own record (Presence is keyed
|
|
278
|
+
* by agentId).
|
|
279
|
+
*/
|
|
280
|
+
async put(content, context) {
|
|
281
|
+
const auth = await resolveAgentAuth(this.getContext?.());
|
|
282
|
+
if (auth.kind === "anonymous") {
|
|
283
|
+
return new Response(JSON.stringify({ error: "authentication required" }), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
284
|
+
}
|
|
285
|
+
if (auth.kind === "agent" && !auth.isAdmin && content?.agentId && content.agentId !== auth.agentId) {
|
|
286
|
+
return new Response(JSON.stringify({ error: "forbidden: cannot write presence for another agent" }), { status: 403, headers: { "Content-Type": "application/json" } });
|
|
287
|
+
}
|
|
288
|
+
return super.put(content, context);
|
|
289
|
+
}
|
|
290
|
+
async delete(id) {
|
|
291
|
+
const auth = await resolveAgentAuth(this.getContext?.());
|
|
292
|
+
if (auth.kind === "anonymous") {
|
|
293
|
+
return new Response(JSON.stringify({ error: "authentication required" }), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
294
|
+
}
|
|
295
|
+
if (auth.kind === "agent" && !auth.isAdmin && id != null && String(id) !== auth.agentId) {
|
|
296
|
+
return new Response(JSON.stringify({ error: "forbidden: cannot delete presence for another agent" }), { status: 403, headers: { "Content-Type": "application/json" } });
|
|
297
|
+
}
|
|
298
|
+
return super.delete(id);
|
|
299
|
+
}
|
|
270
300
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { databases } from "@harperfast/harper";
|
|
2
|
+
import { resolveAgentAuth } from "./agent-auth.js";
|
|
2
3
|
import { checkRateLimit, rateLimitResponse } from "./rate-limiter.js";
|
|
3
4
|
/**
|
|
4
5
|
* Relationship resource — entity-to-entity relationships with temporal validity.
|
|
@@ -13,15 +14,20 @@ import { checkRateLimit, rateLimitResponse } from "./rate-limiter.js";
|
|
|
13
14
|
*/
|
|
14
15
|
export class Relationship extends databases.flair.Relationship {
|
|
15
16
|
async search(query) {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
const auth = await resolveAgentAuth(this.getContext?.());
|
|
18
|
+
// Anonymous HTTP must NOT read relationships (previously `!authAgent` was
|
|
19
|
+
// treated as unfiltered — the anonymous-read leak).
|
|
20
|
+
if (auth.kind === "anonymous") {
|
|
21
|
+
return new Response(JSON.stringify({ error: "authentication required" }), {
|
|
22
|
+
status: 401, headers: { "content-type": "application/json" },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
// Trusted internal call or admin agent → unfiltered.
|
|
26
|
+
if (auth.kind === "internal" || (auth.kind === "agent" && auth.isAdmin)) {
|
|
21
27
|
return super.search(query);
|
|
22
28
|
}
|
|
23
|
-
// Non-admin: scope to own relationships
|
|
24
|
-
const agentCondition = { attribute: "agentId", comparator: "equals", value:
|
|
29
|
+
// Non-admin agent: scope to own relationships.
|
|
30
|
+
const agentCondition = { attribute: "agentId", comparator: "equals", value: auth.agentId };
|
|
25
31
|
if (!query?.conditions) {
|
|
26
32
|
return super.search({ conditions: [agentCondition], ...(query || {}) });
|
|
27
33
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Resource, databases } from "@harperfast/harper";
|
|
2
|
+
import { resolveAgentAuth, allowVerified } from "./agent-auth.js";
|
|
2
3
|
import { getEmbedding, getMode } from "./embeddings-provider.js";
|
|
3
4
|
import { patchRecord, withDetachedTxn } from "./table-helpers.js";
|
|
4
5
|
import { checkRateLimit, rateLimitResponse } from "./rate-limiter.js";
|
|
@@ -45,6 +46,15 @@ function distanceToSimilarity(distance) {
|
|
|
45
46
|
// so composite re-ranking has enough headroom to reorder results.
|
|
46
47
|
const CANDIDATE_MULTIPLIER = 5;
|
|
47
48
|
export class SemanticSearch extends Resource {
|
|
49
|
+
// Self-authorize via the Ed25519 agent verify instead of relying on the auth
|
|
50
|
+
// gate's admin super_user elevation (removed in the auth reshape). Any
|
|
51
|
+
// cryptographically-verified agent may search; per-agent RESULT scoping is
|
|
52
|
+
// enforced in post() below (an agent only sees its own + office-visible +
|
|
53
|
+
// granted memories). Without this, Harper's default denies the POST for the
|
|
54
|
+
// least-privilege flair_agent role (AccessViolation 403).
|
|
55
|
+
async allowCreate() {
|
|
56
|
+
return allowVerified(this.getContext?.());
|
|
57
|
+
}
|
|
48
58
|
async post(data) {
|
|
49
59
|
const { agentId: bodyAgentId, q, queryEmbedding, tag, subject, subjects, limit = 10, includeSuperseded = false, scoring = "composite", minScore = 0, since, asOf } = data || {};
|
|
50
60
|
// Authenticated identity lives on the Harper Resource context (getContext().request).
|
|
@@ -52,11 +62,18 @@ export class SemanticSearch extends Resource {
|
|
|
52
62
|
// silently returned undefined and the defense-in-depth scope check below
|
|
53
63
|
// was bypassed, letting a non-admin agent read another agent's memories
|
|
54
64
|
// by putting the victim's id in the body.
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
const auth = await resolveAgentAuth(this.getContext?.());
|
|
66
|
+
// Anonymous HTTP must NOT search. Previously the no-auth path fell through to
|
|
67
|
+
// honoring the body-supplied agentId (line below), so an unauthenticated
|
|
68
|
+
// caller could read any agent's memories by putting that id in the body.
|
|
69
|
+
if (auth.kind === "anonymous") {
|
|
70
|
+
return new Response(JSON.stringify({ error: "authentication required" }), {
|
|
71
|
+
status: 401, headers: { "Content-Type": "application/json" },
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
const authenticatedAgent = auth.kind === "agent" ? auth.agentId : undefined;
|
|
75
|
+
const callerIsAdmin = auth.kind === "agent" && auth.isAdmin;
|
|
76
|
+
// Rate limiting — use authenticated agent ID (internal calls have none).
|
|
60
77
|
if (authenticatedAgent) {
|
|
61
78
|
const bucket = q && !queryEmbedding ? "embedding" : "general";
|
|
62
79
|
const rl = checkRateLimit(authenticatedAgent, bucket);
|
|
@@ -70,14 +87,14 @@ export class SemanticSearch extends Resource {
|
|
|
70
87
|
: null;
|
|
71
88
|
// Enforce agentId = authenticated agent for non-admins. A mismatched body
|
|
72
89
|
// agentId is a cross-agent read attempt — reject outright. Admins can query
|
|
73
|
-
// any agentId (
|
|
90
|
+
// any agentId (bootstrap / consolidation).
|
|
74
91
|
if (authenticatedAgent && !callerIsAdmin && bodyAgentId && bodyAgentId !== authenticatedAgent) {
|
|
75
92
|
return new Response(JSON.stringify({
|
|
76
93
|
error: "forbidden: agentId must match authenticated agent",
|
|
77
94
|
}), { status: 403, headers: { "Content-Type": "application/json" } });
|
|
78
95
|
}
|
|
79
|
-
// Scope
|
|
80
|
-
//
|
|
96
|
+
// Scope: non-admin agent → own (+ granted). Admin agent or trusted internal
|
|
97
|
+
// call (no request) → honor the body-supplied agentId.
|
|
81
98
|
const agentId = (authenticatedAgent && !callerIsAdmin)
|
|
82
99
|
? authenticatedAgent
|
|
83
100
|
: bodyAgentId;
|
package/dist/resources/Soul.js
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import { databases } from "@harperfast/harper";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
import { resolveAgentAuth } from "./agent-auth.js";
|
|
3
|
+
const FORBIDDEN = (msg) => new Response(JSON.stringify({ error: msg }), { status: 403, headers: { "Content-Type": "application/json" } });
|
|
4
|
+
const UNAUTH = () => new Response(JSON.stringify({ error: "authentication required" }), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
5
|
+
/**
|
|
6
|
+
* Deny anonymous; enforce per-agent write ownership for non-admin agents.
|
|
7
|
+
* The previous header-based check only fired when an agent WAS present (it read
|
|
8
|
+
* x-tps-agent), so an anonymous request — which carries no x-tps-agent — slipped
|
|
9
|
+
* through. With the non-rejecting gate, each write path self-enforces (resolveAgentAuth
|
|
10
|
+
* distinguishes internal/agent/anonymous). Mirrors the WorkspaceState pattern.
|
|
11
|
+
*/
|
|
12
|
+
async function enforceWriteAuth(self, data) {
|
|
13
|
+
const auth = await resolveAgentAuth(self.getContext?.());
|
|
14
|
+
if (auth.kind === "anonymous")
|
|
15
|
+
return UNAUTH();
|
|
16
|
+
if (auth.kind === "agent" && !auth.isAdmin && data?.agentId && data.agentId !== auth.agentId) {
|
|
17
|
+
return FORBIDDEN("forbidden: agentId must match authenticated agent");
|
|
9
18
|
}
|
|
10
19
|
return null;
|
|
11
20
|
}
|
|
12
21
|
export class Soul extends databases.flair.Soul {
|
|
13
22
|
async post(content, context) {
|
|
14
|
-
const denied =
|
|
23
|
+
const denied = await enforceWriteAuth(this, content);
|
|
15
24
|
if (denied)
|
|
16
25
|
return denied;
|
|
17
26
|
content.durability ||= "permanent";
|
|
@@ -20,7 +29,7 @@ export class Soul extends databases.flair.Soul {
|
|
|
20
29
|
return super.post(content, context);
|
|
21
30
|
}
|
|
22
31
|
async put(content, context) {
|
|
23
|
-
const denied =
|
|
32
|
+
const denied = await enforceWriteAuth(this, content);
|
|
24
33
|
if (denied)
|
|
25
34
|
return denied;
|
|
26
35
|
content.updatedAt = new Date().toISOString();
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { Resource, databases } from '@harperfast/harper';
|
|
2
|
+
import { allowVerified } from './agent-auth.js';
|
|
2
3
|
export class FeedSouls extends Resource {
|
|
4
|
+
// Self-authorize the subscription via the Ed25519 agent verify (auth reshape
|
|
5
|
+
// removes the gate's admin elevation). FeedSouls extends Resource (not a Table),
|
|
6
|
+
// so getContext().request is reachable in allow* — same pattern as SemanticSearch.
|
|
7
|
+
async allowRead() {
|
|
8
|
+
return allowVerified(this.getContext?.());
|
|
9
|
+
}
|
|
3
10
|
async *connect(target, incomingMessages) {
|
|
4
11
|
const subscription = await databases.flair.Soul.subscribe(target);
|
|
5
12
|
if (!incomingMessages) {
|
|
@@ -5,7 +5,14 @@
|
|
|
5
5
|
* Auth: requesting agent must match agentId in path (or be admin).
|
|
6
6
|
*/
|
|
7
7
|
import { Resource, databases } from "@harperfast/harper";
|
|
8
|
+
import { allowVerified } from "./agent-auth.js";
|
|
8
9
|
export class WorkspaceLatest extends Resource {
|
|
10
|
+
// Self-authorize via the Ed25519 agent verify (auth reshape removes the gate's
|
|
11
|
+
// admin elevation). Any verified agent may read; the path-vs-agent ownership
|
|
12
|
+
// check stays in get().
|
|
13
|
+
async allowRead() {
|
|
14
|
+
return allowVerified(this.getContext?.());
|
|
15
|
+
}
|
|
9
16
|
async get(pathInfo) {
|
|
10
17
|
const request = this.context?.request ?? this.request;
|
|
11
18
|
const callerAgent = request?.tpsAgent;
|