ofiere-openclaw-plugin 4.54.1 → 4.55.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/src/agent-tier.ts CHANGED
@@ -1,192 +1,192 @@
1
- // src/agent-tier.ts — Plugin-side agent tier resolver.
2
- // Manual override (agent_tier_overrides) wins over auto-detection.
3
- // Mirrors dashboard/lib/agentTier.ts contracts so the dashboard UI
4
- // and plugin enforcement agree on every agent's tier.
5
-
6
- import type { SupabaseClient } from "@supabase/supabase-js";
7
-
8
- export type AgentTier = "c-suite" | "staff" | null;
9
- export type TierSource =
10
- | "manual"
11
- | "executiveRole"
12
- | "roster"
13
- | "subagent"
14
- | "departmentRole"
15
- | "none";
16
-
17
- export interface TierResolution {
18
- tier: AgentTier;
19
- source: TierSource;
20
- }
21
-
22
- // Hard-coded executive roster ids — match dashboard/lib/agentRoster.ts.
23
- // Anything in this set defaults to c-suite when no manual override exists.
24
- const ROSTER_IDS = new Set<string>([
25
- "ofie",
26
- "daisy",
27
- "ivy",
28
- "celia",
29
- "thalia",
30
- "sasha",
31
- "agent-zero",
32
- ]);
33
-
34
- interface CacheEntry { value: TierResolution; expires: number; }
35
- const TTL_MS = 5 * 60 * 1000;
36
- const cache = new Map<string, CacheEntry>();
37
- const k = (userId: string, agentId: string) => `${userId}::${agentId}`;
38
-
39
- export function invalidateAgentTier(userId: string, agentId?: string): void {
40
- if (!agentId) {
41
- for (const key of cache.keys()) if (key.startsWith(`${userId}::`)) cache.delete(key);
42
- return;
43
- }
44
- cache.delete(k(userId, agentId));
45
- }
46
-
47
- export async function resolveAgentTier(
48
- supabase: SupabaseClient,
49
- agentId: string,
50
- userId: string,
51
- ): Promise<TierResolution> {
52
- const id = (agentId || "").trim();
53
- if (!id || !userId) return { tier: null, source: "none" };
54
-
55
- const key = k(userId, id);
56
-
57
- // 1. Manual override — ALWAYS fresh (uncached). Direct-SQL writes to
58
- // agent_tier_overrides must win immediately, even when a non-override
59
- // branch is already cached from a prior call.
60
- let overrideRow: { tier?: string | null } | null = null;
61
- let overrideQueryFailed = false;
62
- try {
63
- const { data } = await supabase
64
- .from("agent_tier_overrides")
65
- .select("tier")
66
- .eq("user_id", userId)
67
- .eq("agent_id", id)
68
- .maybeSingle();
69
- overrideRow = data ?? null;
70
- } catch {
71
- // Table missing in older installs — skip override path
72
- overrideQueryFailed = true;
73
- }
74
-
75
- if (overrideRow?.tier === "c-suite" || overrideRow?.tier === "staff") {
76
- const result: TierResolution = { tier: overrideRow.tier, source: "manual" };
77
- cache.set(key, { value: result, expires: Date.now() + TTL_MS });
78
- return result;
79
- }
80
-
81
- // Override absent: if cache holds a stale `manual` entry from a deleted
82
- // override row, drop it so the auto branches re-resolve.
83
- if (!overrideQueryFailed) {
84
- const cachedManual = cache.get(key);
85
- if (cachedManual && cachedManual.value.source === "manual") {
86
- cache.delete(key);
87
- }
88
- }
89
-
90
- // 2. Cache lookup for non-override branches.
91
- const hit = cache.get(key);
92
- if (hit && hit.expires > Date.now()) return hit.value;
93
-
94
- let result: TierResolution = { tier: null, source: "none" };
95
-
96
- // 3. C-Suite: hardcoded roster
97
- if (ROSTER_IDS.has(id)) {
98
- result = { tier: "c-suite", source: "roster" };
99
- cache.set(key, { value: result, expires: Date.now() + TTL_MS });
100
- return result;
101
- }
102
-
103
- // 4. agent_architectures.executive_role / department_role
104
- try {
105
- const { data } = await supabase
106
- .from("agent_architectures")
107
- .select("executive_role, department_role")
108
- .eq("user_id", userId)
109
- .eq("agent_id", id)
110
- .maybeSingle();
111
- if (data?.department_role === "chief" ||
112
- (data?.executive_role && String(data.executive_role).trim())) {
113
- result = { tier: "c-suite", source: "executiveRole" };
114
- cache.set(key, { value: result, expires: Date.now() + TTL_MS });
115
- return result;
116
- }
117
- if (data?.department_role === "staff") {
118
- result = { tier: "staff", source: "departmentRole" };
119
- cache.set(key, { value: result, expires: Date.now() + TTL_MS });
120
- return result;
121
- }
122
- } catch {
123
- // Table missing — fall through
124
- }
125
-
126
- // 5. Staff: registered as subagent
127
- try {
128
- const { data } = await supabase
129
- .from("agent_subagents")
130
- .select("id")
131
- .eq("user_id", userId)
132
- .eq("id", id)
133
- .maybeSingle();
134
- if (data?.id) {
135
- result = { tier: "staff", source: "subagent" };
136
- cache.set(key, { value: result, expires: Date.now() + TTL_MS });
137
- return result;
138
- }
139
- } catch {
140
- // ignore
141
- }
142
-
143
- cache.set(key, { value: result, expires: Date.now() + TTL_MS });
144
- return result;
145
- }
146
-
147
- export async function getAgentTier(
148
- supabase: SupabaseClient,
149
- agentId: string,
150
- userId: string,
151
- ): Promise<AgentTier> {
152
- return (await resolveAgentTier(supabase, agentId, userId)).tier;
153
- }
154
-
155
- // Target-aware resolution: when a work target carries a subagent_id, the
156
- // work is staff-tier regardless of which chief routes it. Uncached lookup —
157
- // subagent rows can be deleted out from under us.
158
- export interface TargetTierInput {
159
- agentId: string | null;
160
- subagentId?: string | null;
161
- }
162
-
163
- export async function resolveTargetTier(
164
- supabase: SupabaseClient,
165
- target: TargetTierInput,
166
- userId: string,
167
- ): Promise<TierResolution> {
168
- if (target.subagentId) {
169
- try {
170
- const { data } = await supabase
171
- .from("agent_subagents")
172
- .select("id")
173
- .eq("user_id", userId)
174
- .eq("id", target.subagentId)
175
- .maybeSingle();
176
- if (data?.id) return { tier: "staff", source: "subagent" };
177
- } catch {
178
- // Fall through to chief lookup
179
- }
180
- }
181
- if (target.agentId) return resolveAgentTier(supabase, target.agentId, userId);
182
- return { tier: null, source: "none" };
183
- }
184
-
185
- export function isDocKindValidForTier(
186
- docKind: "sop" | "framework",
187
- tier: AgentTier,
188
- ): boolean {
189
- if (tier === "c-suite") return docKind === "framework";
190
- if (tier === "staff") return docKind === "sop";
191
- return false;
192
- }
1
+ // src/agent-tier.ts — Plugin-side agent tier resolver.
2
+ // Manual override (agent_tier_overrides) wins over auto-detection.
3
+ // Mirrors dashboard/lib/agentTier.ts contracts so the dashboard UI
4
+ // and plugin enforcement agree on every agent's tier.
5
+
6
+ import type { SupabaseClient } from "@supabase/supabase-js";
7
+
8
+ export type AgentTier = "c-suite" | "staff" | null;
9
+ export type TierSource =
10
+ | "manual"
11
+ | "executiveRole"
12
+ | "roster"
13
+ | "subagent"
14
+ | "departmentRole"
15
+ | "none";
16
+
17
+ export interface TierResolution {
18
+ tier: AgentTier;
19
+ source: TierSource;
20
+ }
21
+
22
+ // Hard-coded executive roster ids — match dashboard/lib/agentRoster.ts.
23
+ // Anything in this set defaults to c-suite when no manual override exists.
24
+ const ROSTER_IDS = new Set<string>([
25
+ "ofie",
26
+ "daisy",
27
+ "ivy",
28
+ "celia",
29
+ "thalia",
30
+ "sasha",
31
+ "agent-zero",
32
+ ]);
33
+
34
+ interface CacheEntry { value: TierResolution; expires: number; }
35
+ const TTL_MS = 5 * 60 * 1000;
36
+ const cache = new Map<string, CacheEntry>();
37
+ const k = (userId: string, agentId: string) => `${userId}::${agentId}`;
38
+
39
+ export function invalidateAgentTier(userId: string, agentId?: string): void {
40
+ if (!agentId) {
41
+ for (const key of cache.keys()) if (key.startsWith(`${userId}::`)) cache.delete(key);
42
+ return;
43
+ }
44
+ cache.delete(k(userId, agentId));
45
+ }
46
+
47
+ export async function resolveAgentTier(
48
+ supabase: SupabaseClient,
49
+ agentId: string,
50
+ userId: string,
51
+ ): Promise<TierResolution> {
52
+ const id = (agentId || "").trim();
53
+ if (!id || !userId) return { tier: null, source: "none" };
54
+
55
+ const key = k(userId, id);
56
+
57
+ // 1. Manual override — ALWAYS fresh (uncached). Direct-SQL writes to
58
+ // agent_tier_overrides must win immediately, even when a non-override
59
+ // branch is already cached from a prior call.
60
+ let overrideRow: { tier?: string | null } | null = null;
61
+ let overrideQueryFailed = false;
62
+ try {
63
+ const { data } = await supabase
64
+ .from("agent_tier_overrides")
65
+ .select("tier")
66
+ .eq("user_id", userId)
67
+ .eq("agent_id", id)
68
+ .maybeSingle();
69
+ overrideRow = data ?? null;
70
+ } catch {
71
+ // Table missing in older installs — skip override path
72
+ overrideQueryFailed = true;
73
+ }
74
+
75
+ if (overrideRow?.tier === "c-suite" || overrideRow?.tier === "staff") {
76
+ const result: TierResolution = { tier: overrideRow.tier, source: "manual" };
77
+ cache.set(key, { value: result, expires: Date.now() + TTL_MS });
78
+ return result;
79
+ }
80
+
81
+ // Override absent: if cache holds a stale `manual` entry from a deleted
82
+ // override row, drop it so the auto branches re-resolve.
83
+ if (!overrideQueryFailed) {
84
+ const cachedManual = cache.get(key);
85
+ if (cachedManual && cachedManual.value.source === "manual") {
86
+ cache.delete(key);
87
+ }
88
+ }
89
+
90
+ // 2. Cache lookup for non-override branches.
91
+ const hit = cache.get(key);
92
+ if (hit && hit.expires > Date.now()) return hit.value;
93
+
94
+ let result: TierResolution = { tier: null, source: "none" };
95
+
96
+ // 3. C-Suite: hardcoded roster
97
+ if (ROSTER_IDS.has(id)) {
98
+ result = { tier: "c-suite", source: "roster" };
99
+ cache.set(key, { value: result, expires: Date.now() + TTL_MS });
100
+ return result;
101
+ }
102
+
103
+ // 4. agent_architectures.executive_role / department_role
104
+ try {
105
+ const { data } = await supabase
106
+ .from("agent_architectures")
107
+ .select("executive_role, department_role")
108
+ .eq("user_id", userId)
109
+ .eq("agent_id", id)
110
+ .maybeSingle();
111
+ if (data?.department_role === "chief" ||
112
+ (data?.executive_role && String(data.executive_role).trim())) {
113
+ result = { tier: "c-suite", source: "executiveRole" };
114
+ cache.set(key, { value: result, expires: Date.now() + TTL_MS });
115
+ return result;
116
+ }
117
+ if (data?.department_role === "staff") {
118
+ result = { tier: "staff", source: "departmentRole" };
119
+ cache.set(key, { value: result, expires: Date.now() + TTL_MS });
120
+ return result;
121
+ }
122
+ } catch {
123
+ // Table missing — fall through
124
+ }
125
+
126
+ // 5. Staff: registered as subagent
127
+ try {
128
+ const { data } = await supabase
129
+ .from("agent_subagents")
130
+ .select("id")
131
+ .eq("user_id", userId)
132
+ .eq("id", id)
133
+ .maybeSingle();
134
+ if (data?.id) {
135
+ result = { tier: "staff", source: "subagent" };
136
+ cache.set(key, { value: result, expires: Date.now() + TTL_MS });
137
+ return result;
138
+ }
139
+ } catch {
140
+ // ignore
141
+ }
142
+
143
+ cache.set(key, { value: result, expires: Date.now() + TTL_MS });
144
+ return result;
145
+ }
146
+
147
+ export async function getAgentTier(
148
+ supabase: SupabaseClient,
149
+ agentId: string,
150
+ userId: string,
151
+ ): Promise<AgentTier> {
152
+ return (await resolveAgentTier(supabase, agentId, userId)).tier;
153
+ }
154
+
155
+ // Target-aware resolution: when a work target carries a subagent_id, the
156
+ // work is staff-tier regardless of which chief routes it. Uncached lookup —
157
+ // subagent rows can be deleted out from under us.
158
+ export interface TargetTierInput {
159
+ agentId: string | null;
160
+ subagentId?: string | null;
161
+ }
162
+
163
+ export async function resolveTargetTier(
164
+ supabase: SupabaseClient,
165
+ target: TargetTierInput,
166
+ userId: string,
167
+ ): Promise<TierResolution> {
168
+ if (target.subagentId) {
169
+ try {
170
+ const { data } = await supabase
171
+ .from("agent_subagents")
172
+ .select("id")
173
+ .eq("user_id", userId)
174
+ .eq("id", target.subagentId)
175
+ .maybeSingle();
176
+ if (data?.id) return { tier: "staff", source: "subagent" };
177
+ } catch {
178
+ // Fall through to chief lookup
179
+ }
180
+ }
181
+ if (target.agentId) return resolveAgentTier(supabase, target.agentId, userId);
182
+ return { tier: null, source: "none" };
183
+ }
184
+
185
+ export function isDocKindValidForTier(
186
+ docKind: "sop" | "framework",
187
+ tier: AgentTier,
188
+ ): boolean {
189
+ if (tier === "c-suite") return docKind === "framework";
190
+ if (tier === "staff") return docKind === "sop";
191
+ return false;
192
+ }
@@ -1,106 +1,106 @@
1
- // src/attach-token.ts — HMAC confirmation token for agent self-attach.
2
- // Mirrors dashboard/lib/attachmentToken.ts EXACTLY so tokens issued
3
- // here verify against the dashboard and vice-versa.
4
- //
5
- // Format: base64url(payload).base64url(sig)
6
- // payload = JSON({ u, k, t, d, dk, exp })
7
- // sig = HMAC_SHA256(secret, payload)
8
-
9
- import { createHmac, timingSafeEqual } from "crypto";
10
-
11
- const TOKEN_TTL_MS = 5 * 60 * 1000;
12
-
13
- function getSecret(): string {
14
- const s = process.env.OFIERE_ATTACH_SECRET;
15
- if (!s) throw new Error("OFIERE_ATTACH_SECRET is not configured");
16
- return s;
17
- }
18
-
19
- function b64urlEncode(buf: Buffer | string): string {
20
- const b = typeof buf === "string" ? Buffer.from(buf, "utf8") : buf;
21
- return b.toString("base64").replace(/=+$/g, "").replace(/\+/g, "-").replace(/\//g, "_");
22
- }
23
-
24
- function b64urlDecode(s: string): Buffer {
25
- const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - (s.length % 4));
26
- return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/") + pad, "base64");
27
- }
28
-
29
- interface TokenPayload {
30
- u: string;
31
- k: string;
32
- t: string;
33
- d: string[];
34
- dk: "sop" | "framework";
35
- exp: number;
36
- }
37
-
38
- export interface IssueArgs {
39
- userId: string;
40
- targetKind: string;
41
- targetId: string;
42
- docKind: "sop" | "framework";
43
- docIds: string[];
44
- ttlMs?: number;
45
- }
46
-
47
- export function issueAttachmentToken(args: IssueArgs): string {
48
- const exp = Date.now() + (args.ttlMs ?? TOKEN_TTL_MS);
49
- const payload: TokenPayload = {
50
- u: args.userId,
51
- k: args.targetKind,
52
- t: args.targetId,
53
- d: [...args.docIds].sort(),
54
- dk: args.docKind,
55
- exp,
56
- };
57
- const json = JSON.stringify(payload);
58
- const sig = createHmac("sha256", getSecret()).update(json).digest();
59
- return `${b64urlEncode(json)}.${b64urlEncode(sig)}`;
60
- }
61
-
62
- export interface VerifyArgs {
63
- token: string;
64
- userId: string;
65
- targetKind: string;
66
- targetId: string;
67
- docKind: "sop" | "framework";
68
- docIds: string[];
69
- }
70
-
71
- export type VerifyResult =
72
- | { ok: true }
73
- | { ok: false; reason: "malformed" | "bad_signature" | "expired" | "mismatch" };
74
-
75
- export function verifyAttachmentToken(args: VerifyArgs): VerifyResult {
76
- const parts = args.token.split(".");
77
- if (parts.length !== 2) return { ok: false, reason: "malformed" };
78
- let payload: TokenPayload;
79
- try {
80
- payload = JSON.parse(b64urlDecode(parts[0]).toString("utf8"));
81
- } catch {
82
- return { ok: false, reason: "malformed" };
83
- }
84
-
85
- const expectedJson = JSON.stringify(payload);
86
- const expectedSig = createHmac("sha256", getSecret()).update(expectedJson).digest();
87
- const providedSig = b64urlDecode(parts[1]);
88
- if (expectedSig.length !== providedSig.length) return { ok: false, reason: "bad_signature" };
89
- if (!timingSafeEqual(expectedSig, providedSig)) return { ok: false, reason: "bad_signature" };
90
-
91
- if (typeof payload.exp !== "number" || payload.exp < Date.now()) {
92
- return { ok: false, reason: "expired" };
93
- }
94
- if (payload.u !== args.userId) return { ok: false, reason: "mismatch" };
95
- if (payload.k !== args.targetKind || payload.t !== args.targetId) return { ok: false, reason: "mismatch" };
96
- if (payload.dk !== args.docKind) return { ok: false, reason: "mismatch" };
97
-
98
- const sortedRequested = [...args.docIds].sort();
99
- if (
100
- payload.d.length !== sortedRequested.length ||
101
- payload.d.some((v, i) => v !== sortedRequested[i])
102
- ) {
103
- return { ok: false, reason: "mismatch" };
104
- }
105
- return { ok: true };
106
- }
1
+ // src/attach-token.ts — HMAC confirmation token for agent self-attach.
2
+ // Mirrors dashboard/lib/attachmentToken.ts EXACTLY so tokens issued
3
+ // here verify against the dashboard and vice-versa.
4
+ //
5
+ // Format: base64url(payload).base64url(sig)
6
+ // payload = JSON({ u, k, t, d, dk, exp })
7
+ // sig = HMAC_SHA256(secret, payload)
8
+
9
+ import { createHmac, timingSafeEqual } from "crypto";
10
+
11
+ const TOKEN_TTL_MS = 5 * 60 * 1000;
12
+
13
+ function getSecret(): string {
14
+ const s = process.env.OFIERE_ATTACH_SECRET;
15
+ if (!s) throw new Error("OFIERE_ATTACH_SECRET is not configured");
16
+ return s;
17
+ }
18
+
19
+ function b64urlEncode(buf: Buffer | string): string {
20
+ const b = typeof buf === "string" ? Buffer.from(buf, "utf8") : buf;
21
+ return b.toString("base64").replace(/=+$/g, "").replace(/\+/g, "-").replace(/\//g, "_");
22
+ }
23
+
24
+ function b64urlDecode(s: string): Buffer {
25
+ const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - (s.length % 4));
26
+ return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/") + pad, "base64");
27
+ }
28
+
29
+ interface TokenPayload {
30
+ u: string;
31
+ k: string;
32
+ t: string;
33
+ d: string[];
34
+ dk: "sop" | "framework";
35
+ exp: number;
36
+ }
37
+
38
+ export interface IssueArgs {
39
+ userId: string;
40
+ targetKind: string;
41
+ targetId: string;
42
+ docKind: "sop" | "framework";
43
+ docIds: string[];
44
+ ttlMs?: number;
45
+ }
46
+
47
+ export function issueAttachmentToken(args: IssueArgs): string {
48
+ const exp = Date.now() + (args.ttlMs ?? TOKEN_TTL_MS);
49
+ const payload: TokenPayload = {
50
+ u: args.userId,
51
+ k: args.targetKind,
52
+ t: args.targetId,
53
+ d: [...args.docIds].sort(),
54
+ dk: args.docKind,
55
+ exp,
56
+ };
57
+ const json = JSON.stringify(payload);
58
+ const sig = createHmac("sha256", getSecret()).update(json).digest();
59
+ return `${b64urlEncode(json)}.${b64urlEncode(sig)}`;
60
+ }
61
+
62
+ export interface VerifyArgs {
63
+ token: string;
64
+ userId: string;
65
+ targetKind: string;
66
+ targetId: string;
67
+ docKind: "sop" | "framework";
68
+ docIds: string[];
69
+ }
70
+
71
+ export type VerifyResult =
72
+ | { ok: true }
73
+ | { ok: false; reason: "malformed" | "bad_signature" | "expired" | "mismatch" };
74
+
75
+ export function verifyAttachmentToken(args: VerifyArgs): VerifyResult {
76
+ const parts = args.token.split(".");
77
+ if (parts.length !== 2) return { ok: false, reason: "malformed" };
78
+ let payload: TokenPayload;
79
+ try {
80
+ payload = JSON.parse(b64urlDecode(parts[0]).toString("utf8"));
81
+ } catch {
82
+ return { ok: false, reason: "malformed" };
83
+ }
84
+
85
+ const expectedJson = JSON.stringify(payload);
86
+ const expectedSig = createHmac("sha256", getSecret()).update(expectedJson).digest();
87
+ const providedSig = b64urlDecode(parts[1]);
88
+ if (expectedSig.length !== providedSig.length) return { ok: false, reason: "bad_signature" };
89
+ if (!timingSafeEqual(expectedSig, providedSig)) return { ok: false, reason: "bad_signature" };
90
+
91
+ if (typeof payload.exp !== "number" || payload.exp < Date.now()) {
92
+ return { ok: false, reason: "expired" };
93
+ }
94
+ if (payload.u !== args.userId) return { ok: false, reason: "mismatch" };
95
+ if (payload.k !== args.targetKind || payload.t !== args.targetId) return { ok: false, reason: "mismatch" };
96
+ if (payload.dk !== args.docKind) return { ok: false, reason: "mismatch" };
97
+
98
+ const sortedRequested = [...args.docIds].sort();
99
+ if (
100
+ payload.d.length !== sortedRequested.length ||
101
+ payload.d.some((v, i) => v !== sortedRequested[i])
102
+ ) {
103
+ return { ok: false, reason: "mismatch" };
104
+ }
105
+ return { ok: true };
106
+ }