@axiom-lattice/gateway 2.1.111 → 2.1.113

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 (32) hide show
  1. package/.turbo/turbo-build.log +13 -13
  2. package/CHANGELOG.md +20 -0
  3. package/dist/index.js +2118 -319
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +1948 -145
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +5 -6
  8. package/src/controllers/__tests__/workflow-tracking.test.ts +113 -0
  9. package/src/controllers/connections.ts +7 -0
  10. package/src/controllers/eval.ts +44 -4
  11. package/src/controllers/export-import.ts +166 -0
  12. package/src/controllers/run.ts +30 -4
  13. package/src/controllers/workflow-tracking.ts +105 -11
  14. package/src/export_registrations/a2a-api-key.registration.ts +89 -0
  15. package/src/export_registrations/agent.registration.ts +80 -0
  16. package/src/export_registrations/binding.registration.ts +106 -0
  17. package/src/export_registrations/channel-installation.registration.ts +88 -0
  18. package/src/export_registrations/collection.registration.ts +85 -0
  19. package/src/export_registrations/connection.registration.ts +97 -0
  20. package/src/export_registrations/database-config.registration.ts +94 -0
  21. package/src/export_registrations/eval.registration.ts +334 -0
  22. package/src/export_registrations/index.ts +31 -0
  23. package/src/export_registrations/mcp-config.registration.ts +97 -0
  24. package/src/export_registrations/menu.registration.ts +91 -0
  25. package/src/export_registrations/metrics-config.registration.ts +94 -0
  26. package/src/export_registrations/skill.registration.ts +88 -0
  27. package/src/export_registrations/task.registration.ts +99 -0
  28. package/src/index.ts +18 -8
  29. package/src/routes/index.ts +42 -1
  30. package/src/services/ExportImportService.ts +296 -0
  31. package/src/services/__tests__/ExportImportService.test.ts +130 -0
  32. package/src/services/eval-runner.ts +63 -55
