@anteros/core 0.0.41 → 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.
- package/database/audit.ts +9 -59
- package/database/collection.ts +3 -1
- package/database/deleteLog.ts +67 -3
- package/database/file.ts +4 -3
- package/database/mongodbadapter.ts +103 -42
- package/database/replication.ts +110 -32
- package/database/ttl.ts +68 -0
- package/database/workflow.ts +466 -240
- package/index.ts +6 -0
- package/lib/agent.ts +1327 -0
- package/lib/agentMemory.ts +586 -0
- package/lib/agents.ts +157 -0
- package/lib/ai.ts +0 -0
- package/lib/api.ts +246 -0
- package/lib/attachment.ts +351 -0
- package/lib/auditResult.ts +13 -0
- package/lib/define.ts +43 -1
- package/lib/endpoints.ts +80 -0
- package/lib/files.ts +2 -1
- package/lib/jsonSchema.ts +183 -0
- package/lib/lifecycle.ts +7 -1
- package/lib/load.ts +17 -0
- package/lib/mcp.ts +3 -2
- package/lib/messages.ts +299 -0
- package/lib/providers.ts +867 -0
- package/lib/routes.ts +14 -1
- package/lib/scriptWorker.ts +4 -1
- package/lib/scripts.ts +6 -1
- package/lib/sockets.ts +3 -1
- package/lib/workflow.ts +209 -37
- package/lib/zip.ts +109 -0
- package/package.json +1 -1
- package/server/access.ts +96 -0
- package/server/agents.ts +451 -0
- package/server/api.ts +14 -72
- package/server/boot.ts +11 -0
- package/server/hono.ts +8 -3
- package/server/mcp.ts +17 -63
- package/server/routes.ts +19 -24
- package/tests/agent-memory.test.ts +617 -0
- package/tests/agent-working-memory.test.ts +402 -0
- package/tests/agent.test.ts +1239 -0
- package/tests/agents-api.test.ts +593 -0
- package/tests/api.test.ts +51 -0
- package/tests/attachment.test.ts +241 -0
- package/tests/audit-operations.test.ts +1 -1
- package/tests/audit-redact.test.ts +29 -3
- package/tests/audit-run-action.test.ts +1 -1
- package/tests/endpoints.test.ts +80 -0
- package/tests/fixtures/agent-tenant/agents/support.agent.ts +9 -0
- package/tests/fixtures/agent-tenant/agents/weather.agent.ts +21 -0
- package/tests/fixtures/agents-api/agents/assistant.agent.ts +46 -0
- package/tests/fixtures/agents-api/agents/plain.agent.ts +23 -0
- package/tests/fixtures/agents-api/agents/private.agent.ts +15 -0
- package/tests/fixtures/agents-api/collections/notes.model.ts +26 -0
- package/tests/fixtures/agents-api/routes/probe.route.ts +21 -0
- package/tests/fixtures/agents-api/services/probe.service.ts +19 -0
- package/tests/fixtures/attachment/data.csv +2 -0
- package/tests/fixtures/fake-provider.ts +128 -0
- package/tests/fixtures/replication-meta/agents/chat.agent.ts +16 -0
- package/tests/fixtures/replication-meta/collections/counters.model.ts +13 -0
- package/tests/fixtures/replication-meta/collections/orders.model.ts +9 -0
- package/tests/fixtures/replication-meta/workflows/run.workflow.ts +8 -0
- package/tests/fixtures/vars-tenant/vars/private.var.ts +4 -1
- package/tests/fixtures/workflow-engine/workflows/failing.workflow.ts +34 -0
- package/tests/fixtures/workflow-engine/workflows/flaky.workflow.ts +23 -0
- package/tests/fixtures/workflow-engine/workflows/hang.workflow.ts +17 -0
- package/tests/fixtures/workflow-engine/workflows/simple.workflow.ts +19 -0
- package/tests/fixtures/workflow-engine/workflows/slow.workflow.ts +23 -0
- package/tests/fixtures/workflow-engine/workflows/versioned.workflow.ts +19 -0
- package/tests/fixtures/workflow-tenant/workflows/broken.workflow.ts +3 -0
- package/tests/fixtures/workflow-tenant/workflows/disabled.workflow.ts +8 -0
- package/tests/fixtures/workflow-tenant/workflows/invoice.workflow.ts +21 -0
- package/tests/fixtures/workflow-tenant/workflows/no-id.workflow.ts +7 -0
- package/tests/fixtures/workflow-tenant/workflows/order-duplicate.workflow.ts +13 -0
- package/tests/fixtures/workflow-tenant/workflows/order.workflow.ts +17 -0
- package/tests/hono.test.ts +6 -3
- package/tests/locks.test.ts +120 -0
- package/tests/mcp.test.ts +11 -11
- package/tests/messages.test.ts +258 -0
- package/tests/replication-control.test.ts +2 -2
- package/tests/replication-meta.test.ts +243 -0
- package/tests/upload.test.ts +44 -12
- package/tests/vars-api.test.ts +1 -1
- package/tests/vars-replication.test.ts +3 -2
- package/tests/workflow-engine.test.ts +340 -0
- package/tests/workflow-retention.test.ts +103 -0
- package/tests/workflow.test.ts +186 -0
- package/types/agent.d.ts +837 -0
- package/types/config.d.ts +16 -0
- package/types/file.d.ts +7 -7
- package/types/hook.d.ts +6 -0
- package/types/lifecycle.d.ts +6 -0
- package/types/mcp.d.ts +11 -1
- package/types/replication.d.ts +29 -0
- package/types/route.d.ts +7 -1
- package/types/scripts.d.ts +6 -0
- package/types/service.d.ts +7 -1
- package/types/tenant.d.ts +13 -0
- package/types/vars.d.ts +2 -2
- package/types/websocket.d.ts +6 -0
- package/types/workflow.d.ts +50 -3
package/database/replication.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { getTenant } from "./tenant";
|
|
|
4
4
|
import { useRest } from "./rest";
|
|
5
5
|
import { logger } from "../utils/logger";
|
|
6
6
|
import * as func from "../utils/func";
|
|
7
|
+
import * as os from "node:os";
|
|
7
8
|
import type { Collection } from "../types/collection";
|
|
8
9
|
import type { Tenant } from "../types/tenant";
|
|
9
10
|
import type { HookContext } from "../types/hook";
|
|
@@ -11,6 +12,7 @@ import type {
|
|
|
11
12
|
ReplicationConfig,
|
|
12
13
|
ReplicationDestination,
|
|
13
14
|
ReplicationInitialSync,
|
|
15
|
+
ReplicationMetaName,
|
|
14
16
|
ReplicationResetOptions,
|
|
15
17
|
ReplicationResetResult,
|
|
16
18
|
ReplicationRunResult,
|
|
@@ -20,6 +22,10 @@ import type {
|
|
|
20
22
|
ReplicationState,
|
|
21
23
|
ReplicationStats,
|
|
22
24
|
} from "../types/replication";
|
|
25
|
+
import { AUDIT_TTL_INDEX, resolveAuditRetention } from "./audit";
|
|
26
|
+
import { WORKFLOW_RUNS_TTL_INDEX, resolveWorkflowsRetention } from "../lib/workflow";
|
|
27
|
+
import { agentMemoryCollections } from "../lib/agents";
|
|
28
|
+
import { syncTtlRetention } from "./ttl";
|
|
23
29
|
|
|
24
30
|
const META_COLLECTION = "_replication_";
|
|
25
31
|
const DEFAULT_KEY = "updatedAt";
|
|
@@ -29,6 +35,76 @@ const BULK_CHUNK = 1000;
|
|
|
29
35
|
/** Distributed-lock TTL used to guard a run across cluster workers. */
|
|
30
36
|
const LOCK_TTL = 5 * 60_000;
|
|
31
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Framework collections replication covers **by default**, with the date key each
|
|
40
|
+
* one is read with (the engine's cursor). `'vars'` is handled apart, per namespace,
|
|
41
|
+
* and `'memory'` covers the agent memory collections — whose names the tenant
|
|
42
|
+
* chooses, so they come from the agent registry instead (see below).
|
|
43
|
+
*/
|
|
44
|
+
const META_COLLECTIONS: Record<Exclude<ReplicationMetaName, 'vars' | 'memory'>, { slug: string; key: string }> = {
|
|
45
|
+
audit: { slug: "_audit_", key: "ts" },
|
|
46
|
+
workflows: { slug: "_workflows_", key: "updatedAt" },
|
|
47
|
+
locks: { slug: "_locks_", key: "expiresAt" },
|
|
48
|
+
replication: { slug: "_replication_", key: "updatedAt" },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Everything replicated for a tenant: the collections that opted in, plus the
|
|
53
|
+
* framework's meta collections — which are replicated **by default** and left out
|
|
54
|
+
* with `replication.exclude: ['audit', 'locks', …]`.
|
|
55
|
+
*/
|
|
56
|
+
function replicatedCollectionsFor(tenantId: string, config: ReplicationConfig): Collection[] {
|
|
57
|
+
const excluded = new Set<string>(config.exclude ?? []);
|
|
58
|
+
|
|
59
|
+
const collections: Collection[] = [
|
|
60
|
+
...(cfg.collections ?? []).filter((c) => c._tenant_ === tenantId && c.replication?.enabled),
|
|
61
|
+
...(cfg.fileCollections ?? [])
|
|
62
|
+
.filter((c) => c._tenant_ === tenantId && c.replication?.enabled)
|
|
63
|
+
.map((c) => ({ slug: c.slug, _tenant_: tenantId, replication: c.replication }) as any),
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
for (const [name, meta] of Object.entries(META_COLLECTIONS)) {
|
|
67
|
+
if (excluded.has(name)) continue;
|
|
68
|
+
// Each meta collection knows its own date key (`_audit_` is indexed on `ts`),
|
|
69
|
+
// so a tenant-level `key` never breaks it
|
|
70
|
+
collections.push({
|
|
71
|
+
slug: meta.slug,
|
|
72
|
+
_tenant_: tenantId,
|
|
73
|
+
replication: { enabled: true, key: meta.key },
|
|
74
|
+
} as any);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!excluded.has('vars')) {
|
|
78
|
+
// Every namespace, minus the ones explicitly opting out — a per-document
|
|
79
|
+
// filter: `replication.exclude: ['vars']` is the blunt way out.
|
|
80
|
+
const optedOut = (cfg.vars ?? [])
|
|
81
|
+
.filter((v) => v._tenant_ === tenantId && v.replication?.enabled === false)
|
|
82
|
+
.map((v) => v.namespace);
|
|
83
|
+
collections.push({
|
|
84
|
+
slug: "_vars_",
|
|
85
|
+
_tenant_: tenantId,
|
|
86
|
+
replication: { enabled: true },
|
|
87
|
+
_baseFilter_: { ns: { $nin: optedOut } },
|
|
88
|
+
} as any);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Agent memory collections — the tenant names them, the agent loader publishes
|
|
92
|
+
// them (`cfg.agentMemories`). `exclude: ['memory']` covers all of them.
|
|
93
|
+
if (!excluded.has('memory')) {
|
|
94
|
+
for (const slug of agentMemoryCollections(tenantId)) {
|
|
95
|
+
// A memory collection may also be declared as a collection: one entry only
|
|
96
|
+
if (collections.some((collection) => collection.slug === slug)) continue;
|
|
97
|
+
collections.push({
|
|
98
|
+
slug,
|
|
99
|
+
_tenant_: tenantId,
|
|
100
|
+
replication: { enabled: true, key: "updatedAt" },
|
|
101
|
+
} as any);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return collections;
|
|
106
|
+
}
|
|
107
|
+
|
|
32
108
|
type Runtime = {
|
|
33
109
|
tenantId: string;
|
|
34
110
|
destination: ReplicationDestination;
|
|
@@ -221,6 +297,11 @@ async function saveState(
|
|
|
221
297
|
status,
|
|
222
298
|
error,
|
|
223
299
|
stats: { ...stats },
|
|
300
|
+
// Which process owns this run — `reusePort`/forked deployments can
|
|
301
|
+
// see at a glance which worker is replicating (the lock holds the
|
|
302
|
+
// same information)
|
|
303
|
+
pid: process.pid,
|
|
304
|
+
hostname: os.hostname(),
|
|
224
305
|
updatedAt: now,
|
|
225
306
|
},
|
|
226
307
|
$setOnInsert: { createdAt: now },
|
|
@@ -311,7 +392,15 @@ async function flushDeletes(
|
|
|
311
392
|
|
|
312
393
|
const ops = markers.map((m: any) => ({
|
|
313
394
|
deleteOne: {
|
|
314
|
-
filter: {
|
|
395
|
+
filter: {
|
|
396
|
+
_id: toId(m.docId),
|
|
397
|
+
// The key is a Date on most collections, but a number on `_locks_`:
|
|
398
|
+
// accept either, so a numeric key never silently skips the delete
|
|
399
|
+
$or: [
|
|
400
|
+
{ [key]: { $lte: m.deletedAt } },
|
|
401
|
+
{ [key]: { $lte: m.deletedAt?.getTime?.() ?? m.deletedAt } },
|
|
402
|
+
],
|
|
403
|
+
},
|
|
315
404
|
},
|
|
316
405
|
}));
|
|
317
406
|
const res = await withRetry(() => runtime.db.collection(collection.slug).bulkWrite(ops as any, { ordered: false }));
|
|
@@ -388,6 +477,23 @@ async function replicateCollection(
|
|
|
388
477
|
await ensureIndex(tenant.database.db!, collection.slug, key, "source");
|
|
389
478
|
await ensureIndex(runtime.db, collection.slug, key, `destination:${runtime.destination.id}`);
|
|
390
479
|
|
|
480
|
+
// Meta collections have no hooks, so their deletions never reach the
|
|
481
|
+
// destination (an audit TTL expiry is invisible to a date cursor): mirror the
|
|
482
|
+
// source's retention on the destination, otherwise the copy grows forever.
|
|
483
|
+
if (collection.slug === '_audit_' || collection.slug === '_workflows_') {
|
|
484
|
+
const isAudit = collection.slug === '_audit_';
|
|
485
|
+
const retention = isAudit ? resolveAuditRetention(tenant) : resolveWorkflowsRetention(tenant);
|
|
486
|
+
if (retention !== undefined) {
|
|
487
|
+
await syncTtlRetention(runtime.db, {
|
|
488
|
+
collection: collection.slug,
|
|
489
|
+
index: isAudit ? AUDIT_TTL_INDEX : WORKFLOW_RUNS_TTL_INDEX,
|
|
490
|
+
field: isAudit ? 'ts' : 'completedAt',
|
|
491
|
+
retention,
|
|
492
|
+
label: `${collection.slug} (destination)`,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
391
497
|
// Resolve the initial cursor on the first run — `initialSync` lets a
|
|
392
498
|
// pre-loaded destination skip the full backfill (`full` = default).
|
|
393
499
|
if (!state) {
|
|
@@ -452,29 +558,7 @@ async function replicateTenant(
|
|
|
452
558
|
const mainDb = tenant?.database?.db;
|
|
453
559
|
if (!tenant || !config || !mainDb) return [];
|
|
454
560
|
|
|
455
|
-
const collections = (
|
|
456
|
-
|
|
457
|
-
// File collections opt in the same way (`define.FileCollection({ replication })`).
|
|
458
|
-
collections.push(
|
|
459
|
-
...(cfg.fileCollections ?? [])
|
|
460
|
-
.filter((c) => c._tenant_ === tenantId && c.replication?.enabled)
|
|
461
|
-
.map((c) => ({ slug: c.slug, _tenant_: tenantId, replication: c.replication }) as any),
|
|
462
|
-
);
|
|
463
|
-
|
|
464
|
-
// `define.Vars({ replication: { enabled: true } })` → replicate the opted-in
|
|
465
|
-
// namespaces of `_vars_`. A per-document filter keeps the namespaces that
|
|
466
|
-
// did NOT opt in (e.g. secrets) out of the destination.
|
|
467
|
-
const varsNamespaces = (cfg.vars ?? [])
|
|
468
|
-
.filter((v) => v._tenant_ === tenantId && v.replication?.enabled)
|
|
469
|
-
.map((v) => v.namespace);
|
|
470
|
-
if (varsNamespaces.length) {
|
|
471
|
-
collections.push({
|
|
472
|
-
slug: "_vars_",
|
|
473
|
-
_tenant_: tenantId,
|
|
474
|
-
replication: { enabled: true },
|
|
475
|
-
_baseFilter_: { ns: { $in: varsNamespaces } },
|
|
476
|
-
} as any);
|
|
477
|
-
}
|
|
561
|
+
const collections = replicatedCollectionsFor(tenantId, config);
|
|
478
562
|
const results: ReplicationRunResult[] = [];
|
|
479
563
|
const meta = mainDb.collection(META_COLLECTION);
|
|
480
564
|
const rest = new useRest({ tenant_id: tenantId });
|
|
@@ -606,14 +690,8 @@ async function seedReplication(
|
|
|
606
690
|
if (!tenant || !config || !mainDb) return { tenant: tenantId, seeded: 0 };
|
|
607
691
|
|
|
608
692
|
const meta: any = mainDb.collection(META_COLLECTION);
|
|
609
|
-
const collections = (
|
|
610
|
-
(c) =>
|
|
611
|
-
);
|
|
612
|
-
collections.push(
|
|
613
|
-
...(cfg.fileCollections ?? [])
|
|
614
|
-
.filter((c) => c._tenant_ === tenantId && c.replication?.enabled && (!opts?.collection || c.slug === opts.collection))
|
|
615
|
-
.map((c) => ({ slug: c.slug, _tenant_: tenantId, replication: c.replication }) as any),
|
|
616
|
-
);
|
|
693
|
+
const collections = replicatedCollectionsFor(tenantId, config)
|
|
694
|
+
.filter((c) => !opts?.collection || c.slug === opts.collection);
|
|
617
695
|
const destinations = (config.destinations ?? []).filter(
|
|
618
696
|
(d) => d.enabled !== false && d.id && d.uri && (!opts?.destination || d.id === opts.destination),
|
|
619
697
|
);
|
package/database/ttl.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Db } from "mongodb";
|
|
2
|
+
import { parseDuration } from "../utils/func";
|
|
3
|
+
|
|
4
|
+
export type RetentionResult = 'applied' | 'dropped' | 'unchanged' | 'skipped'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Declarative TTL retention for a collection:
|
|
8
|
+
*
|
|
9
|
+
* - a duration string (`'90d'`, `'24h'`) → ensure the TTL index with that expiry
|
|
10
|
+
* - `false` → explicitly disabled: drop the TTL index
|
|
11
|
+
* - omitted → untouched
|
|
12
|
+
*
|
|
13
|
+
* MongoDB cannot change `expireAfterSeconds` in place through `createIndex`:
|
|
14
|
+
* `collMod` does it instantly, dropping/recreating is the fallback. Never fatal —
|
|
15
|
+
* a failure is logged and `'skipped'` is returned.
|
|
16
|
+
*/
|
|
17
|
+
async function syncTtlRetention(
|
|
18
|
+
db: Db,
|
|
19
|
+
opts: { collection: string; index: string; field: string; retention?: string | false; label?: string },
|
|
20
|
+
): Promise<RetentionResult> {
|
|
21
|
+
const { collection, index, field, retention } = opts;
|
|
22
|
+
const label = opts.label ?? collection;
|
|
23
|
+
|
|
24
|
+
if (retention === undefined) return 'skipped';
|
|
25
|
+
|
|
26
|
+
const col: any = db.collection(collection);
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const existing = (await col.listIndexes().toArray())
|
|
30
|
+
.find((entry: any) => entry.name === index);
|
|
31
|
+
|
|
32
|
+
if (retention === false) {
|
|
33
|
+
if (!existing) return 'unchanged';
|
|
34
|
+
await col.dropIndex(index);
|
|
35
|
+
console.log(`🧹 ${label} retention disabled — ${index} dropped`);
|
|
36
|
+
return 'dropped';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const parsed = parseDuration(retention);
|
|
40
|
+
if (parsed === null) {
|
|
41
|
+
console.error(`Invalid ${label} retention '${retention}' — expected a duration like '90d' or '24h'`);
|
|
42
|
+
return 'skipped';
|
|
43
|
+
}
|
|
44
|
+
const expireAfterSeconds = Math.round(parsed / 1000);
|
|
45
|
+
|
|
46
|
+
if (!existing) {
|
|
47
|
+
await col.createIndex({ [field]: 1 }, { name: index, expireAfterSeconds, background: true });
|
|
48
|
+
console.log(`🧹 ${label} retention enabled — entries expire after ${retention}`);
|
|
49
|
+
return 'applied';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (existing.expireAfterSeconds === expireAfterSeconds) return 'unchanged';
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
await db.command({ collMod: collection, index: { name: index, expireAfterSeconds } });
|
|
56
|
+
} catch {
|
|
57
|
+
await col.dropIndex(index);
|
|
58
|
+
await col.createIndex({ [field]: 1 }, { name: index, expireAfterSeconds, background: true });
|
|
59
|
+
}
|
|
60
|
+
console.log(`🧹 ${label} retention updated — entries expire after ${retention}`);
|
|
61
|
+
return 'applied';
|
|
62
|
+
} catch (err: any) {
|
|
63
|
+
console.error(`${label} retention failed (${retention})`, err?.message);
|
|
64
|
+
return 'skipped';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { syncTtlRetention }
|