forge-openclaw-plugin 0.2.48 → 0.2.49

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.
@@ -1,7 +1,8 @@
1
- import { randomUUID } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import { getDatabase, runInTransaction } from "../db.js";
3
3
  import { recordActivityEvent } from "./activity-events.js";
4
4
  import { listAgentActions } from "./collaboration.js";
5
+ import { ensureBotUser, getUserById } from "./users.js";
5
6
  import { agentActionSchema, agentRuntimeEventLevelSchema, agentRuntimeReconnectPlanSchema, agentRuntimeSessionEventSchema, agentRuntimeSessionSchema, createAgentRuntimeSessionEventSchema, createAgentRuntimeSessionSchema, disconnectAgentRuntimeSessionSchema, heartbeatAgentRuntimeSessionSchema, reconnectAgentRuntimeSessionSchema } from "../types.js";
6
7
  function parseMetadata(raw) {
7
8
  try {
@@ -211,7 +212,95 @@ function ensureCurrentSessionInstance(row, externalSessionId) {
211
212
  }
212
213
  return true;
213
214
  }
214
- function disconnectSupersededSingletonSessions(parsed, sessionId, now) {
215
+ function normalizeIdentityPart(value) {
216
+ return (value
217
+ ?.trim()
218
+ .toLowerCase()
219
+ .replace(/[^a-z0-9._:]+/g, "_")
220
+ .replace(/^_+|_+$/g, "") ?? "");
221
+ }
222
+ function shortHash(value) {
223
+ return createHash("sha1").update(value).digest("hex").slice(0, 12);
224
+ }
225
+ function canonicalRuntimeAgentLabel(provider) {
226
+ if (provider === "openclaw") {
227
+ return "Forge OpenClaw";
228
+ }
229
+ if (provider === "hermes") {
230
+ return "Forge Hermes";
231
+ }
232
+ return "Forge Codex";
233
+ }
234
+ function canonicalRuntimeDescription(provider) {
235
+ return `${canonicalRuntimeAgentLabel(provider)} runtime agent with stable Forge identity and linked Kanban user.`;
236
+ }
237
+ function canonicalAgentUserSpec(provider) {
238
+ if (provider === "openclaw") {
239
+ return {
240
+ id: "user_agent_openclaw",
241
+ handle: "openclaw",
242
+ displayName: "OpenClaw",
243
+ description: "OpenClaw runtime actor linked to Forge agent identity and Kanban ownership.",
244
+ accentColor: "#38bdf8"
245
+ };
246
+ }
247
+ if (provider === "hermes") {
248
+ return {
249
+ id: "user_agent_hermes",
250
+ handle: "hermes",
251
+ displayName: "Hermes",
252
+ description: "Hermes runtime actor linked to Forge agent identity and Kanban ownership.",
253
+ accentColor: "#a78bfa"
254
+ };
255
+ }
256
+ return {
257
+ id: "user_agent_codex",
258
+ handle: "codex",
259
+ displayName: "Codex",
260
+ description: "Codex runtime actor linked to Forge agent identity and Kanban ownership.",
261
+ accentColor: "#22c55e"
262
+ };
263
+ }
264
+ function deriveMachineKey(input) {
265
+ const explicit = normalizeIdentityPart(input.machineKey);
266
+ if (explicit) {
267
+ return explicit;
268
+ }
269
+ const source = [
270
+ normalizeText(input.dataRoot) ?? "",
271
+ normalizeText(input.baseUrl) ?? "local"
272
+ ].join("|");
273
+ return `machine_${shortHash(source)}`;
274
+ }
275
+ function derivePersonaKey(input) {
276
+ return (normalizeIdentityPart(input.personaKey) ||
277
+ normalizeIdentityPart(input.agentType) ||
278
+ "default");
279
+ }
280
+ function deriveAgentIdentityKey(input) {
281
+ const explicit = normalizeIdentityPart(input.agentIdentityKey);
282
+ if (explicit) {
283
+ return explicit;
284
+ }
285
+ return `runtime:${input.provider}:${deriveMachineKey(input)}:${derivePersonaKey(input)}`;
286
+ }
287
+ function linkAgentIdentityUsers(agentId, provider, linkedUserIds, now) {
288
+ const primaryUser = ensureBotUser(canonicalAgentUserSpec(provider));
289
+ const normalizedUserIds = Array.from(new Set([primaryUser.id, ...linkedUserIds.map((id) => id.trim()).filter(Boolean)]));
290
+ for (const userId of normalizedUserIds) {
291
+ if (!getUserById(userId)) {
292
+ continue;
293
+ }
294
+ getDatabase()
295
+ .prepare(`INSERT INTO agent_identity_users (agent_id, user_id, role, created_at, updated_at)
296
+ VALUES (?, ?, ?, ?, ?)
297
+ ON CONFLICT(agent_id, user_id) DO UPDATE SET
298
+ role = excluded.role,
299
+ updated_at = excluded.updated_at`)
300
+ .run(agentId, userId, userId === primaryUser.id ? "primary" : "linked", now, now);
301
+ }
302
+ }
303
+ function disconnectSupersededSingletonSessions(parsed, sessionId, agentId, now) {
215
304
  if (!parsed.metadata?.singleton) {
216
305
  return;
217
306
  }
@@ -224,11 +313,11 @@ function disconnectSupersededSingletonSessions(parsed, sessionId, now) {
224
313
  created_at, updated_at
225
314
  FROM agent_runtime_sessions
226
315
  WHERE provider = ?
227
- AND agent_label = ?
316
+ AND agent_id = ?
228
317
  AND coalesce(base_url, '') = coalesce(?, '')
229
318
  AND coalesce(data_root, '') = coalesce(?, '')
230
319
  AND id <> ?`)
231
- .all(parsed.provider, parsed.agentLabel, normalizeText(parsed.baseUrl), normalizeText(parsed.dataRoot), sessionId);
320
+ .all(parsed.provider, agentId, normalizeText(parsed.baseUrl), normalizeText(parsed.dataRoot), sessionId);
232
321
  for (const row of rows) {
233
322
  if (row.status === "disconnected" && row.ended_at) {
234
323
  continue;
@@ -251,29 +340,45 @@ function disconnectSupersededSingletonSessions(parsed, sessionId, now) {
251
340
  }
252
341
  }
253
342
  function upsertRuntimeAgentIdentity(input) {
343
+ const identityKey = deriveAgentIdentityKey(input);
344
+ const machineKey = deriveMachineKey(input);
345
+ const personaKey = derivePersonaKey(input);
346
+ const label = canonicalRuntimeAgentLabel(input.provider);
254
347
  const existing = getDatabase()
255
348
  .prepare(`SELECT id
256
349
  FROM agent_identities
257
- WHERE lower(label) = lower(?)
350
+ WHERE identity_key = ?
351
+ OR (
352
+ (identity_key IS NULL OR machine_key IS NULL OR machine_key = 'legacy' OR identity_key LIKE 'runtime:%:legacy:%')
353
+ AND (
354
+ provider = ?
355
+ OR lower(agent_type) = lower(?)
356
+ OR lower(label) = lower(?)
357
+ )
358
+ )
258
359
  LIMIT 1`)
259
- .get(input.agentLabel);
360
+ .get(identityKey, input.provider, input.provider, label);
260
361
  const now = new Date().toISOString();
261
- const description = `${input.provider[0].toUpperCase()}${input.provider.slice(1)} runtime session participant registered through Forge.`;
362
+ const description = canonicalRuntimeDescription(input.provider);
262
363
  if (existing) {
263
364
  getDatabase()
264
365
  .prepare(`UPDATE agent_identities
265
- SET agent_type = ?, updated_at = ?
366
+ SET label = ?, agent_type = ?, identity_key = ?, provider = ?,
367
+ machine_key = ?, persona_key = ?, description = ?, updated_at = ?
266
368
  WHERE id = ?`)
267
- .run(input.agentType || input.provider, now, existing.id);
369
+ .run(label, input.agentType || input.provider, identityKey, input.provider, machineKey, personaKey, description, now, existing.id);
370
+ linkAgentIdentityUsers(existing.id, input.provider, input.linkedUserIds, now);
268
371
  return existing.id;
269
372
  }
270
373
  const agentId = `agt_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
271
374
  getDatabase()
272
375
  .prepare(`INSERT INTO agent_identities (
273
- id, label, agent_type, trust_level, autonomy_mode, approval_mode,
376
+ id, label, agent_type, identity_key, provider, machine_key, persona_key,
377
+ trust_level, autonomy_mode, approval_mode,
274
378
  description, created_at, updated_at
275
- ) VALUES (?, ?, ?, 'trusted', 'approval_required', 'approval_by_default', ?, ?, ?)`)
276
- .run(agentId, input.agentLabel, input.agentType || input.provider, description, now, now);
379
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 'trusted', 'approval_required', 'approval_by_default', ?, ?, ?)`)
380
+ .run(agentId, label, input.agentType || input.provider, identityKey, input.provider, machineKey, personaKey, description, now, now);
381
+ linkAgentIdentityUsers(agentId, input.provider, input.linkedUserIds, now);
277
382
  return agentId;
278
383
  }
279
384
  function insertSessionEvent(sessionId, input, now = new Date().toISOString()) {
@@ -321,6 +426,7 @@ export function registerAgentRuntimeSession(input) {
321
426
  return runInTransaction(() => {
322
427
  const now = new Date().toISOString();
323
428
  const agentId = upsertRuntimeAgentIdentity(parsed);
429
+ const agentLabel = canonicalRuntimeAgentLabel(parsed.provider);
324
430
  const existing = getSessionRowByCompositeKey(parsed.provider, parsed.sessionKey);
325
431
  const sessionId = existing?.id ?? `ags_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
326
432
  if (existing) {
@@ -332,7 +438,7 @@ export function registerAgentRuntimeSession(input) {
332
438
  last_error = ?, last_seen_at = ?, last_heartbeat_at = ?, started_at = ?,
333
439
  ended_at = NULL, metadata_json = ?, updated_at = ?
334
440
  WHERE id = ?`)
335
- .run(agentId, parsed.agentLabel, parsed.agentType || parsed.provider, parsed.sessionLabel || parsed.sessionKey, parsed.actorLabel, parsed.connectionMode, parsed.status === "error" ? "error" : "connected", normalizeText(parsed.baseUrl), normalizeText(parsed.webUrl), normalizeText(parsed.dataRoot), normalizeText(parsed.externalSessionId), parsed.staleAfterSeconds, normalizeText(parsed.lastError), now, now, now, JSON.stringify(parsed.metadata), now, sessionId);
441
+ .run(agentId, agentLabel, parsed.agentType || parsed.provider, parsed.sessionLabel || parsed.sessionKey, parsed.actorLabel, parsed.connectionMode, parsed.status === "error" ? "error" : "connected", normalizeText(parsed.baseUrl), normalizeText(parsed.webUrl), normalizeText(parsed.dataRoot), normalizeText(parsed.externalSessionId), parsed.staleAfterSeconds, normalizeText(parsed.lastError), now, now, now, JSON.stringify(parsed.metadata), now, sessionId);
336
442
  insertSessionEvent(sessionId, {
337
443
  eventType: "session_registered",
338
444
  title: "Session re-registered",
@@ -349,7 +455,7 @@ export function registerAgentRuntimeSession(input) {
349
455
  last_seen_at, last_heartbeat_at, started_at, ended_at, metadata_json,
350
456
  created_at, updated_at
351
457
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, NULL, ?, ?, ?, ?, NULL, ?, ?, ?)`)
352
- .run(sessionId, agentId, parsed.agentLabel, parsed.agentType || parsed.provider, parsed.provider, parsed.sessionKey, parsed.sessionLabel || parsed.sessionKey, parsed.actorLabel, parsed.connectionMode, parsed.status === "error" ? "error" : "connected", normalizeText(parsed.baseUrl), normalizeText(parsed.webUrl), normalizeText(parsed.dataRoot), normalizeText(parsed.externalSessionId), parsed.staleAfterSeconds, normalizeText(parsed.lastError), now, now, now, JSON.stringify(parsed.metadata), now, now);
458
+ .run(sessionId, agentId, agentLabel, parsed.agentType || parsed.provider, parsed.provider, parsed.sessionKey, parsed.sessionLabel || parsed.sessionKey, parsed.actorLabel, parsed.connectionMode, parsed.status === "error" ? "error" : "connected", normalizeText(parsed.baseUrl), normalizeText(parsed.webUrl), normalizeText(parsed.dataRoot), normalizeText(parsed.externalSessionId), parsed.staleAfterSeconds, normalizeText(parsed.lastError), now, now, now, JSON.stringify(parsed.metadata), now, now);
353
459
  insertSessionEvent(sessionId, {
354
460
  eventType: "session_registered",
355
461
  title: "Session registered",
@@ -357,12 +463,12 @@ export function registerAgentRuntimeSession(input) {
357
463
  metadata: parsed.metadata
358
464
  }, now);
359
465
  }
360
- disconnectSupersededSingletonSessions(parsed, sessionId, now);
466
+ disconnectSupersededSingletonSessions(parsed, sessionId, agentId, now);
361
467
  recordActivityEvent({
362
468
  entityType: "session",
363
469
  entityId: sessionId,
364
470
  eventType: "agent_session_registered",
365
- title: `Agent session registered: ${parsed.agentLabel}`,
471
+ title: `Agent session registered: ${agentLabel}`,
366
472
  description: `${parsed.provider} registered a live agent session.`,
367
473
  actor: parsed.actorLabel,
368
474
  source: "agent",
@@ -47,6 +47,11 @@ export function buildConnectionAgentIdentity(connection) {
47
47
  id: connection.agentId,
48
48
  label: connection.agentLabel,
49
49
  agentType: connection.provider,
50
+ identityKey: `model:${connection.id}`,
51
+ provider: null,
52
+ machineKey: null,
53
+ personaKey: connection.provider,
54
+ linkedUsers: [],
50
55
  trustLevel: "trusted",
51
56
  autonomyMode: "approval_required",
52
57
  approvalMode: "approval_by_default",
@@ -7,6 +7,7 @@ import { recordActivityEvent } from "./activity-events.js";
7
7
  import { recordEventLog } from "./event-log.js";
8
8
  import { resolveGoogleCalendarOauthPublicConfig } from "../services/google-calendar-oauth-config.js";
9
9
  import { buildConnectionAgentIdentity, FORGE_DEFAULT_AGENT_ID, listAiModelConnections, syncForgeManagedWikiProfile } from "./model-settings.js";
10
+ import { listUsersByIds } from "./users.js";
10
11
  import { agentBootstrapPolicySchema, agentScopePolicySchema, createAgentTokenSchema, legacyAgentBootstrapPolicy, defaultAgentScopePolicy, agentIdentitySchema, customThemeSchema, settingsPayloadSchema, updateSettingsSchema } from "../types.js";
11
12
  const settingsFileSchema = settingsPayloadSchema.deepPartial();
12
13
  let settingsFileSyncDepth = 0;
@@ -303,11 +304,40 @@ function pickComparableOverrideSubset(source, template) {
303
304
  }
304
305
  return picked;
305
306
  }
306
- function mapAgent(row) {
307
+ function listAgentIdentityUserLinks(agentIds) {
308
+ if (agentIds.length === 0) {
309
+ return new Map();
310
+ }
311
+ const placeholders = agentIds.map(() => "?").join(",");
312
+ const rows = getDatabase()
313
+ .prepare(`SELECT agent_id, user_id, role
314
+ FROM agent_identity_users
315
+ WHERE agent_id IN (${placeholders})
316
+ ORDER BY role = 'primary' DESC, created_at ASC`)
317
+ .all(...agentIds);
318
+ const usersById = new Map(listUsersByIds(rows.map((row) => row.user_id)).map((user) => [user.id, user]));
319
+ const linksByAgentId = new Map();
320
+ for (const row of rows) {
321
+ const current = linksByAgentId.get(row.agent_id) ?? [];
322
+ current.push({
323
+ userId: row.user_id,
324
+ role: row.role,
325
+ user: usersById.get(row.user_id) ?? null
326
+ });
327
+ linksByAgentId.set(row.agent_id, current);
328
+ }
329
+ return linksByAgentId;
330
+ }
331
+ function mapAgent(row, linkedUsers = []) {
307
332
  return agentIdentitySchema.parse({
308
333
  id: row.id,
309
334
  label: row.label,
310
335
  agentType: row.agent_type,
336
+ identityKey: row.identity_key,
337
+ provider: row.provider,
338
+ machineKey: row.machine_key,
339
+ personaKey: row.persona_key,
340
+ linkedUsers,
311
341
  trustLevel: row.trust_level,
312
342
  autonomyMode: row.autonomy_mode,
313
343
  approvalMode: row.approval_mode,
@@ -351,6 +381,10 @@ function findAgentIdentity(agentId) {
351
381
  agent_identities.id,
352
382
  agent_identities.label,
353
383
  agent_identities.agent_type,
384
+ agent_identities.identity_key,
385
+ agent_identities.provider,
386
+ agent_identities.machine_key,
387
+ agent_identities.persona_key,
354
388
  agent_identities.trust_level,
355
389
  agent_identities.autonomy_mode,
356
390
  agent_identities.approval_mode,
@@ -358,36 +392,76 @@ function findAgentIdentity(agentId) {
358
392
  agent_identities.created_at,
359
393
  agent_identities.updated_at,
360
394
  COUNT(agent_tokens.id) AS token_count,
361
- COALESCE(SUM(CASE WHEN agent_tokens.revoked_at IS NULL THEN 1 ELSE 0 END), 0) AS active_token_count
395
+ COALESCE(SUM(CASE WHEN agent_tokens.id IS NOT NULL AND agent_tokens.revoked_at IS NULL THEN 1 ELSE 0 END), 0) AS active_token_count
362
396
  FROM agent_identities
363
397
  LEFT JOIN agent_tokens ON agent_tokens.agent_id = agent_identities.id
364
398
  WHERE agent_identities.id = ?
365
399
  GROUP BY agent_identities.id`)
366
400
  .get(agentId);
367
- return row ? mapAgent(row) : undefined;
401
+ const links = row ? listAgentIdentityUserLinks([row.id]) : new Map();
402
+ return row ? mapAgent(row, links.get(row.id) ?? []) : undefined;
403
+ }
404
+ function normalizeAgentIdentityPart(value) {
405
+ return value
406
+ ?.trim()
407
+ .toLowerCase()
408
+ .replace(/[^a-z0-9._:]+/g, "_")
409
+ .replace(/^_+|_+$/g, "") || "";
410
+ }
411
+ function runtimeProviderFromAgentType(agentType) {
412
+ const normalized = normalizeAgentIdentityPart(agentType);
413
+ if (normalized === "openclaw" ||
414
+ normalized === "hermes" ||
415
+ normalized === "codex") {
416
+ return normalized;
417
+ }
418
+ return null;
419
+ }
420
+ function deriveTokenAgentIdentityFields(input) {
421
+ const provider = runtimeProviderFromAgentType(input.agentType);
422
+ if (!provider) {
423
+ return {
424
+ identityKey: null,
425
+ provider: null,
426
+ machineKey: null,
427
+ personaKey: null
428
+ };
429
+ }
430
+ return {
431
+ identityKey: `runtime:${provider}:token:default`,
432
+ provider,
433
+ machineKey: "token",
434
+ personaKey: "default"
435
+ };
368
436
  }
369
437
  function upsertAgentIdentity(input) {
370
438
  const now = new Date().toISOString();
439
+ const identityFields = deriveTokenAgentIdentityFields(input);
371
440
  const existing = getDatabase()
372
441
  .prepare(`SELECT id
373
442
  FROM agent_identities
374
- WHERE lower(label) = lower(?)
443
+ WHERE (? IS NOT NULL AND identity_key = ?)
444
+ OR lower(label) = lower(?)
375
445
  LIMIT 1`)
376
- .get(input.agentLabel);
446
+ .get(identityFields.identityKey, identityFields.identityKey, input.agentLabel);
377
447
  if (existing) {
378
448
  getDatabase()
379
449
  .prepare(`UPDATE agent_identities
380
- SET agent_type = ?, trust_level = ?, autonomy_mode = ?, approval_mode = ?, description = ?, updated_at = ?
450
+ SET agent_type = ?, identity_key = COALESCE(identity_key, ?),
451
+ provider = COALESCE(provider, ?), machine_key = COALESCE(machine_key, ?),
452
+ persona_key = COALESCE(persona_key, ?), trust_level = ?,
453
+ autonomy_mode = ?, approval_mode = ?, description = ?, updated_at = ?
381
454
  WHERE id = ?`)
382
- .run(input.agentType, input.trustLevel, input.autonomyMode, input.approvalMode, input.description, now, existing.id);
455
+ .run(input.agentType, identityFields.identityKey, identityFields.provider, identityFields.machineKey, identityFields.personaKey, input.trustLevel, input.autonomyMode, input.approvalMode, input.description, now, existing.id);
383
456
  return findAgentIdentity(existing.id);
384
457
  }
385
458
  const agentId = `agt_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
386
459
  getDatabase()
387
460
  .prepare(`INSERT INTO agent_identities (
388
- id, label, agent_type, trust_level, autonomy_mode, approval_mode, description, created_at, updated_at
389
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
390
- .run(agentId, input.agentLabel, input.agentType, input.trustLevel, input.autonomyMode, input.approvalMode, input.description, now, now);
461
+ id, label, agent_type, identity_key, provider, machine_key, persona_key,
462
+ trust_level, autonomy_mode, approval_mode, description, created_at, updated_at
463
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
464
+ .run(agentId, input.agentLabel, input.agentType, identityFields.identityKey, identityFields.provider, identityFields.machineKey, identityFields.personaKey, input.trustLevel, input.autonomyMode, input.approvalMode, input.description, now, now);
391
465
  return findAgentIdentity(agentId);
392
466
  }
393
467
  function ensureSettingsRow(now = new Date().toISOString()) {
@@ -441,6 +515,10 @@ export function listAgentIdentities() {
441
515
  agent_identities.id,
442
516
  agent_identities.label,
443
517
  agent_identities.agent_type,
518
+ agent_identities.identity_key,
519
+ agent_identities.provider,
520
+ agent_identities.machine_key,
521
+ agent_identities.persona_key,
444
522
  agent_identities.trust_level,
445
523
  agent_identities.autonomy_mode,
446
524
  agent_identities.approval_mode,
@@ -448,19 +526,25 @@ export function listAgentIdentities() {
448
526
  agent_identities.created_at,
449
527
  agent_identities.updated_at,
450
528
  COUNT(agent_tokens.id) AS token_count,
451
- COALESCE(SUM(CASE WHEN agent_tokens.revoked_at IS NULL THEN 1 ELSE 0 END), 0) AS active_token_count
529
+ COALESCE(SUM(CASE WHEN agent_tokens.id IS NOT NULL AND agent_tokens.revoked_at IS NULL THEN 1 ELSE 0 END), 0) AS active_token_count
452
530
  FROM agent_identities
453
531
  LEFT JOIN agent_tokens ON agent_tokens.agent_id = agent_identities.id
454
532
  GROUP BY agent_identities.id
455
533
  ORDER BY agent_identities.created_at DESC`)
456
534
  .all();
457
- const manualAgents = rows.map(mapAgent);
535
+ const links = listAgentIdentityUserLinks(rows.map((row) => row.id));
536
+ const manualAgents = rows.map((row) => mapAgent(row, links.get(row.id) ?? []));
458
537
  const modelAgents = listAiModelConnections().map(buildConnectionAgentIdentity);
459
538
  const settings = readSettingsRow();
460
539
  const forgeAgent = agentIdentitySchema.parse({
461
540
  id: FORGE_DEFAULT_AGENT_ID,
462
541
  label: "Forge Agent",
463
542
  agentType: "forge_default",
543
+ identityKey: "forge:default",
544
+ provider: null,
545
+ machineKey: null,
546
+ personaKey: "default",
547
+ linkedUsers: [],
464
548
  trustLevel: "trusted",
465
549
  autonomyMode: "approval_required",
466
550
  approvalMode: "approval_by_default",
@@ -470,7 +554,11 @@ export function listAgentIdentities() {
470
554
  createdAt: settings.created_at,
471
555
  updatedAt: settings.updated_at
472
556
  });
473
- return [forgeAgent, ...modelAgents, ...manualAgents];
557
+ const deduped = new Map();
558
+ for (const agent of [forgeAgent, ...modelAgents, ...manualAgents]) {
559
+ deduped.set(agent.identityKey ?? agent.id, agent);
560
+ }
561
+ return Array.from(deduped.values());
474
562
  }
475
563
  export function isPsycheAuthRequired() {
476
564
  ensureSettingsRow();
@@ -153,6 +153,29 @@ export function ensureSystemUsers() {
153
153
  }
154
154
  }
155
155
  }
156
+ export function ensureBotUser(input) {
157
+ const parsed = createUserSchema.parse({
158
+ kind: "bot",
159
+ handle: normalizeHandle(input.handle),
160
+ displayName: input.displayName,
161
+ description: input.description,
162
+ accentColor: input.accentColor
163
+ });
164
+ const now = new Date().toISOString();
165
+ getDatabase()
166
+ .prepare(`INSERT INTO users (id, kind, handle, display_name, description, accent_color, created_at, updated_at)
167
+ VALUES (?, 'bot', ?, ?, ?, ?, ?, ?)
168
+ ON CONFLICT(id) DO UPDATE SET
169
+ kind = 'bot',
170
+ handle = excluded.handle,
171
+ display_name = excluded.display_name,
172
+ description = excluded.description,
173
+ accent_color = excluded.accent_color,
174
+ updated_at = excluded.updated_at`)
175
+ .run(input.id, parsed.handle, parsed.displayName, parsed.description, parsed.accentColor, now, now);
176
+ ensurePermissiveGrantsForUser(input.id, now);
177
+ return getUserById(input.id);
178
+ }
156
179
  function ensurePermissiveGrantsForUser(userId, now) {
157
180
  const existingUsers = listUsers();
158
181
  for (const otherUser of existingUsers) {
@@ -2104,6 +2104,15 @@ export const agentIdentitySchema = z.object({
2104
2104
  id: z.string(),
2105
2105
  label: z.string(),
2106
2106
  agentType: z.string(),
2107
+ identityKey: z.string().nullable().default(null),
2108
+ provider: agentRuntimeProviderSchema.nullable().default(null),
2109
+ machineKey: z.string().nullable().default(null),
2110
+ personaKey: z.string().nullable().default(null),
2111
+ linkedUsers: z.array(z.object({
2112
+ userId: z.string(),
2113
+ role: z.string(),
2114
+ user: userSummarySchema.nullable().default(null)
2115
+ })).default([]),
2107
2116
  trustLevel: agentTrustLevelSchema,
2108
2117
  autonomyMode: autonomyModeSchema,
2109
2118
  approvalMode: approvalModeSchema,
@@ -2188,6 +2197,10 @@ export const createAgentRuntimeSessionSchema = z.object({
2188
2197
  provider: agentRuntimeProviderSchema,
2189
2198
  agentLabel: nonEmptyTrimmedString,
2190
2199
  agentType: trimmedString.default("assistant"),
2200
+ agentIdentityKey: trimmedString.optional(),
2201
+ machineKey: trimmedString.optional(),
2202
+ personaKey: trimmedString.optional(),
2203
+ linkedUserIds: uniqueStringArraySchema.default([]),
2191
2204
  actorLabel: nonEmptyTrimmedString,
2192
2205
  sessionKey: nonEmptyTrimmedString,
2193
2206
  sessionLabel: trimmedString.default(""),
@@ -2,7 +2,7 @@
2
2
  "id": "forge-openclaw-plugin",
3
3
  "name": "Forge",
4
4
  "description": "Curated OpenClaw adapter for the Forge collaboration API, UI entrypoint, and localhost auto-start runtime.",
5
- "version": "0.2.48",
5
+ "version": "0.2.49",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-openclaw-plugin",
3
- "version": "0.2.48",
3
+ "version": "0.2.49",
4
4
  "description": "Curated OpenClaw adapter for the Forge collaboration API, UI entrypoint, and localhost auto-start runtime.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -95,10 +95,13 @@
95
95
  "zustand": "^5.0.5"
96
96
  },
97
97
  "overrides": {
98
+ "@aws-sdk/xml-builder": "^3.972.19",
98
99
  "basic-ftp": "^5.3.0",
99
100
  "axios": "^1.15.0",
101
+ "fast-xml-parser": "^5.7.1",
100
102
  "follow-redirects": "^1.16.0",
101
- "hono": "4.12.14"
103
+ "hono": "4.12.14",
104
+ "uuid": "^14.0.0"
102
105
  },
103
106
  "scripts": {
104
107
  "build": "node ./scripts/build.mjs"