@@ -0,0 +1,88 @@
1
+ import type { ExportableEntityDefinition, ExportableEntity, ImportPreviewResult } from '@axiom-lattice/core';
2
+ import { getStoreLattice } from '@axiom-lattice/core';
3
+ import type { SkillStore } from '@axiom-lattice/protocols';
4
+
5
+ function getStore(): SkillStore {
6
+ return getStoreLattice('default', 'skill').store as unknown as SkillStore;
7
+ }
8
+
9
+ export const skillRegistration: ExportableEntityDefinition = {
10
+ entityType: 'skill',
11
+ label: 'Skill(技能)',
12
+ category: 'core',
13
+ dependsOn: [],
14
+ cascadeParents: [],
15
+
16
+ async listForExport(tenantId: string): Promise<ExportableEntity[]> {
17
+ const store = getStore();
18
+ const skills = await store.getAllSkills(tenantId);
19
+ return skills.map((s) => ({
20
+ _exportId: '',
21
+ data: {
22
+ id: s.id,
23
+ name: s.name,
24
+ description: s.description,
25
+ license: s.license,
26
+ compatibility: s.compatibility,
27
+ metadata: s.metadata,
28
+ content: s.content,
29
+ subSkills: s.subSkills,
30
+ },
31
+ }));
32
+ },
33
+
34
+ async previewImport(
35
+ tenantId: string,
36
+ entities: ExportableEntity[],
37
+ ): Promise<ImportPreviewResult> {
38
+ const store = getStore();
39
+ const existing = await store.getAllSkills(tenantId);
40
+ const existingIds = new Set(existing.map((s) => s.id));
41
+
42
+ const conflicts: ImportPreviewResult['conflicts'] = [];
43
+ const insertions: ImportPreviewResult['insertions'] = [];
44
+
45
+ for (const entity of entities) {
46
+ const id = entity.data.id as string;
47
+ if (existingIds.has(id)) {
48
+ conflicts.push({
49
+ _exportId: entity._exportId,
50
+ entityType: 'skill',
51
+ conflictType: 'id_exists',
52
+ existingId: id,
53
+ existingName: (entity.data.name as string) || id,
54
+ });
55
+ } else {
56
+ insertions.push({
57
+ _exportId: entity._exportId,
58
+ entityType: 'skill',
59
+ name: (entity.data.name as string) || id,
60
+ });
61
+ }
62
+ }
63
+
64
+ return { conflicts, insertions, errors: [] };
65
+ },
66
+
67
+ async applyImport(tenantId, entity, resolution) {
68
+ const store = getStore();
69
+ const data = entity.data as Record<string, unknown>;
70
+ const id = resolution.action === 'rename' && resolution.newId ? resolution.newId : data.id as string;
71
+
72
+ if (resolution.action === 'overwrite') {
73
+ try { await store.deleteSkill(tenantId, id); } catch { /* may not exist */ }
74
+ }
75
+
76
+ await store.createSkill(tenantId, id, {
77
+ name: data.name as string,
78
+ description: data.description as string,
79
+ license: data.license as string | undefined,
80
+ compatibility: data.compatibility as string | undefined,
81
+ metadata: data.metadata as Record<string, string> | undefined,
82
+ content: data.content as string | undefined,
83
+ subSkills: data.subSkills as string[] | undefined,
84
+ });
85
+
86
+ return { newId: id };
87
+ },
88
+ };
@@ -0,0 +1,99 @@
1
+ import type { ExportableEntityDefinition, ExportableEntity, ImportPreviewResult } from '@axiom-lattice/core';
2
+ import { getStoreLattice } from '@axiom-lattice/core';
3
+ import type { TaskStore } from '@axiom-lattice/protocols';
4
+
5
+ function getStore(): TaskStore {
6
+ return getStoreLattice('default', 'task').store as unknown as TaskStore;
7
+ }
8
+
9
+ export const taskRegistration: ExportableEntityDefinition = {
10
+ entityType: 'task',
11
+ label: 'Task(任务)',
12
+ category: 'core',
13
+ dependsOn: ['agent'],
14
+ cascadeParents: [],
15
+ referenceFields: { ownerId: 'agent', parentId: 'task' },
16
+
17
+ async listForExport(tenantId: string): Promise<ExportableEntity[]> {
18
+ const store = getStore();
19
+ const tasks = await store.list({ tenantId, limit: 10000, offset: 0 });
20
+ return tasks.map((t) => ({
21
+ _exportId: '',
22
+ data: {
23
+ id: t.id,
24
+ title: t.title,
25
+ description: t.description,
26
+ status: t.status,
27
+ priority: t.priority,
28
+ dueDate: t.dueDate,
29
+ ownerType: t.ownerType,
30
+ ownerId: t.ownerId,
31
+ metadata: t.metadata,
32
+ parentId: t.parentId,
33
+ sourceId: t.sourceId,
34
+ context: t.context,
35
+ },
36
+ }));
37
+ },
38
+
39
+ async previewImport(
40
+ tenantId: string,
41
+ entities: ExportableEntity[],
42
+ ): Promise<ImportPreviewResult> {
43
+ const store = getStore();
44
+ const existing = await store.list({ tenantId, limit: 10000, offset: 0 });
45
+ const existingIds = new Set(existing.map((t) => t.id));
46
+
47
+ const conflicts: ImportPreviewResult['conflicts'] = [];
48
+ const insertions: ImportPreviewResult['insertions'] = [];
49
+
50
+ for (const entity of entities) {
51
+ const d = entity.data as Record<string, unknown>;
52
+ const id = d.id as string;
53
+ if (existingIds.has(id)) {
54
+ conflicts.push({
55
+ _exportId: entity._exportId,
56
+ entityType: 'task',
57
+ conflictType: 'id_exists',
58
+ existingId: id,
59
+ existingName: (d.title as string) || id,
60
+ });
61
+ } else {
62
+ insertions.push({
63
+ _exportId: entity._exportId,
64
+ entityType: 'task',
65
+ name: (d.title as string) || id,
66
+ });
67
+ }
68
+ }
69
+
70
+ return { conflicts, insertions, errors: [] };
71
+ },
72
+
73
+ async applyImport(tenantId, entity, resolution) {
74
+ const store = getStore();
75
+ const data = entity.data as Record<string, unknown>;
76
+ const id = resolution.action === 'rename' && resolution.newId ? resolution.newId : data.id as string;
77
+
78
+ if (resolution.action === 'overwrite') {
79
+ try { await store.delete(tenantId, id); } catch { /* may not exist */ }
80
+ }
81
+
82
+ await store.create({
83
+ tenantId,
84
+ ownerType: (data.ownerType as 'user' | 'agent') || 'user',
85
+ ownerId: data.ownerId as string || '',
86
+ title: data.title as string,
87
+ description: data.description as string | undefined,
88
+ status: data.status as 'pending' | 'in_progress' | 'completed' | 'cancelled' | undefined,
89
+ priority: data.priority as 'low' | 'medium' | 'high' | undefined,
90
+ dueDate: data.dueDate as string | undefined,
91
+ metadata: data.metadata as Record<string, unknown> | undefined,
92
+ parentId: data.parentId as string | undefined,
93
+ sourceId: data.sourceId as string | undefined,
94
+ context: data.context as Record<string, unknown> | undefined,
95
+ });
96
+
97
+ return { newId: id };
98
+ },
99
+ };
package/src/index.ts CHANGED
@@ -8,14 +8,16 @@ import path from "path";
8
8
  import { fileURLToPath } from "url";
