forge-openclaw-plugin 0.2.4 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +113 -5
  2. package/dist/assets/board-CzgvdLO8.js +6 -0
  3. package/dist/assets/board-CzgvdLO8.js.map +1 -0
  4. package/dist/assets/favicon-BCHm9dUV.ico +0 -0
  5. package/dist/assets/index-8d_oM8fL.js +27 -0
  6. package/dist/assets/index-8d_oM8fL.js.map +1 -0
  7. package/dist/assets/index-D4A_bq8m.css +1 -0
  8. package/dist/assets/motion-STUd1O46.js +10 -0
  9. package/dist/assets/motion-STUd1O46.js.map +1 -0
  10. package/dist/assets/plus-jakarta-sans-latin-ext-wght-normal-DmpS2jIq.woff2 +0 -0
  11. package/dist/assets/plus-jakarta-sans-latin-wght-normal-eXO_dkmS.woff2 +0 -0
  12. package/dist/assets/plus-jakarta-sans-vietnamese-wght-normal-qRpaaN48.woff2 +0 -0
  13. package/dist/assets/sora-latin-ext-wght-normal-CawQDOvP.woff2 +0 -0
  14. package/dist/assets/sora-latin-wght-normal-DdqRvwsR.woff2 +0 -0
  15. package/dist/assets/space-grotesk-latin-500-normal-CNSSEhBt.woff +0 -0
  16. package/dist/assets/space-grotesk-latin-500-normal-lFbtlQH6.woff2 +0 -0
  17. package/dist/assets/space-grotesk-latin-700-normal-CwsQ-cCU.woff +0 -0
  18. package/dist/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
  19. package/dist/assets/space-grotesk-latin-ext-500-normal-3dgZTiw9.woff +0 -0
  20. package/dist/assets/space-grotesk-latin-ext-500-normal-DUe3BAxM.woff2 +0 -0
  21. package/dist/assets/space-grotesk-latin-ext-700-normal-BQnZhY3m.woff2 +0 -0
  22. package/dist/assets/space-grotesk-latin-ext-700-normal-HVCqSBdx.woff +0 -0
  23. package/dist/assets/space-grotesk-vietnamese-500-normal-BTqKIpxg.woff +0 -0
  24. package/dist/assets/space-grotesk-vietnamese-500-normal-BmEvtly_.woff2 +0 -0
  25. package/dist/assets/space-grotesk-vietnamese-700-normal-DMty7AZE.woff2 +0 -0
  26. package/dist/assets/space-grotesk-vietnamese-700-normal-Duxec5Rn.woff +0 -0
  27. package/dist/assets/table-CtNlETLc.js +23 -0
  28. package/dist/assets/table-CtNlETLc.js.map +1 -0
  29. package/dist/assets/ui-ThzkR_oW.js +46 -0
  30. package/dist/assets/ui-ThzkR_oW.js.map +1 -0
  31. package/dist/assets/vendor-CRS-psbw.css +1 -0
  32. package/dist/assets/vendor-DyHAI6nk.js +423 -0
  33. package/dist/assets/vendor-DyHAI6nk.js.map +1 -0
  34. package/dist/assets/viz-BJuBCz_G.js +34 -0
  35. package/dist/assets/viz-BJuBCz_G.js.map +1 -0
  36. package/dist/favicon.ico +0 -0
  37. package/dist/favicon.png +0 -0
  38. package/dist/index.html +29 -0
  39. package/dist/openclaw/api-client.d.ts +8 -0
  40. package/dist/openclaw/api-client.js +31 -4
  41. package/dist/openclaw/local-runtime.d.ts +3 -0
  42. package/dist/openclaw/local-runtime.js +135 -0
  43. package/dist/openclaw/parity.d.ts +4 -4
  44. package/dist/openclaw/parity.js +23 -33
  45. package/dist/openclaw/plugin-entry-shared.d.ts +4 -2
  46. package/dist/openclaw/plugin-entry-shared.js +51 -9
  47. package/dist/openclaw/routes.d.ts +12 -3
  48. package/dist/openclaw/routes.js +156 -924
  49. package/dist/openclaw/tools.js +242 -1100
  50. package/dist/server/app.js +2450 -0
  51. package/dist/server/db.js +313 -0
  52. package/dist/server/e2e-server.js +20 -0
  53. package/dist/server/errors.js +15 -0
  54. package/dist/server/index.js +16 -0
  55. package/dist/server/managers/base.js +17 -0
  56. package/dist/server/managers/contracts.js +47 -0
  57. package/dist/server/managers/platform/api-gateway-manager.js +11 -0
  58. package/dist/server/managers/platform/audit-manager.js +15 -0
  59. package/dist/server/managers/platform/authentication-manager.js +56 -0
  60. package/dist/server/managers/platform/authorization-manager.js +56 -0
  61. package/dist/server/managers/platform/background-job-manager.js +10 -0
  62. package/dist/server/managers/platform/configuration-manager.js +33 -0
  63. package/dist/server/managers/platform/database-manager.js +14 -0
  64. package/dist/server/managers/platform/event-bus-manager.js +7 -0
  65. package/dist/server/managers/platform/external-service-manager.js +11 -0
  66. package/dist/server/managers/platform/health-manager.js +7 -0
  67. package/dist/server/managers/platform/migration-manager.js +8 -0
  68. package/dist/server/managers/platform/search-index-manager.js +4 -0
  69. package/dist/server/managers/platform/secrets-manager.js +19 -0
  70. package/dist/server/managers/platform/session-manager.js +121 -0
  71. package/dist/server/managers/platform/storage-manager.js +16 -0
  72. package/dist/server/managers/platform/token-manager.js +37 -0
  73. package/dist/server/managers/platform/transaction-manager.js +8 -0
  74. package/dist/server/managers/platform/trusted-network.js +39 -0
  75. package/dist/server/managers/runtime.js +56 -0
  76. package/dist/server/managers/type-guards.js +4 -0
  77. package/dist/server/openapi.js +3512 -0
  78. package/dist/server/psyche-types.js +395 -0
  79. package/dist/server/repositories/activity-events.js +157 -0
  80. package/dist/server/repositories/collaboration.js +497 -0
  81. package/dist/server/repositories/comments.js +176 -0
  82. package/dist/server/repositories/deleted-entities.js +192 -0
  83. package/dist/server/repositories/domains.js +30 -0
  84. package/dist/server/repositories/event-log.js +64 -0
  85. package/dist/server/repositories/goals.js +159 -0
  86. package/dist/server/repositories/projects.js +214 -0
  87. package/dist/server/repositories/psyche.js +1356 -0
  88. package/dist/server/repositories/rewards.js +675 -0
  89. package/dist/server/repositories/settings.js +399 -0
  90. package/dist/server/repositories/tags.js +160 -0
  91. package/dist/server/repositories/task-runs.js +488 -0
  92. package/dist/server/repositories/tasks.js +413 -0
  93. package/dist/server/services/context.js +214 -0
  94. package/dist/server/services/dashboard.js +170 -0
  95. package/dist/server/services/entity-crud.js +576 -0
  96. package/dist/server/services/gamification.js +215 -0
  97. package/dist/server/services/insights.js +91 -0
  98. package/dist/server/services/projects.js +75 -0
  99. package/dist/server/services/psyche.js +63 -0
  100. package/dist/server/services/relations.js +28 -0
  101. package/dist/server/services/reviews.js +88 -0
  102. package/dist/server/services/run-recovery.js +13 -0
  103. package/dist/server/services/tagging.js +49 -0
  104. package/dist/server/services/task-run-watchdog.js +92 -0
  105. package/dist/server/services/work-time.js +176 -0
  106. package/dist/server/types.js +999 -0
  107. package/dist/server/web.js +91 -0
  108. package/openclaw.plugin.json +21 -9
  109. package/package.json +17 -4
  110. package/server/migrations/001_core.sql +333 -0
  111. package/server/migrations/002_psyche.sql +241 -0
  112. package/server/migrations/003_timer_execution.sql +18 -0
  113. package/server/migrations/004_psyche_linked_entities.sql +5 -0
  114. package/server/migrations/005_adaptive_schemas.sql +157 -0
  115. package/server/migrations/006_psyche_auth_setting.sql +4 -0
  116. package/server/migrations/007_deleted_entities.sql +16 -0
  117. package/skills/forge-openclaw/SKILL.md +189 -275
