forge-openclaw-plugin 0.2.3 → 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.
- package/README.md +114 -6
- package/dist/assets/board-CzgvdLO8.js +6 -0
- package/dist/assets/board-CzgvdLO8.js.map +1 -0
- package/dist/assets/favicon-BCHm9dUV.ico +0 -0
- package/dist/assets/index-8d_oM8fL.js +27 -0
- package/dist/assets/index-8d_oM8fL.js.map +1 -0
- package/dist/assets/index-D4A_bq8m.css +1 -0
- package/dist/assets/motion-STUd1O46.js +10 -0
- package/dist/assets/motion-STUd1O46.js.map +1 -0
- package/dist/assets/plus-jakarta-sans-latin-ext-wght-normal-DmpS2jIq.woff2 +0 -0
- package/dist/assets/plus-jakarta-sans-latin-wght-normal-eXO_dkmS.woff2 +0 -0
- package/dist/assets/plus-jakarta-sans-vietnamese-wght-normal-qRpaaN48.woff2 +0 -0
- package/dist/assets/sora-latin-ext-wght-normal-CawQDOvP.woff2 +0 -0
- package/dist/assets/sora-latin-wght-normal-DdqRvwsR.woff2 +0 -0
- package/dist/assets/space-grotesk-latin-500-normal-CNSSEhBt.woff +0 -0
- package/dist/assets/space-grotesk-latin-500-normal-lFbtlQH6.woff2 +0 -0
- package/dist/assets/space-grotesk-latin-700-normal-CwsQ-cCU.woff +0 -0
- package/dist/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
- package/dist/assets/space-grotesk-latin-ext-500-normal-3dgZTiw9.woff +0 -0
- package/dist/assets/space-grotesk-latin-ext-500-normal-DUe3BAxM.woff2 +0 -0
- package/dist/assets/space-grotesk-latin-ext-700-normal-BQnZhY3m.woff2 +0 -0
- package/dist/assets/space-grotesk-latin-ext-700-normal-HVCqSBdx.woff +0 -0
- package/dist/assets/space-grotesk-vietnamese-500-normal-BTqKIpxg.woff +0 -0
- package/dist/assets/space-grotesk-vietnamese-500-normal-BmEvtly_.woff2 +0 -0
- package/dist/assets/space-grotesk-vietnamese-700-normal-DMty7AZE.woff2 +0 -0
- package/dist/assets/space-grotesk-vietnamese-700-normal-Duxec5Rn.woff +0 -0
- package/dist/assets/table-CtNlETLc.js +23 -0
- package/dist/assets/table-CtNlETLc.js.map +1 -0
- package/dist/assets/ui-ThzkR_oW.js +46 -0
- package/dist/assets/ui-ThzkR_oW.js.map +1 -0
- package/dist/assets/vendor-CRS-psbw.css +1 -0
- package/dist/assets/vendor-DyHAI6nk.js +423 -0
- package/dist/assets/vendor-DyHAI6nk.js.map +1 -0
- package/dist/assets/viz-BJuBCz_G.js +34 -0
- package/dist/assets/viz-BJuBCz_G.js.map +1 -0
- package/dist/favicon.ico +0 -0
- package/dist/favicon.png +0 -0
- package/dist/index.html +29 -0
- package/dist/openclaw/api-client.d.ts +8 -0
- package/dist/openclaw/api-client.js +31 -4
- package/dist/openclaw/local-runtime.d.ts +3 -0
- package/dist/openclaw/local-runtime.js +135 -0
- package/dist/openclaw/parity.d.ts +4 -4
- package/dist/openclaw/parity.js +23 -33
- package/dist/openclaw/plugin-entry-shared.d.ts +5 -3
- package/dist/openclaw/plugin-entry-shared.js +52 -10
- package/dist/openclaw/routes.d.ts +12 -3
- package/dist/openclaw/routes.js +156 -924
- package/dist/openclaw/tools.js +242 -1100
- package/dist/server/app.js +2450 -0
- package/dist/server/db.js +313 -0
- package/dist/server/e2e-server.js +20 -0
- package/dist/server/errors.js +15 -0
- package/dist/server/index.js +16 -0
- package/dist/server/managers/base.js +17 -0
- package/dist/server/managers/contracts.js +47 -0
- package/dist/server/managers/platform/api-gateway-manager.js +11 -0
- package/dist/server/managers/platform/audit-manager.js +15 -0
- package/dist/server/managers/platform/authentication-manager.js +56 -0
- package/dist/server/managers/platform/authorization-manager.js +56 -0
- package/dist/server/managers/platform/background-job-manager.js +10 -0
- package/dist/server/managers/platform/configuration-manager.js +33 -0
- package/dist/server/managers/platform/database-manager.js +14 -0
- package/dist/server/managers/platform/event-bus-manager.js +7 -0
- package/dist/server/managers/platform/external-service-manager.js +11 -0
- package/dist/server/managers/platform/health-manager.js +7 -0
- package/dist/server/managers/platform/migration-manager.js +8 -0
- package/dist/server/managers/platform/search-index-manager.js +4 -0
- package/dist/server/managers/platform/secrets-manager.js +19 -0
- package/dist/server/managers/platform/session-manager.js +121 -0
- package/dist/server/managers/platform/storage-manager.js +16 -0
- package/dist/server/managers/platform/token-manager.js +37 -0
- package/dist/server/managers/platform/transaction-manager.js +8 -0
- package/dist/server/managers/platform/trusted-network.js +39 -0
- package/dist/server/managers/runtime.js +56 -0
- package/dist/server/managers/type-guards.js +4 -0
- package/dist/server/openapi.js +3512 -0
- package/dist/server/psyche-types.js +395 -0
- package/dist/server/repositories/activity-events.js +157 -0
- package/dist/server/repositories/collaboration.js +497 -0
- package/dist/server/repositories/comments.js +176 -0
- package/dist/server/repositories/deleted-entities.js +192 -0
- package/dist/server/repositories/domains.js +30 -0
- package/dist/server/repositories/event-log.js +64 -0
- package/dist/server/repositories/goals.js +159 -0
- package/dist/server/repositories/projects.js +214 -0
- package/dist/server/repositories/psyche.js +1356 -0
- package/dist/server/repositories/rewards.js +675 -0
- package/dist/server/repositories/settings.js +399 -0
- package/dist/server/repositories/tags.js +160 -0
- package/dist/server/repositories/task-runs.js +488 -0
- package/dist/server/repositories/tasks.js +413 -0
- package/dist/server/services/context.js +214 -0
- package/dist/server/services/dashboard.js +170 -0
- package/dist/server/services/entity-crud.js +576 -0
- package/dist/server/services/gamification.js +215 -0
- package/dist/server/services/insights.js +91 -0
- package/dist/server/services/projects.js +75 -0
- package/dist/server/services/psyche.js +63 -0
- package/dist/server/services/relations.js +28 -0
- package/dist/server/services/reviews.js +88 -0
- package/dist/server/services/run-recovery.js +13 -0
- package/dist/server/services/tagging.js +49 -0
- package/dist/server/services/task-run-watchdog.js +92 -0
- package/dist/server/services/work-time.js +176 -0
- package/dist/server/types.js +999 -0
- package/dist/server/web.js +91 -0
- package/openclaw.plugin.json +22 -10
- package/package.json +17 -4
- package/server/migrations/001_core.sql +333 -0
- package/server/migrations/002_psyche.sql +241 -0
- package/server/migrations/003_timer_execution.sql +18 -0
- package/server/migrations/004_psyche_linked_entities.sql +5 -0
- package/server/migrations/005_adaptive_schemas.sql +157 -0
- package/server/migrations/006_psyche_auth_setting.sql +4 -0
- package/server/migrations/007_deleted_entities.sql +16 -0
- 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
|
+
}
|