9
9
  import { registerLatticeRoutes } from "./routes";
10
10
  import { registerResourceRoutes } from "./routes/resource-routes";
11
+ import { registerAllBuiltinEntities } from "./export_registrations";
11
12
  import type { ChannelInstallationStore, BindingRegistry } from "@axiom-lattice/protocols";
12
13
  import { MessageRouter } from "./router/MessageRouter";
13
14
  import { ChannelAdapterRegistry } from "./channels/registry";
14
15
  import { createDeduplicationMiddleware, createRateLimitMiddleware, createAuditLoggerMiddleware } from "./router/middlewares";
15
- import { setBindingRegistry, setMenuRegistry } from "@axiom-lattice/core";
16
+ import { setBindingRegistry, setMenuRegistry, setEvalRunService } from "@axiom-lattice/core";
16
17
  import { larkChannelAdapter } from "./channels/lark/LarkChannelAdapter";
17
18
  import { extractUserFromAuthHeader } from "./controllers/auth";
18
19
  import { setChannelControllerDeps } from "./controllers/channel-installations";
20
+ import { evalRunner } from "./services/eval-runner";
19
21
  import { configureSwagger } from "./swagger";
20
22
  import {
21
23
  setQueueServiceType,
@@ -387,6 +389,8 @@ const start = async (config?: LatticeGatewayConfig) => {
387
389
  });
388
390
  }
389
391
 
392
+ setEvalRunService(evalRunner);
393
+
390
394
  // Initialize MenuRegistry for custom menu items