@@ -0,0 +1,399 @@
1
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
2
+ import { getDatabase, runInTransaction } from "../db.js";
3
+ import { recordActivityEvent } from "./activity-events.js";
4
+ import { recordEventLog } from "./event-log.js";
5
+ import { createAgentTokenSchema, agentIdentitySchema, settingsPayloadSchema, updateSettingsSchema } from "../types.js";
6
+ function boolFromInt(value) {
7
+ return value === 1;
8
+ }
9
+ function toInt(value) {
10
+ return value ? 1 : 0;
11
+ }
12
+ function hashToken(token) {
13
+ return createHash("sha256").update(token).digest("hex");
14
+ }
15
+ function buildTokenSecret() {
16
+ return `fg_live_${randomBytes(18).toString("hex")}`;
17
+ }
18
+ function mapAgent(row) {
19
+ return agentIdentitySchema.parse({
20
+ id: row.id,
21
+ label: row.label,
22
+ agentType: row.agent_type,
23
+ trustLevel: row.trust_level,
24
+ autonomyMode: row.autonomy_mode,
25
+ approvalMode: row.approval_mode,
26
+ description: row.description,
27
+ tokenCount: row.token_count,
28
+ activeTokenCount: row.active_token_count,
29
+ createdAt: row.created_at,
30
+ updatedAt: row.updated_at
31
+ });
32
+ }
33
+ function mapToken(row) {
34
+ return {
35
+ id: row.id,
36
+ label: row.label,
37
+ tokenPrefix: row.token_prefix,
38
+ scopes: JSON.parse(row.scopes_json),
39
+ agentId: row.agent_id,
40
+ agentLabel: row.agent_label,
41
+ trustLevel: row.trust_level,
42
+ autonomyMode: row.autonomy_mode,
43
+ approvalMode: row.approval_mode,
44
+ description: row.description,
45
+ lastUsedAt: row.last_used_at,
46
+ revokedAt: row.revoked_at,
47
+ createdAt: row.created_at,
48
+ updatedAt: row.updated_at,
49
+ status: row.revoked_at ? "revoked" : "active"
50
+ };
51
+ }
52
+ function findAgentIdentity(agentId) {
53
+ const row = getDatabase()
54
+ .prepare(`SELECT
55
+ agent_identities.id,
56
+ agent_identities.label,
57
+ agent_identities.agent_type,
58
+ agent_identities.trust_level,
59
+ agent_identities.autonomy_mode,
60
+ agent_identities.approval_mode,
61
+ agent_identities.description,
62
+ agent_identities.created_at,
63
+ agent_identities.updated_at,
64
+ COUNT(agent_tokens.id) AS token_count,
65
+ COALESCE(SUM(CASE WHEN agent_tokens.revoked_at IS NULL THEN 1 ELSE 0 END), 0) AS active_token_count
66
+ FROM agent_identities
67
+ LEFT JOIN agent_tokens ON agent_tokens.agent_id = agent_identities.id
68
+ WHERE agent_identities.id = ?
69
+ GROUP BY agent_identities.id`)
70
+ .get(agentId);
71
+ return row ? mapAgent(row) : undefined;
72
+ }
73
+ function upsertAgentIdentity(input) {
74
+ const now = new Date().toISOString();
75
+ const existing = getDatabase()
76
+ .prepare(`SELECT id
77
+ FROM agent_identities
78
+ WHERE lower(label) = lower(?)
79
+ LIMIT 1`)
80
+ .get(input.agentLabel);
81
+ if (existing) {
82
+ getDatabase()
83
+ .prepare(`UPDATE agent_identities
84
+ SET agent_type = ?, trust_level = ?, autonomy_mode = ?, approval_mode = ?, description = ?, updated_at = ?
85
+ WHERE id = ?`)
86
+ .run(input.agentType, input.trustLevel, input.autonomyMode, input.approvalMode, input.description, now, existing.id);
87
+ return findAgentIdentity(existing.id);
88
+ }
89
+ const agentId = `agt_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
90
+ getDatabase()
91
+ .prepare(`INSERT INTO agent_identities (
92
+ id, label, agent_type, trust_level, autonomy_mode, approval_mode, description, created_at, updated_at
93
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
94
+ .run(agentId, input.agentLabel, input.agentType, input.trustLevel, input.autonomyMode, input.approvalMode, input.description, now, now);
95
+ return findAgentIdentity(agentId);
96
+ }
97
+ function ensureSettingsRow(now = new Date().toISOString()) {
98
+ getDatabase()
99
+ .prepare(`INSERT OR IGNORE INTO app_settings (
100
+ id, operator_name, operator_email, operator_title, theme_preference, locale_preference, goal_drift_alerts,
101
+ daily_quest_reminders, achievement_celebrations, max_active_tasks, time_accounting_mode, integrity_score, last_audit_at, created_at, updated_at
102
+ ) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
103
+ .run("Master Architect", "architect@kineticforge.ai", "Local-first operator", "obsidian", "en", 1, 1, 1, 2, "split", 98, now, now, now);
104
+ }
105
+ function readSettingsRow() {
106
+ ensureSettingsRow();
107
+ return getDatabase()
108
+ .prepare(`SELECT
109
+ operator_name, operator_email, operator_title, theme_preference, locale_preference,
110
+ goal_drift_alerts, daily_quest_reminders, achievement_celebrations, max_active_tasks, time_accounting_mode,
111
+ integrity_score, last_audit_at, psyche_auth_required, created_at, updated_at
112
+ FROM app_settings
113
+ WHERE id = 1`)
114
+ .get();
115
+ }
116
+ export function listAgentTokens() {
117
+ const rows = getDatabase()
118
+ .prepare(`SELECT
119
+ agent_tokens.id,
120
+ agent_tokens.label,
121
+ agent_tokens.token_prefix,
122
+ agent_tokens.scopes_json,
123
+ agent_tokens.agent_id,
124
+ agent_identities.label AS agent_label,
125
+ agent_tokens.trust_level,
126
+ agent_tokens.autonomy_mode,
127
+ agent_tokens.approval_mode,
128
+ agent_tokens.description,
129
+ agent_tokens.last_used_at,
130
+ agent_tokens.revoked_at,
131
+ agent_tokens.created_at,
132
+ agent_tokens.updated_at
133
+ FROM agent_tokens
134
+ LEFT JOIN agent_identities ON agent_identities.id = agent_tokens.agent_id
135
+ ORDER BY agent_tokens.created_at DESC`)
136
+ .all();
137
+ return rows.map(mapToken);
138
+ }
139
+ export function listAgentIdentities() {
140
+ const rows = getDatabase()
141
+ .prepare(`SELECT
142
+ agent_identities.id,
143
+ agent_identities.label,
144
+ agent_identities.agent_type,
145
+ agent_identities.trust_level,
146
+ agent_identities.autonomy_mode,
147
+ agent_identities.approval_mode,
148
+ agent_identities.description,
149
+ agent_identities.created_at,
150
+ agent_identities.updated_at,
151
+ COUNT(agent_tokens.id) AS token_count,
152
+ COALESCE(SUM(CASE WHEN agent_tokens.revoked_at IS NULL THEN 1 ELSE 0 END), 0) AS active_token_count
153
+ FROM agent_identities
154
+ LEFT JOIN agent_tokens ON agent_tokens.agent_id = agent_identities.id
155
+ GROUP BY agent_identities.id
156
+ ORDER BY agent_identities.created_at DESC`)
157
+ .all();
158
+ return rows.map(mapAgent);
159
+ }
160
+ export function isPsycheAuthRequired() {
161
+ ensureSettingsRow();
162
+ const row = getDatabase()
163
+ .prepare(`SELECT psyche_auth_required FROM app_settings WHERE id = 1`)
164
+ .get();
165
+ return row ? boolFromInt(row.psyche_auth_required) : false;
166
+ }
167
+ export function getSettings() {
168
+ const row = readSettingsRow();
169
+ return settingsPayloadSchema.parse({
170
+ profile: {
171
+ operatorName: row.operator_name,
172
+ operatorEmail: row.operator_email,
173
+ operatorTitle: row.operator_title
174
+ },
175
+ notifications: {
176
+ goalDriftAlerts: boolFromInt(row.goal_drift_alerts),
177
+ dailyQuestReminders: boolFromInt(row.daily_quest_reminders),
178
+ achievementCelebrations: boolFromInt(row.achievement_celebrations)
179
+ },
180
+ execution: {
181
+ maxActiveTasks: row.max_active_tasks,
182
+ timeAccountingMode: row.time_accounting_mode
183
+ },
184
+ themePreference: row.theme_preference,
185
+ localePreference: row.locale_preference,
186
+ security: {
187
+ integrityScore: row.integrity_score,
188
+ lastAuditAt: row.last_audit_at,
189
+ storageMode: "local-first",
190
+ activeSessions: 1,
191
+ tokenCount: listAgentTokens().filter((token) => token.status === "active").length,
192
+ psycheAuthRequired: boolFromInt(row.psyche_auth_required)
193
+ },
194
+ agents: listAgentIdentities(),
195
+ agentTokens: listAgentTokens()
196
+ });
197
+ }
198
+ export function updateSettings(input, activity) {
199
+ const parsed = updateSettingsSchema.parse(input);
200
+ return runInTransaction(() => {
201
+ const current = getSettings();
202
+ const now = new Date().toISOString();
203
+ const next = {
204
+ profile: {
205
+ operatorName: parsed.profile?.operatorName ?? current.profile.operatorName,
206
+ operatorEmail: parsed.profile?.operatorEmail ?? current.profile.operatorEmail,
207
+ operatorTitle: parsed.profile?.operatorTitle ?? current.profile.operatorTitle
208
+ },
209
+ notifications: {
210
+ goalDriftAlerts: parsed.notifications?.goalDriftAlerts ?? current.notifications.goalDriftAlerts,
211
+ dailyQuestReminders: parsed.notifications?.dailyQuestReminders ?? current.notifications.dailyQuestReminders,
212
+ achievementCelebrations: parsed.notifications?.achievementCelebrations ?? current.notifications.achievementCelebrations
213
+ },
214
+ execution: {
215
+ maxActiveTasks: parsed.execution?.maxActiveTasks ?? current.execution.maxActiveTasks,
216
+ timeAccountingMode: parsed.execution?.timeAccountingMode ?? current.execution.timeAccountingMode
217
+ },
218
+ themePreference: parsed.themePreference ?? current.themePreference,
219
+ localePreference: parsed.localePreference ?? current.localePreference,
220
+ psycheAuthRequired: parsed.security?.psycheAuthRequired ?? current.security.psycheAuthRequired
221
+ };
222
+ getDatabase()
223
+ .prepare(`UPDATE app_settings
224
+ SET operator_name = ?, operator_email = ?, operator_title = ?, theme_preference = ?, locale_preference = ?,
225
+ goal_drift_alerts = ?, daily_quest_reminders = ?, achievement_celebrations = ?, max_active_tasks = ?, time_accounting_mode = ?,
226
+ psyche_auth_required = ?, updated_at = ?
227
+ WHERE id = 1`)
228
+ .run(next.profile.operatorName, next.profile.operatorEmail, next.profile.operatorTitle, next.themePreference, next.localePreference, toInt(next.notifications.goalDriftAlerts), toInt(next.notifications.dailyQuestReminders), toInt(next.notifications.achievementCelebrations), next.execution.maxActiveTasks, next.execution.timeAccountingMode, toInt(next.psycheAuthRequired), now);
229
+ if (activity) {
230
+ recordActivityEvent({
231
+ entityType: "system",
232
+ entityId: "app_settings",
233
+ eventType: "settings_updated",
234
+ title: "Forge settings updated",
235
+ description: `Theme is now ${next.themePreference}. Language is ${next.localePreference}.`,
236
+ actor: activity.actor ?? null,
237
+ source: activity.source,
238
+ metadata: {
239
+ themePreference: next.themePreference,
240
+ localePreference: next.localePreference,
241
+ goalDriftAlerts: next.notifications.goalDriftAlerts,
242
+ dailyQuestReminders: next.notifications.dailyQuestReminders,
243
+ maxActiveTasks: next.execution.maxActiveTasks,
244
+ timeAccountingMode: next.execution.timeAccountingMode
245
+ }
246
+ });
247
+ }
248
+ return getSettings();
249
+ });
250
+ }
251
+ export function createAgentToken(input, activity) {
252
+ const parsed = createAgentTokenSchema.parse(input);
253
+ return runInTransaction(() => {
254
+ const now = new Date().toISOString();
255
+ const agent = upsertAgentIdentity(parsed);
256
+ const id = `tok_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
257
+ const token = buildTokenSecret();
258
+ const tokenPrefix = `${token.slice(0, 10)}••••`;
259
+ getDatabase()
260
+ .prepare(`INSERT INTO agent_tokens (
261
+ id, label, token_hash, token_prefix, scopes_json, agent_id, trust_level, autonomy_mode, approval_mode, description, created_at, updated_at
262
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
263
+ .run(id, parsed.label, hashToken(token), tokenPrefix, JSON.stringify(parsed.scopes), agent.id, parsed.trustLevel, parsed.autonomyMode, parsed.approvalMode, parsed.description, now, now);
264
+ const tokenSummary = listAgentTokens().find((entry) => entry.id === id);
265
+ if (activity) {
266
+ recordActivityEvent({
267
+ entityType: "system",
268
+ entityId: id,
269
+ eventType: "agent_token_created",
270
+ title: `Agent token created: ${parsed.label}`,
271
+ description: "A new local API token was issued.",
272
+ actor: activity.actor ?? null,
273
+ source: activity.source,
274
+ metadata: {
275
+ agentId: agent.id,
276
+ agentLabel: agent.label,
277
+ scopes: parsed.scopes.join(",")
278
+ }
279
+ });
280
+ recordEventLog({
281
+ eventKind: "agent.token_created",
282
+ entityType: "agent_token",
283
+ entityId: id,
284
+ actor: activity.actor ?? null,
285
+ source: activity.source,
286
+ metadata: {
287
+ agentId: agent.id,
288
+ trustLevel: parsed.trustLevel,
289
+ autonomyMode: parsed.autonomyMode,
290
+ approvalMode: parsed.approvalMode
291
+ }
292
+ });
293
+ }
294
+ return {
295
+ token,
296
+ tokenSummary
297
+ };
298
+ });
299
+ }
300
+ export function rotateAgentToken(tokenId, activity) {
301
+ const existing = listAgentTokens().find((token) => token.id === tokenId);
302
+ if (!existing) {
303
+ return null;
304
+ }
305
+ return runInTransaction(() => {
306
+ const now = new Date().toISOString();
307
+ const token = buildTokenSecret();
308
+ const tokenPrefix = `${token.slice(0, 10)}••••`;
309
+ getDatabase()
310
+ .prepare(`UPDATE agent_tokens SET token_hash = ?, token_prefix = ?, revoked_at = NULL, updated_at = ? WHERE id = ?`)
311
+ .run(hashToken(token), tokenPrefix, now, tokenId);
312
+ const tokenSummary = listAgentTokens().find((entry) => entry.id === tokenId);
313
+ if (activity) {
314
+ recordActivityEvent({
315
+ entityType: "system",
316
+ entityId: tokenId,
317
+ eventType: "agent_token_rotated",
318
+ title: `Agent token rotated: ${existing.label}`,
319
+ description: "Local API token credentials were rotated.",
320
+ actor: activity.actor ?? null,
321
+ source: activity.source
322
+ });
323
+ recordEventLog({
324
+ eventKind: "agent.token_rotated",
325
+ entityType: "agent_token",
326
+ entityId: tokenId,
327
+ actor: activity.actor ?? null,
328
+ source: activity.source
329
+ });
330
+ }
331
+ return {
332
+ token,
333
+ tokenSummary
334
+ };
335
+ });
336
+ }
337
+ export function revokeAgentToken(tokenId, activity) {
338
+ const existing = listAgentTokens().find((token) => token.id === tokenId);
339
+ if (!existing) {
340
+ return null;
341
+ }
342
+ return runInTransaction(() => {
343
+ const now = new Date().toISOString();
344
+ getDatabase()
345
+ .prepare(`UPDATE agent_tokens SET revoked_at = ?, updated_at = ? WHERE id = ?`)
346
+ .run(now, now, tokenId);
347
+ const tokenSummary = listAgentTokens().find((entry) => entry.id === tokenId);
348
+ if (activity) {
349
+ recordActivityEvent({
350
+ entityType: "system",
351
+ entityId: tokenId,
352
+ eventType: "agent_token_revoked",
353
+ title: `Agent token revoked: ${existing.label}`,
354
+ description: "The token can no longer access the local API.",
355
+ actor: activity.actor ?? null,
356
+ source: activity.source
357
+ });
358
+ recordEventLog({
359
+ eventKind: "agent.token_revoked",
360
+ entityType: "agent_token",
361
+ entityId: tokenId,
362
+ actor: activity.actor ?? null,
363
+ source: activity.source
364
+ });
365
+ }
366
+ return tokenSummary;
367
+ });
368
+ }
369
+ export function getAgentTokenById(tokenId) {
370
+ return listAgentTokens().find((token) => token.id === tokenId);
371
+ }
372
+ export function verifyAgentToken(token) {
373
+ const hash = hashToken(token);
374
+ const row = getDatabase()
375
+ .prepare(`SELECT
376
+ agent_tokens.id,
377
+ agent_tokens.label,
378
+ agent_tokens.token_prefix,
379
+ agent_tokens.scopes_json,
380
+ agent_tokens.agent_id,
381
+ agent_identities.label AS agent_label,
382
+ agent_tokens.trust_level,
383
+ agent_tokens.autonomy_mode,
384
+ agent_tokens.approval_mode,
385
+ agent_tokens.description,
386
+ agent_tokens.last_used_at,
387
+ agent_tokens.revoked_at,
388
+ agent_tokens.created_at,
389
+ agent_tokens.updated_at
390
+ FROM agent_tokens
391
+ LEFT JOIN agent_identities ON agent_identities.id = agent_tokens.agent_id
392
+ WHERE agent_tokens.token_hash = ?`)
393
+ .get(hash);
394
+ if (!row || row.revoked_at) {
395
+ return null;
396
+ }
397
+ getDatabase().prepare(`UPDATE agent_tokens SET last_used_at = ?, updated_at = ? WHERE id = ?`).run(new Date().toISOString(), new Date().toISOString(), row.id);
398
+ return mapToken(row);
399
+ }
@@ -0,0 +1,160 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { getDatabase } from "../db.js";
3
+ import { HttpError } from "../errors.js";
4
+ import { filterDeletedEntities, isEntityDeleted } from "./deleted-entities.js";
5
+ import { recordActivityEvent } from "./activity-events.js";
6
+ import { tagSchema, updateTagSchema } from "../types.js";
7
+ function mapTag(row) {
8
+ return tagSchema.parse({
9
+ id: row.id,
10
+ name: row.name,
11
+ kind: row.kind,
12
+ color: row.color,
13
+ description: row.description
14
+ });
15
+ }
16
+ export function listTags() {
17
+ const rows = getDatabase()
18
+ .prepare(`SELECT id, name, kind, color, description
19
+ FROM tags
20
+ ORDER BY
21
+ CASE kind WHEN 'value' THEN 0 WHEN 'category' THEN 1 ELSE 2 END,
22
+ name`)
23
+ .all();
24
+ return filterDeletedEntities("tag", rows.map(mapTag));
25
+ }
26
+ export function getTagById(tagId) {
27
+ if (isEntityDeleted("tag", tagId)) {
28
+ return undefined;
29
+ }
30
+ const row = getDatabase()
31
+ .prepare(`SELECT id, name, kind, color, description
32
+ FROM tags
33
+ WHERE id = ?`)
34
+ .get(tagId);
35
+ return row ? mapTag(row) : undefined;
36
+ }
37
+ export function createTag(input, activity) {
38
+ const now = new Date().toISOString();
39
+ const normalizedName = input.name.trim();
40
+ const existing = getDatabase()
41
+ .prepare(`SELECT id, name, kind, color, description
42
+ FROM tags
43
+ WHERE lower(name) = lower(?)`)
44
+ .get(normalizedName);
45
+ if (existing) {
46
+ return mapTag(existing);
47
+ }
48
+ const tag = tagSchema.parse({
49
+ id: `tag_${randomUUID().replaceAll("-", "").slice(0, 10)}`,
50
+ name: normalizedName,
51
+ kind: input.kind,
52
+ color: input.color,
53
+ description: input.description
54
+ });
55
+ getDatabase()
56
+ .prepare(`INSERT INTO tags (id, name, kind, color, description, created_at)
57
+ VALUES (?, ?, ?, ?, ?, ?)`)
58
+ .run(tag.id, tag.name, tag.kind, tag.color, tag.description, now);
59
+ if (activity) {
60
+ recordActivityEvent({
61
+ entityType: "tag",
62
+ entityId: tag.id,
63
+ eventType: "tag_created",
64
+ title: `Tag created: ${tag.name}`,
65
+ description: tag.description || `New ${tag.kind} tag added to the operating system.`,
66
+ actor: activity.actor ?? null,
67
+ source: activity.source,
68
+ metadata: {
69
+ kind: tag.kind,
70
+ color: tag.color
71
+ }
72
+ });
73
+ }
74
+ return tag;
75
+ }
76
+ export function updateTag(tagId, input, activity) {
77
+ const current = getTagById(tagId);
78
+ if (!current) {
79
+ return undefined;
80
+ }
81
+ const parsed = updateTagSchema.parse(input);
82
+ const nextName = parsed.name ?? current.name;
83
+ const duplicate = getDatabase()
84
+ .prepare(`SELECT id
85
+ FROM tags
86
+ WHERE lower(name) = lower(?)
87
+ AND id != ?`)
88
+ .get(nextName, tagId);
89
+ if (duplicate) {
90
+ throw new HttpError(409, "tag_conflict", `A tag named '${nextName}' already exists`);
91
+ }
92
+ const tag = tagSchema.parse({
93
+ id: current.id,
94
+ name: nextName,
95
+ kind: parsed.kind ?? current.kind,
96
+ color: parsed.color ?? current.color,
97
+ description: parsed.description ?? current.description
98
+ });
99
+ getDatabase()
100
+ .prepare(`UPDATE tags
101
+ SET name = ?, kind = ?, color = ?, description = ?
102
+ WHERE id = ?`)
103
+ .run(tag.name, tag.kind, tag.color, tag.description, tagId);
104
+ if (activity) {
105
+ recordActivityEvent({
106
+ entityType: "tag",
107
+ entityId: tag.id,
108
+ eventType: "tag_updated",
109
+ title: `Tag updated: ${tag.name}`,
110
+ description: tag.description || "Tag details were updated.",
111
+ actor: activity.actor ?? null,
112
+ source: activity.source,
113
+ metadata: {
114
+ previousName: current.name,
115
+ kind: tag.kind,
116
+ previousKind: current.kind,
117
+ color: tag.color,
118
+ previousColor: current.color
119
+ }
120
+ });
121
+ }
122
+ return tag;
123
+ }
124
+ export function deleteTag(tagId, activity) {
125
+ const current = getTagById(tagId);
126
+ if (!current) {
127
+ return undefined;
128
+ }
129
+ getDatabase()
130
+ .prepare(`DELETE FROM tags WHERE id = ?`)
131
+ .run(tagId);
132
+ if (activity) {
133
+ recordActivityEvent({
134
+ entityType: "tag",
135
+ entityId: current.id,
136
+ eventType: "tag_deleted",
137
+ title: `Tag deleted: ${current.name}`,
138
+ description: current.description || "Tag removed from the system.",
139
+ actor: activity.actor ?? null,
140
+ source: activity.source,
141
+ metadata: {
142
+ kind: current.kind,
143
+ color: current.color
144
+ }
145
+ });
146
+ }
147
+ return current;
148
+ }
149
+ export function listTagsByIds(tagIds) {
150
+ if (tagIds.length === 0) {
151
+ return [];
152
+ }
153
+ const placeholders = tagIds.map(() => "?").join(", ");
154
+ const rows = getDatabase()
155
+ .prepare(`SELECT id, name, kind, color, description
156
+ FROM tags
157
+ WHERE id IN (${placeholders})`)
158
+ .all(...tagIds);
159
+ return filterDeletedEntities("tag", rows.map(mapTag));
160
+ }