@anteros/core 0.0.40 → 0.0.42

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 (130) hide show
  1. package/database/audit.ts +143 -0
  2. package/database/collection.ts +6 -4
  3. package/database/deleteLog.ts +98 -0
  4. package/database/file.ts +7 -6
  5. package/database/mongodbadapter.ts +420 -185
  6. package/database/replication.ts +858 -0
  7. package/database/schema.ts +140 -125
  8. package/database/tenant.ts +4 -0
  9. package/database/ttl.ts +68 -0
  10. package/database/vars.ts +405 -0
  11. package/database/workflow.ts +466 -240
  12. package/index.ts +16 -0
  13. package/lib/agent.ts +1327 -0
  14. package/lib/agentMemory.ts +586 -0
  15. package/lib/agents.ts +157 -0
  16. package/lib/ai.ts +0 -0
  17. package/lib/api.ts +246 -0
  18. package/lib/attachment.ts +351 -0
  19. package/lib/audit.ts +271 -0
  20. package/lib/auditResult.ts +133 -0
  21. package/lib/define.ts +62 -1
  22. package/lib/endpoints.ts +80 -0
  23. package/lib/files.ts +66 -12
  24. package/lib/jsonSchema.ts +183 -0
  25. package/lib/lifecycle.ts +147 -0
  26. package/lib/load.ts +17 -0
  27. package/lib/mcp.ts +3 -2
  28. package/lib/messages.ts +299 -0
  29. package/lib/providers.ts +867 -0
  30. package/lib/redact.ts +117 -0
  31. package/lib/routes.ts +14 -1
  32. package/lib/scriptWorker.ts +4 -1
  33. package/lib/scripts.ts +6 -1
  34. package/lib/sockets.ts +3 -1
  35. package/lib/workflow.ts +209 -37
  36. package/lib/zip.ts +109 -0
  37. package/package.json +1 -1
  38. package/server/access.ts +96 -0
  39. package/server/agents.ts +451 -0
  40. package/server/api.ts +121 -95
  41. package/server/boot.ts +196 -53
  42. package/server/flags.ts +136 -0
  43. package/server/hono.ts +9 -4
  44. package/server/mcp.ts +17 -63
  45. package/server/routes.ts +19 -24
  46. package/tests/agent-memory.test.ts +617 -0
  47. package/tests/agent-working-memory.test.ts +402 -0
  48. package/tests/agent.test.ts +1239 -0
  49. package/tests/agents-api.test.ts +593 -0
  50. package/tests/api.test.ts +51 -0
  51. package/tests/attachment.test.ts +241 -0
  52. package/tests/audit-file.test.ts +296 -0
  53. package/tests/audit-operations.test.ts +184 -0
  54. package/tests/audit-redact.test.ts +259 -0
  55. package/tests/audit-results.test.ts +188 -0
  56. package/tests/audit-retention-config.test.ts +76 -0
  57. package/tests/audit-run-action.test.ts +120 -0
  58. package/tests/audit.test.ts +320 -0
  59. package/tests/db-cache.test.ts +7 -7
  60. package/tests/endpoints.test.ts +80 -0
  61. package/tests/file-collection-replication.test.ts +98 -0
  62. package/tests/fixtures/agent-tenant/agents/support.agent.ts +9 -0
  63. package/tests/fixtures/agent-tenant/agents/weather.agent.ts +21 -0
  64. package/tests/fixtures/agents-api/agents/assistant.agent.ts +46 -0
  65. package/tests/fixtures/agents-api/agents/plain.agent.ts +23 -0
  66. package/tests/fixtures/agents-api/agents/private.agent.ts +15 -0
  67. package/tests/fixtures/agents-api/collections/notes.model.ts +26 -0
  68. package/tests/fixtures/agents-api/routes/probe.route.ts +21 -0
  69. package/tests/fixtures/agents-api/services/probe.service.ts +19 -0
  70. package/tests/fixtures/attachment/data.csv +2 -0
  71. package/tests/fixtures/fake-provider.ts +128 -0
  72. package/tests/fixtures/lc-disabled/lifecycle.ts +11 -0
  73. package/tests/fixtures/lc-hang/lifecycle.ts +13 -0
  74. package/tests/fixtures/lc-normal/lifecycle.ts +23 -0
  75. package/tests/fixtures/lc-throw/lifecycle.ts +6 -0
  76. package/tests/fixtures/repl-only/app.ts +26 -0
  77. package/tests/fixtures/repl-only/tenant/collections/orders.model.ts +14 -0
  78. package/tests/fixtures/repl-only/tenant/lifecycle.ts +14 -0
  79. package/tests/fixtures/repl-only/tenant/scripts/probe.run.ts +8 -0
  80. package/tests/fixtures/replication-meta/agents/chat.agent.ts +16 -0
  81. package/tests/fixtures/replication-meta/collections/counters.model.ts +13 -0
  82. package/tests/fixtures/replication-meta/collections/orders.model.ts +9 -0
  83. package/tests/fixtures/replication-meta/workflows/run.workflow.ts +8 -0
  84. package/tests/fixtures/vars-tenant/vars/config.var.ts +23 -0
  85. package/tests/fixtures/vars-tenant/vars/private.var.ts +9 -0
  86. package/tests/fixtures/workflow-engine/workflows/failing.workflow.ts +34 -0
  87. package/tests/fixtures/workflow-engine/workflows/flaky.workflow.ts +23 -0
  88. package/tests/fixtures/workflow-engine/workflows/hang.workflow.ts +17 -0
  89. package/tests/fixtures/workflow-engine/workflows/simple.workflow.ts +19 -0
  90. package/tests/fixtures/workflow-engine/workflows/slow.workflow.ts +23 -0
  91. package/tests/fixtures/workflow-engine/workflows/versioned.workflow.ts +19 -0
  92. package/tests/fixtures/workflow-tenant/workflows/broken.workflow.ts +3 -0
  93. package/tests/fixtures/workflow-tenant/workflows/disabled.workflow.ts +8 -0
  94. package/tests/fixtures/workflow-tenant/workflows/invoice.workflow.ts +21 -0
  95. package/tests/fixtures/workflow-tenant/workflows/no-id.workflow.ts +7 -0
  96. package/tests/fixtures/workflow-tenant/workflows/order-duplicate.workflow.ts +13 -0
  97. package/tests/fixtures/workflow-tenant/workflows/order.workflow.ts +17 -0
  98. package/tests/hono.test.ts +6 -3
  99. package/tests/lifecycle.test.ts +96 -0
  100. package/tests/locks.test.ts +120 -0
  101. package/tests/mcp.test.ts +11 -11
  102. package/tests/messages.test.ts +258 -0
  103. package/tests/replication-control.test.ts +128 -0
  104. package/tests/replication-initial-sync.test.ts +86 -0
  105. package/tests/replication-meta.test.ts +243 -0
  106. package/tests/replication-only.test.ts +358 -0
  107. package/tests/replication.test.ts +188 -0
  108. package/tests/upload.test.ts +141 -8
  109. package/tests/vars-api.test.ts +123 -0
  110. package/tests/vars-replication.test.ts +96 -0
  111. package/tests/vars.test.ts +216 -0
  112. package/tests/workflow-engine.test.ts +340 -0
  113. package/tests/workflow-retention.test.ts +103 -0
  114. package/tests/workflow.test.ts +186 -0
  115. package/types/agent.d.ts +837 -0
  116. package/types/collection.d.ts +3 -0
  117. package/types/config.d.ts +80 -0
  118. package/types/file.d.ts +11 -8
  119. package/types/hook.d.ts +6 -0
  120. package/types/lifecycle.d.ts +77 -0
  121. package/types/mcp.d.ts +11 -1
  122. package/types/replication.d.ts +228 -0
  123. package/types/route.d.ts +7 -1
  124. package/types/scripts.d.ts +6 -0
  125. package/types/service.d.ts +7 -1
  126. package/types/tenant.d.ts +36 -0
  127. package/types/vars.d.ts +147 -0
  128. package/types/websocket.d.ts +6 -0
  129. package/types/workflow.d.ts +50 -3
  130. package/utils/func.ts +67 -52