391
395
  try {
392
396
  const menuStore = getStoreLattice("default", "menu").store;
@@ -396,6 +400,9 @@ const start = async (config?: LatticeGatewayConfig) => {
396
400
  // Menu store optional — custom menus won't work without it
397
401
  }
398
402
 
403
+ // Register export/import entity types before routes
404
+ registerAllBuiltinEntities();
405
+
399
406
  // Register all routes (channel routes only active if channelDeps is set)
400
407
  // Wire controller deps so enable/disable/delete can manage live connections
401
408
  if (channelDeps?.router) {
@@ -466,13 +473,16 @@ const start = async (config?: LatticeGatewayConfig) => {
466
473
  }
467
474
 
468
475
  // Restore agent instances with pending messages (fire-and-forget to avoid blocking startup)
469
- agentInstanceManager.restore()
470
- .then((stats) => {
471
- logger.info(`Agent recovery complete: ${stats.restored} threads restored, ${stats.errors} errors`);
472
- })
473
- .catch((error) => {
474
- logger.error("Agent recovery failed", { error });
475
- });
476
+ // Temporarily disabled: do not auto-consume pending messages on startup.
477
+ if (process.env.AXIOM_RESTORE_PENDING_ON_STARTUP === "true") {
478
+ agentInstanceManager.restore()
479
+ .then((stats) => {
480
+ logger.info(`Agent recovery complete: ${stats.restored} threads restored, ${stats.errors} errors`);
481
+ })
482
+ .catch((error) => {
483
+ logger.error("Agent recovery failed", { error });
484
+ });
485
+ }
476
486
 
477
487
  // Restore MCP server connections that were connected before shutdown
478
488
  restoreMcpConnections().catch((error) => {
@@ -19,6 +19,7 @@ import * as dataQueryController from "../controllers/data-query";
19
19
  import * as workflowTrackingController from "../controllers/workflow-tracking";
20
20
  import * as paController from "../controllers/personal-assistant";
21
21
  import * as tasksController from "../controllers/tasks";
22
+ import * as exportImportController from "../controllers/export-import";
22
23
  import { middlewareTypesRoutes } from "../controllers/middleware-types";
23
24
  import {
24
25
  createRunSchema,
@@ -510,7 +511,9 @@ export const registerLatticeRoutes = (app: FastifyInstance, channelDeps?: { rout
510
511
  workflowTrackingController.getAllWorkflowRuns
511
512
  );
512
513
 
513
- app.get(
514
+ app.get<{
515
+ Querystring: { page?: string; pageSize?: string };
516
+ }>(
514
517
  "/api/workflows/inbox",
515
518
  workflowTrackingController.getInboxItems
516
519
  );
@@ -546,6 +549,11 @@ export const registerLatticeRoutes = (app: FastifyInstance, channelDeps?: { rout
546
549
  Params: { runId: string };
547
550
  }>("/api/workflows/runs/:runId/tasks", workflowTrackingController.getRunTasks);
548
551
 
552
+ // Abort a running workflow — stops graph + all sub-agents
553
+ app.post<{
554
+ Params: { runId: string };
555
+ }>("/api/workflows/runs/:runId/abort", workflowTrackingController.abortWorkflowRun);
556
+
549
557
  // 简化的 Inbox 任务回复接口(后台 streaming 执行,立即返回)
550
558
  app.post<{
551
559
  Body: { runId: string; answers: Array<{ questionIndex: number; selectedOptions: string[] }> };
@@ -568,4 +576,37 @@ export const registerLatticeRoutes = (app: FastifyInstance, channelDeps?: { rout
568
576
  "/api/assistants/:assistant_id/threads/:thread_id/pending-messages/:message_id",
569
577
  removePendingMessageHandler
570
578
  );
579
+
580
+ // Export/Import endpoints
581
+ app.get("/api/tenants/exportable-types", exportImportController.getExportableTypes);
582
+
583
+ app.post<{ Params: { tenantId: string }; Body: { entityTypes: string[]; entityIds?: Record<string, string[]> } }>(
584
+ "/api/tenants/:tenantId/export",
585
+ exportImportController.exportConfig,
586
+ );
587
+
588
+ app.post<{ Params: { tenantId: string }; Body: { entityTypes: string[]; entityIds?: Record<string, string[]> } }>(
589
+ "/api/tenants/:tenantId/export/confirm",
590
+ exportImportController.exportConfigConfirm,
591
+ );
592
+
593
+ app.post<{ Params: { tenantId: string }; Body: { entityTypes: string[] } }>(
594
+ "/api/tenants/:tenantId/export/entities",
595
+ exportImportController.previewEntities,
596
+ );
597
+
598
+ app.get<{ Params: { tenantId: string; jobId: string } }>(
599
+ "/api/tenants/:tenantId/export/:jobId/download",
600
+ exportImportController.downloadExport,
601
+ );
602
+
603
+ app.post<{ Params: { tenantId: string }; Body: { bundle: any } }>(
604
+ "/api/tenants/:tenantId/import/preview",
605
+ exportImportController.importPreview,
606
+ );
607
+
608
+ app.post<{ Params: { tenantId: string }; Body: { bundle: any; resolutions: Record<string, any> } }>(
609
+ "/api/tenants/:tenantId/import/apply",
610
+ exportImportController.importApply,
611
+ );
571
612
  };
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Core orchestration service for tenant configuration export and import.
3
+ *
4
+ * @module @axiom-lattice/gateway/services/ExportImportService
5
+ */
6
+
7
+ import { ExportableEntityRegistry, DependencyResolver, IdRemapper } from '@axiom-lattice/core';
8
+ import type {
9
+ ExportBundle,
10
+ ExportableEntity,
11
+ ImportPreviewResult,
12
+ Resolution,
13
+ ImportApplyResult,
14
+ ImportEntityResult,
15
+ } from '@axiom-lattice/core';
16
+ import { randomUUID } from 'crypto';
17
+
18
+ interface JobEntry {
19
+ bundle: ExportBundle;
20
+ tenantId: string;
21
+ createdAt: number;
22
+ }
23
+
24
+ /** Ephemeral in-memory job store (jobs expire after TTL). */
25
+ const exportJobs = new Map<string, JobEntry>();
26
+
27
+ /** TTL: 1 hour */
28
+ const JOB_TTL_MS = 60 * 60 * 1000;
29
+
30
+ function cleanExpiredJobs(): void {
31
+ const now = Date.now();
32
+ for (const [id, entry] of exportJobs) {
33
+ if (now - entry.createdAt > JOB_TTL_MS) {
34
+ exportJobs.delete(id);
35
+ }
36
+ }
37
+ }
38
+
39
+ export class ExportImportService {
40
+ private registry = ExportableEntityRegistry.getInstance();
41
+
42
+ /**
43
+ * Preview an export: check which types would be auto-included, which dependencies are missing.
44
+ */
45
+ async exportPreview(
46
+ tenantId: string,
47
+ selectedTypes: string[],
48
+ ): Promise<{ missingDependencies: string[]; cascadeAdditions: string[] }> {
49
+ const allDefs = this.registry.getAll();
50
+
51
+ const expanded = DependencyResolver.expandCascade(allDefs, selectedTypes);
52
+ const cascadeAdditions = expanded.filter((t) => !selectedTypes.includes(t));
53
+
54
+ const deps = DependencyResolver.computeDependencies(allDefs, expanded);
55
+ const registeredTypes = new Set(allDefs.map((d) => d.entityType));
56
+ const missingDependencies = deps.missing.filter((t) => registeredTypes.has(t));
57
+
58
+ return { missingDependencies, cascadeAdditions };
59
+ }
60
+
61
+ /**
62
+ * Generate a full ExportBundle for the given tenant and entity types.
63
+ * Stores the bundle in-memory and returns a jobId for download.
64
+ */
65
+ async export(
66
+ tenantId: string,
67
+ entityTypes: string[],
68
+ entityIds?: Record<string, string[]>,
69
+ ): Promise<{ jobId: string; bundle: ExportBundle }> {
70
+ const allDefs = this.registry.getAll();
71
+ const expanded = DependencyResolver.expandCascade(allDefs, entityTypes);
72
+ const finalTypes = [...new Set([...expanded, ...entityTypes])];
73
+
74
+ const dependencyOrder = DependencyResolver.resolveOrder(
75
+ finalTypes.map((t) => this.registry.get(t)),
76
+ );
77
+
78
+ const entities: Record<string, ExportableEntity[]> = {};
79
+ let exportCounter = 0;
80
+
81
+ for (const type of dependencyOrder) {
82
+ const def = this.registry.get(type);
83
+ let raw = await def.listForExport(tenantId);
84
+
85
+ if (entityIds && entityIds[type] && entityIds[type].length > 0) {
86
+ const idSet = new Set(entityIds[type]);
87
+ raw = raw.filter((e) => idSet.has(e.data.id as string));
88
+ }
89
+
90
+ entities[type] = raw.map((e) => ({
91
+ ...e,
92
+ _exportId: `exp-${type}-${String(++exportCounter).padStart(3, '0')}`,
93
+ data: this.sanitizeEntityData(e.data),
94
+ }));
95
+ }
96
+
97
+ // Build reverse map: raw entity ID → _exportId
98
+ const rawIdToExportId = new Map<string, string>();
99
+ for (const [, entityList] of Object.entries(entities)) {
100
+ for (const entity of entityList) {
101
+ const rawId = entity.data.id as string;
102
+ if (rawId) {
103
+ rawIdToExportId.set(rawId, entity._exportId);
104
+ }
105
+ }
106
+ }
107
+
108
+ // Rewrite reference fields to @type/exportId format
109
+ for (const [type, entityList] of Object.entries(entities)) {
110
+ const def = this.registry.get(type);
111
+ const refFields = def.referenceFields || {};
112
+ for (const entity of entityList) {
113
+ for (const [fieldName, refEntityType] of Object.entries(refFields)) {
114
+ const rawValue = entity.data[fieldName];
115
+ if (typeof rawValue === 'string' && rawValue) {
116
+ const exportId = rawIdToExportId.get(rawValue);
117
+ if (exportId) {
118
+ entity.data[fieldName] = `@${refEntityType}/${exportId}`;
119
+ }
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ const bundle: ExportBundle = {
126
+ version: 1,
127
+ exportedAt: new Date().toISOString(),
128
+ sourceTenantId: tenantId,
129
+ entities,
130
+ dependencyOrder,
131
+ _warning: 'This file contains plaintext secrets (passwords, API keys, tokens). Store it securely and delete after import.',
132
+ };
133
+
134
+ const jobId = randomUUID();
135
+ exportJobs.set(jobId, { bundle, tenantId, createdAt: Date.now() });
136
+
137
+ return { jobId, bundle };
138
+ }
139
+
140
+ /**
141
+ * Retrieve a previously generated export bundle by jobId with tenant validation.
142
+ */
143
+ getExportJob(jobId: string, tenantId: string): ExportBundle | undefined {
144
+ cleanExpiredJobs();
145
+ const entry = exportJobs.get(jobId);
146
+ if (!entry || entry.tenantId !== tenantId) return undefined;
147
+ return entry.bundle;
148
+ }
149
+
150
+ /**
151
+ * Preview import: validate bundle and detect conflicts.
152
+ */
153
+ async previewImport(
154
+ tenantId: string,
155
+ bundle: ExportBundle,
156
+ ): Promise<ImportPreviewResult> {
157
+ this.validateBundle(bundle);
158
+
159
+ const allConflicts: ImportPreviewResult['conflicts'] = [];
160
+ const allInsertions: ImportPreviewResult['insertions'] = [];
161
+ const allErrors: ImportPreviewResult['errors'] = [];
162
+
163
+ for (const [entityType, entities] of Object.entries(bundle.entities)) {
164
+ if (entities.length === 0) continue;
165
+
166
+ try {
167
+ const def = this.registry.get(entityType);
168
+ const result = await def.previewImport(tenantId, entities);
169
+
170
+ allConflicts.push(...result.conflicts);
171
+ allInsertions.push(...result.insertions);
172
+ allErrors.push(...result.errors);
173
+ } catch (err) {
174
+ for (const entity of entities) {
175
+ allErrors.push({
176
+ _exportId: entity._exportId,
177
+ entityType,
178
+ error: `Preview failed: ${(err as Error).message}`,
179
+ });
180
+ }
181
+ }
182
+ }
183
+
184
+ return { conflicts: allConflicts, insertions: allInsertions, errors: allErrors };
185
+ }
186
+
187
+ /**
188
+ * Apply import: create entities in dependency order with ID remapping.
189
+ */
190
+ async applyImport(
191
+ tenantId: string,
192
+ bundle: ExportBundle,
193
+ resolutions: Record<string, Resolution>,
194
+ ): Promise<ImportApplyResult> {
195
+ this.validateBundle(bundle);
196
+
197
+ const results: ImportEntityResult[] = [];
198
+ const idMap: Record<string, string> = {};
199
+ // Track skill old id → new id for agent graphDefinition implicit refs
200
+ const skillIdRemap: Record<string, string> = {};
201
+
202
+ for (const entityType of bundle.dependencyOrder) {
203
+ const entities = bundle.entities[entityType];
204
+ if (!entities || entities.length === 0) continue;
205
+
206
+ const def = this.registry.get(entityType);
207
+ const remapper = new IdRemapper(idMap);
208
+
209
+ for (const entity of entities) {
210
+ const resolution = resolutions[entity._exportId];
211
+
212
+ if (resolution?.action === 'skip') {
213
+ results.push({ _exportId: entity._exportId, entityType, status: 'skipped' });
214
+ continue;
215
+ }
216
+
217
+ try {
218
+ const remappedData = remapper.remapReferences(entity.data) as Record<string, unknown>;
219
+
220
+ // Remap implicit skill references in agent graphDefinition.skillIds
221
+ if (entityType === 'agent' && Object.keys(skillIdRemap).length > 0) {
222
+ const skillRemapper = new IdRemapper({});
223
+ remappedData.graphDefinition = skillRemapper.remapSkillIds(
224
+ (remappedData.graphDefinition as Record<string, unknown>) || {},
225
+ skillIdRemap,
226
+ );
227
+ }
228
+
229
+ if (resolution?.action === 'rename' && resolution.newId) {
230
+ remappedData.id = resolution.newId;
231
+ }
232
+
233
+ const entityWithRemappedData = { ...entity, data: remappedData };
234
+ const { newId } = await def.applyImport(
235
+ tenantId,
236
+ entityWithRemappedData,
237
+ resolution ?? { _exportId: entity._exportId, action: 'overwrite' },
238
+ );
239
+
240
+ if (newId) {
241
+ idMap[entity._exportId] = newId;
242
+ // Track skill ID renames for agent graphDefinition remapping
243
+ if (entityType === 'skill' && newId) {
244
+ const oldSkillId = remappedData.id as string;
245
+ if (oldSkillId !== newId) {
246
+ skillIdRemap[oldSkillId] = newId;
247
+ }
248
+ }
249
+ results.push({
250
+ _exportId: entity._exportId,
251
+ entityType,
252
+ status: resolution?.action === 'overwrite' ? 'updated' : 'created',
253
+ newId,
254
+ });
255
+ }
256
+ } catch (err) {
257
+ results.push({
258
+ _exportId: entity._exportId,
259
+ entityType,
260
+ status: 'failed',
261
+ error: (err as Error).message,
262
+ });
263
+ }
264
+ }
265
+ }
266
+
267
+ return { results, idMap };
268
+ }
269
+
270
+ /**
271
+ * List all registered exportable types (for frontend).
272
+ */
273
+ listExportableTypes() {
274
+ return this.registry.listTypes();
275
+ }
276
+
277
+ private validateBundle(bundle: ExportBundle): void {
278
+ if (bundle.version !== 1) {
279
+ throw new Error(`Unsupported bundle version: ${bundle.version}. Expected: 1`);
280
+ }
281
+ if (!bundle.entities || typeof bundle.entities !== 'object') {
282
+ throw new Error('Invalid bundle: missing entities');
283
+ }
284
+ if (!Array.isArray(bundle.dependencyOrder)) {
285
+ throw new Error('Invalid bundle: missing dependencyOrder');
286
+ }
287
+ }
288
+
289
+ private sanitizeEntityData(data: Record<string, unknown>): Record<string, unknown> {
290
+ const sanitized = { ...data };
291
+ delete sanitized.tenantId;
292
+ delete sanitized.createdAt;
293
+ delete sanitized.updatedAt;
294
+ return sanitized;
295
+ }
296
+ }