@@ -0,0 +1,143 @@
1
+ import type { Db } from "mongodb";
2
+ import { cfg } from "../server/config";
3
+ import type { Tenant } from "../types/tenant";
4
+ import { syncTtlRetention, type RetentionResult } from "./ttl";
5
+
6
+ /**
7
+ * Collection holding the audit trail — one document per database operation
8
+ * (see `types/activity.d.ts` / `rest.audit`).
9
+ */
10
+ const AUDIT_COLLECTION = '_audit_'
11
+
12
+ /** Previous name of the audit collection — renamed once at boot by `migrateAuditCollection`. */
13
+ const LEGACY_AUDIT_COLLECTION = '_activities_'
14
+
15
+ /**
16
+ * TTL index used for retention. Created only when a `tenant.audit.retention`
17
+ * is configured — audit entries are kept forever by default.
18
+ */
19
+ const AUDIT_TTL_INDEX = '_audit_ttl_'
20
+
21
+ /**
22
+ * Query indexes, always ensured in the background (same convention as `_vars_`),
23
+ * whatever the tenant config — an audit collection is queried by date, by
24
+ * collection, by status and by trace, and grows fast enough that a COLLSCAN
25
+ * is never acceptable.
26
+ */
27
+ const AUDIT_INDEXES: { name: string; key: Record<string, 1 | -1> }[] = [
28
+ /** Listing / date ranges (`getActivities` sorts on `ts` by default) */
29
+ { name: '_audit_ts_', key: { ts: -1 } },
30
+ /** `$match: { 'operation.collection': … }` + recent first */
31
+ { name: '_audit_collection_ts_', key: { 'operation.collection': 1, ts: -1 } },
32
+ /** `$match: { 'operation.status': 'error' }` + recent first */
33
+ { name: '_audit_status_ts_', key: { 'operation.status': 1, ts: -1 } },
34
+ /** Correlate every operation of a request (`trace.id`) */
35
+ { name: '_audit_trace_', key: { 'trace.id': 1 } },
36
+ ]
37
+
38
+ /**
39
+ * One-off migration: `_activities_` → `_audit_`.
40
+ *
41
+ * Idempotent and never fatal:
42
+ * - legacy collection absent → no-op (the common case after the migration)
43
+ * - target already exists → no-op, the legacy collection is left untouched
44
+ * (never merged nor dropped: losing audit entries silently is worse than a stale collection)
45
+ */
46
+ async function migrateAuditCollection(db: Db): Promise<boolean> {
47
+ try {
48
+ const legacy = await db.listCollections({ name: LEGACY_AUDIT_COLLECTION }).toArray()
49
+ if (legacy.length === 0) return false
50
+
51
+ const target = await db.listCollections({ name: AUDIT_COLLECTION }).toArray()
52
+ if (target.length > 0) return false
53
+
54
+ await db.renameCollection(LEGACY_AUDIT_COLLECTION, AUDIT_COLLECTION, { dropTarget: false })
55
+ console.log(`🔁 Audit collection renamed: ${LEGACY_AUDIT_COLLECTION} → ${AUDIT_COLLECTION}`)
56
+ return true
57
+ } catch (err: any) {
58
+ console.error(
59
+ `Audit collection migration failed (${LEGACY_AUDIT_COLLECTION} → ${AUDIT_COLLECTION})`,
60
+ err?.message,
61
+ )
62
+ return false
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Ensure the audit query indexes. `createIndex` is idempotent, so this runs on
68
+ * every boot and only fills in what is missing. Never fatal, never blocking
69
+ * (`background: true`).
70
+ *
71
+ * ⚠️ Indexes cost writes, and the audit collection is the most write-heavy one —
72
+ * four indexes are a deliberate trade-off (see the Audit documentation).
73
+ */
74
+ async function ensureAuditIndexes(db: Db): Promise<string[]> {
75
+ const ensured: string[] = []
76
+ const col: any = db.collection(AUDIT_COLLECTION)
77
+
78
+ for (const index of AUDIT_INDEXES) {
79
+ try {
80
+ await col.createIndex(index.key, { name: index.name, background: true })
81
+ ensured.push(index.name)
82
+ } catch (err: any) {
83
+ console.error(`Audit index '${index.name}' failed`, err?.message)
84
+ }
85
+ }
86
+
87
+ return ensured
88
+ }
89
+
90
+ /**
91
+ * Declarative retention (MongoDB TTL on `ts`) — shared TTL helper:
92
+ * - duration string (`'90d'`, `'24h'`) → ensure the TTL index with that expiry
93
+ * - `false` → explicitly disabled: drop the TTL index
94
+ * - omitted → untouched (entries are kept forever)
95
+ */
96
+ async function syncAuditRetention(db: Db, retention?: string | false): Promise<RetentionResult> {
97
+ return syncTtlRetention(db, {
98
+ collection: AUDIT_COLLECTION,
99
+ index: AUDIT_TTL_INDEX,
100
+ field: 'ts',
101
+ retention,
102
+ label: 'Audit',
103
+ })
104
+ }
105
+
106
+ /**
107
+ * Retention resolution — **the tenant always wins**, `server.audit.retention`
108
+ * is only the fallback:
109
+ *
110
+ * - `tenant.audit.retention` set (a duration, even `false`) → used as-is
111
+ * - otherwise → `server.audit.retention`
112
+ * - neither → `undefined` (kept forever)
113
+ */
114
+ function resolveAuditRetention(tenant: Tenant): string | false | undefined {
115
+ if (tenant.audit?.retention !== undefined) return tenant.audit.retention
116
+ return cfg.server?.audit?.retention
117
+ }
118
+
119
+ /**
120
+ * Boot setup for a tenant's audit collection, in this order:
121
+ * migration → indexes → retention.
122
+ *
123
+ * The migration comes first on purpose: creating the indexes would create an
124
+ * empty `_audit_`, and the migration would then refuse to rename the legacy
125
+ * collection (leaving its entries stranded).
126
+ */
127
+ async function setupAuditCollection(db: Db, retention?: string | false): Promise<void> {
128
+ await migrateAuditCollection(db)
129
+ await ensureAuditIndexes(db)
130
+ await syncAuditRetention(db, retention)
131
+ }
132
+
133
+ export {
134
+ AUDIT_COLLECTION,
135
+ LEGACY_AUDIT_COLLECTION,
136
+ AUDIT_TTL_INDEX,
137
+ AUDIT_INDEXES,
138
+ migrateAuditCollection,
139
+ ensureAuditIndexes,
140
+ syncAuditRetention,
141
+ resolveAuditRetention,
142
+ setupAuditCollection
143
+ }
@@ -5,6 +5,7 @@ import fs from "fs/promises"
5
5
  import type { Collection } from "../types/collection"
6
6
  import type { IndexDescription } from "mongodb"
7
7
  import type { Field } from "../types/field"
8
+ import { importDefinition } from "../lib/load"
8
9
  import { getTenant } from "./tenant"
9
10
  import { buildSchema } from "./schema"
10
11
  import type { FileCollection } from "../types/file"
@@ -38,7 +39,8 @@ async function syncCollections() {
38
39
  if (isDirectory) {
39
40
  const glob = new Glob(path.join(COLLECTIONS_PATH, '**/*.model.ts'))
40
41
  for await (let file of glob.scan('.')) {
41
- let collectionModule = await import(file)
42
+ // A broken model file is skipped, never fatal for the others
43
+ let collectionModule = await importDefinition(file, 'collection')
42
44
  if (collectionModule?.default?._isCollection_) {
43
45
  collections.push({
44
46
  ...collectionModule?.default,
@@ -49,7 +51,7 @@ async function syncCollections() {
49
51
  }
50
52
  }
51
53
 
52
- // construction des schema arktype pour les collections
54
+ // build the arktype schemas for the collections
53
55
  for (let collection of collections) {
54
56
  collection._schema_ = buildSchema(collection)
55
57
  collection._schemaPartial_ = buildSchema(collection, { partial: true })
@@ -102,7 +104,7 @@ async function initializeOnDatabase(collections: Collection[]) {
102
104
 
103
105
 
104
106
 
105
- // creation d'index sur les champs
107
+ // create index on the fields
106
108
  let specsFieldsIndexes: IndexDescription[] = []
107
109
  for (let field of collection?.fields || []) {
108
110
  field.indexOptions = field.indexOptions ?? {}
@@ -190,7 +192,7 @@ function getCollection(collectionName: string, tenantId: string): Collection | n
190
192
  return col
191
193
  }
192
194
 
193
- // Chercher aussi dans les file collections
195
+ // Also look in the file collections
194
196
  let fileCol = cfg.fileCollections?.find(fc => fc.slug == collectionName && fc._tenant_ == tenantId)
195
197
  if (fileCol) {
196
198
  return {
@@ -0,0 +1,98 @@
1
+ import type { Db } from "mongodb";
2
+ import { cfg } from "../server/config";
3
+ import type { ReplicationMetaName } from "../types/replication";
4
+
5
+ /** Collection holding the replication state (`_replication_`). */
6
+ const MARKERS = "_replication_";
7
+
8
+ /** Meta collection slug → the name `replication.exclude` knows it by. */
9
+ const META_BY_SLUG: Record<string, ReplicationMetaName> = {
10
+ [MARKERS]: "replication",
11
+ _audit_: "audit",
12
+ _workflows_: "workflows",
13
+ _locks_: "locks",
14
+ _vars_: "vars",
15
+ };
16
+
17
+ /** Replication is armed: enabled, with at least one usable destination. */
18
+ function replicationReady(tenantId: string): boolean {
19
+ const tenant: any = (cfg.tenants ?? []).find((candidate) => candidate.id === tenantId);
20
+ const replication = tenant?.replication;
21
+
22
+ if (!replication || replication.enabled === false) return false;
23
+ return (replication.destinations ?? []).some((destination: any) =>
24
+ destination?.enabled !== false && destination?.id && destination?.uri);
25
+ }
26
+
27
+ /** `replication.exclude` names this collection. */
28
+ function isExcluded(tenantId: string, name: string): boolean {
29
+ const tenant: any = (cfg.tenants ?? []).find((candidate) => candidate.id === tenantId);
30
+ return (tenant?.replication?.exclude ?? []).includes(name);
31
+ }
32
+
33
+ /**
34
+ * Whether the framework replicates a **meta** collection (`_audit_`, `_vars_`…)
35
+ * for this tenant.
36
+ */
37
+ function replicatesMetaCollection(tenantId: string, name: ReplicationMetaName): boolean {
38
+ return replicationReady(tenantId) && !isExcluded(tenantId, name);
39
+ }
40
+
41
+ /**
42
+ * The same question, by **collection slug** — used by the stores that delete on
43
+ * their own instead of going through the collection hooks (agent memory, and the
44
+ * variables / file paths mirror this).
45
+ *
46
+ * It answers exactly what `replicatedCollectionsFor()` would: a meta collection,
47
+ * an agent memory collection (`exclude: ['memory']` covers them all — the tenant
48
+ * names them), or a declared collection that opted in. A collection nobody
49
+ * replicates gets no tombstone: it would never be flushed and would only pile up
50
+ * in `_replication_`.
51
+ */
52
+ function replicatesCollection(tenantId: string, slug: string): boolean {
53
+ const asMeta = META_BY_SLUG[slug];
54
+ if (asMeta) return replicatesMetaCollection(tenantId, asMeta);
55
+ if (!replicationReady(tenantId)) return false;
56
+
57
+ const isAgentMemory = (cfg.agentMemories ?? [])
58
+ .some((memory) => memory._tenant_ === tenantId && memory.collection === slug);
59
+ if (isAgentMemory) return !isExcluded(tenantId, "memory");
60
+
61
+ const declared = [...(cfg.collections ?? []), ...(cfg.fileCollections ?? [])]
62
+ .some((collection: any) =>
63
+ collection._tenant_ === tenantId
64
+ && collection.slug === slug
65
+ && collection.replication?.enabled);
66
+
67
+ return declared && !isExcluded(tenantId, slug);
68
+ }
69
+
70
+ /**
71
+ * Upsert delete tombstones consumed by the replication engine.
72
+ *
73
+ * Some collections bypass the collection hooks when deleting (variables,
74
+ * agent memory, file-collection documents) — those paths call this directly so
75
+ * their deletions still propagate to the destinations.
76
+ */
77
+ async function writeDeleteMarkers(
78
+ db: Db,
79
+ tenantId: string,
80
+ collection: string,
81
+ docIds: string[],
82
+ ): Promise<void> {
83
+ if (!docIds.length) return;
84
+ const now = new Date();
85
+ const ops = docIds.map((docId) => ({
86
+ updateOne: {
87
+ filter: { _id: `rep.delete:${tenantId}:${collection}:${docId}` },
88
+ update: {
89
+ $set: { type: "delete", tenant: tenantId, collection, docId, deletedAt: now, updatedAt: now },
90
+ $setOnInsert: { synced: [] },
91
+ },
92
+ upsert: true,
93
+ },
94
+ }));
95
+ await db.collection(MARKERS).bulkWrite(ops as any, { ordered: false }).catch(() => { /* ignore */ });
96
+ }
97
+
98
+ export { writeDeleteMarkers, replicatesMetaCollection, replicatesCollection };
package/database/file.ts CHANGED
@@ -4,6 +4,7 @@ import path from "path"
4
4
  import fs from "fs/promises"
5
5
  import type { FileCollection } from "../types/file"
6
6
  import { getTenant } from "./tenant"
7
+ import { importDefinition } from "../lib/load"
7
8
  import type { IndexDescription } from "mongodb"
8
9
 
9
10
  async function syncFileCollections() {
@@ -18,7 +19,7 @@ async function syncFileCollections() {
18
19
  if (filesExist && (await fs.stat(FILES_PATH)).isDirectory()) {
19
20
  const globFiles = new Glob(path.join(FILES_PATH, '**/*.file.ts'))
20
21
  for await (let file of globFiles.scan('.')) {
21
- let fileModule = await import(file)
22
+ let fileModule = await importDefinition(file, 'file collection')
22
23
  if (fileModule?.default?._isFileCollection_) {
23
24
  fileCollections.push({
24
25
  ...fileModule?.default,
@@ -30,7 +31,7 @@ async function syncFileCollections() {
30
31
  // Also scan *.model.ts in files/ with _isFileCollection_
31
32
  const globModels = new Glob(path.join(FILES_PATH, '**/*.model.ts'))
32
33
  for await (let file of globModels.scan('.')) {
33
- let fileModule = await import(file)
34
+ let fileModule = await importDefinition(file, 'file collection')
34
35
  if (fileModule?.default?._isFileCollection_) {
35
36
  fileCollections.push({
36
37
  ...fileModule?.default,
@@ -46,7 +47,7 @@ async function syncFileCollections() {
46
47
  if (collectionsExist && (await fs.stat(COLLECTIONS_PATH)).isDirectory()) {
47
48
  const globFiles = new Glob(path.join(COLLECTIONS_PATH, '**/*.file.ts'))
48
49
  for await (let file of globFiles.scan('.')) {
49
- let fileModule = await import(file)
50
+ let fileModule = await importDefinition(file, 'file collection')
50
51
  if (fileModule?.default?._isFileCollection_) {
51
52
  fileCollections.push({
52
53
  ...fileModule?.default,
@@ -75,14 +76,14 @@ async function initializeFileCollectionsOnDatabase(files: FileCollection[]) {
75
76
  const db = tenant?.database?.db
76
77
  if (!db) continue;
77
78
 
78
- // Créer la collection si elle n'existe pas
79
+ // Create the collection if it does not exist
79
80
  const collections = await db.listCollections({ name: fileCollection.slug }).toArray()
80
81
  if (collections.length === 0) {
81
82
  await db.createCollection(fileCollection.slug)
82
83
  }
83
84
 
84
- // Créer les indexes par défaut (uniquement ceux qui manquent —
85
- // ne jamais forcer la recréation d'un index déjà existant)
85
+ // Create the default indexes (only the missing ones —
86
+ // never force the re-creation of an already existing index)
86
87
  const col = db.collection(fileCollection.slug)
87
88
  const existingIndexes = await col.listIndexes().toArray()
88
89
  const existingNames = new Set(existingIndexes.map((idx) => idx.name))