@electric-ax/agents-server 0.3.0 → 0.4.1

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.
@@ -0,0 +1,5 @@
1
+ ALTER TABLE entities
2
+ ADD COLUMN created_by text;
3
+
4
+ CREATE INDEX idx_entities_created_by
5
+ ON entities (tenant_id, created_by);
@@ -43,6 +43,13 @@
43
43
  "when": 1776265200000,
44
44
  "tag": "0005_pull_wake_control_plane",
45
45
  "breakpoints": true
46
+ },
47
+ {
48
+ "idx": 6,
49
+ "version": "7",
50
+ "when": 1776268800000,
51
+ "tag": "0006_principals",
52
+ "breakpoints": true
46
53
  }
47
54
  ]
48
55
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electric-ax/agents-server",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Electric Agents entity runtime server",
5
5
  "author": "Durable Stream contributors",
6
6
  "bin": {
@@ -54,7 +54,7 @@
54
54
  "pino-pretty": "^13.0.0",
55
55
  "postgres": "^3.4.0",
56
56
  "undici": "^7.24.7",
57
- "@electric-ax/agents-runtime": "0.1.3"
57
+ "@electric-ax/agents-runtime": "0.2.1"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "^22.19.15",
@@ -65,9 +65,9 @@
65
65
  "tsx": "^4.19.0",
66
66
  "typescript": "^5.0.0",
67
67
  "vitest": "^4.1.0",
68
- "@electric-ax/agents-server-conformance-tests": "0.1.2",
69
- "@electric-ax/agents": "0.3.0",
70
- "@electric-ax/agents-server-ui": "0.3.0"
68
+ "@electric-ax/agents": "0.4.1",
69
+ "@electric-ax/agents-server-conformance-tests": "0.1.4",
70
+ "@electric-ax/agents-server-ui": "0.4.1"
71
71
  },
72
72
  "files": [
73
73
  "dist",
@@ -80,7 +80,7 @@
80
80
  "scripts": {
81
81
  "build": "tsdown",
82
82
  "dev": "tsdown --watch",
83
- "start": "cross-env-shell \"DATABASE_URL=${DATABASE_URL:-postgresql://electric_agents:electric_agents@localhost:5432/electric_agents} ELECTRIC_URL=${ELECTRIC_URL:-http://localhost:3060} tsx --watch src/entrypoint.ts\"",
83
+ "start": "cross-env-shell DATABASE_URL=${DATABASE_URL:-postgresql://electric_agents:electric_agents@localhost:5432/electric_agents} ELECTRIC_URL=${ELECTRIC_URL:-http://localhost:3060} \"tsx --watch src/entrypoint.ts\"",
84
84
  "test": "vitest run",
85
85
  "coverage": "ELECTRIC_AGENTS_KEEP_BACKEND=1 pnpm exec vitest run --coverage $(find test -name '*.test.ts' ! -name 'conformance.test.ts' | sort) && pnpm exec vitest run test/conformance.test.ts",
86
86
  "typecheck": "tsc --noEmit",
@@ -1,17 +1,7 @@
1
- import type { AuthenticatedRequestUser } from './electric-agents-types.js'
1
+ import type { Principal } from './principal.js'
2
2
 
3
- function clean(value: string | undefined): string | undefined {
4
- const trimmed = value?.trim()
5
- return trimmed || undefined
6
- }
7
-
8
- export function formatAuthenticatedUser(
9
- user: AuthenticatedRequestUser | null | undefined
3
+ export function formatRequestPrincipal(
4
+ principal: Principal | null | undefined
10
5
  ): string | undefined {
11
- if (!user) return undefined
12
- const email = clean(user.email)
13
- const name = clean(user.name)
14
- const userId = clean(user.userId)
15
- if (name && email) return `${name} <${email}>`
16
- return email ?? name ?? userId
6
+ return principal?.key
17
7
  }
package/src/db/schema.ts CHANGED
@@ -50,6 +50,7 @@ export const entities = pgTable(
50
50
  .default(sql`'{}'::text[]`),
51
51
  spawnArgs: jsonb(`spawn_args`).default({}),
52
52
  parent: text(`parent`),
53
+ createdBy: text(`created_by`),
53
54
  typeRevision: integer(`type_revision`),
54
55
  inboxSchemas: jsonb(`inbox_schemas`),
55
56
  stateSchemas: jsonb(`state_schemas`),
@@ -61,6 +62,7 @@ export const entities = pgTable(
61
62
  index(`idx_entities_type`).on(table.tenantId, table.type),
62
63
  index(`idx_entities_status`).on(table.tenantId, table.status),
63
64
  index(`idx_entities_parent`).on(table.tenantId, table.parent),
65
+ index(`idx_entities_created_by`).on(table.tenantId, table.createdBy),
64
66
  index(`entities_tags_index_gin`).using(`gin`, table.tagsIndex),
65
67
  check(
66
68
  `chk_entities_status`,
@@ -3,18 +3,14 @@
3
3
  */
4
4
 
5
5
  import type { WebhookNotification } from '@electric-ax/agents-runtime'
6
+ import type { Principal } from './principal.js'
6
7
 
7
8
  type WakeNotification = WebhookNotification
8
9
 
9
- export interface AuthenticatedRequestUser {
10
- userId: string
11
- email?: string
12
- name?: string
13
- }
14
-
10
+ export type RequestPrincipal = Principal
15
11
  export type AuthenticateRequest = (
16
12
  request: Request
17
- ) => Promise<AuthenticatedRequestUser | null> | AuthenticatedRequestUser | null
13
+ ) => Promise<Principal | null> | Principal | null
18
14
 
19
15
  export type EntityStatus = `spawning` | `running` | `idle` | `stopped`
20
16
 
@@ -211,6 +207,7 @@ export interface ElectricAgentsEntity {
211
207
  type_revision?: number
212
208
  inbox_schemas?: Record<string, Record<string, unknown>>
213
209
  state_schemas?: Record<string, Record<string, unknown>>
210
+ created_by?: string
214
211
  created_at: number
215
212
  updated_at: number
216
213
  }
@@ -225,6 +222,7 @@ export interface PublicElectricAgentsEntity {
225
222
  tags: Record<string, string>
226
223
  spawn_args?: Record<string, unknown>
227
224
  parent?: string
225
+ created_by?: string
228
226
  created_at: number
229
227
  updated_at: number
230
228
  }
@@ -248,6 +246,7 @@ export function toPublicEntity(
248
246
  tags: entity.tags,
249
247
  spawn_args: entity.spawn_args,
250
248
  parent: entity.parent,
249
+ created_by: entity.created_by,
251
250
  created_at: entity.created_at,
252
251
  updated_at: entity.updated_at,
253
252
  }
@@ -283,6 +282,7 @@ export interface TypedSpawnRequest {
283
282
  parent?: string
284
283
  dispatch_policy?: DispatchPolicy
285
284
  initialMessage?: unknown
285
+ created_by?: string
286
286
  wake?: {
287
287
  subscriberUrl: string
288
288
  condition:
@@ -303,6 +303,8 @@ export interface SendRequest {
303
303
  payload?: unknown
304
304
  key?: string
305
305
  type?: string
306
+ mode?: `immediate` | `queued` | `paused` | `steer`
307
+ position?: string
306
308
  }
307
309
 
308
310
  export interface SetTagRequest {
@@ -312,6 +314,7 @@ export interface SetTagRequest {
312
314
  export interface EntityListFilter {
313
315
  type?: string
314
316
  status?: EntityStatus
317
+ created_by?: string
315
318
  }
316
319
 
317
320
  export const ErrCodeDuplicateURL = `DUPLICATE_URL`
@@ -70,7 +70,7 @@ const ENTITY_SHAPE_COLUMNS = [
70
70
  ] as const
71
71
 
72
72
  function parseElectricOffset(offset: string): Offset | null {
73
- if (offset === `-1` || offset === `now`) {
73
+ if (offset === `-1`) {
74
74
  return offset
75
75
  }
76
76
  return /^\d+_\d+$/.test(offset) ? (offset as Offset) : null
@@ -27,6 +27,14 @@ import {
27
27
  ErrCodeUnknownMessageType,
28
28
  } from './electric-agents-types.js'
29
29
  import { parseDispatchPolicy } from './dispatch-policy-schema.js'
30
+ import { applyTypeDefaultSubscriptionScope } from './routing/dispatch-policy.js'
31
+ import {
32
+ isBuiltInSystemPrincipalUrl,
33
+ principalFromCreatedBy,
34
+ principalUrl,
35
+ principalIdentityStateSchema,
36
+ principalUpdateIdentityMessageSchema,
37
+ } from './principal.js'
30
38
  import { EntityAlreadyExistsError } from './entity-registry.js'
31
39
  import { serverLog } from './utils/log.js'
32
40
  import {
@@ -44,7 +52,6 @@ import type { SchemaValidator } from './electric-agents/schema-validator.js'
44
52
  import type { StreamClient } from './stream-client.js'
45
53
  import type {
46
54
  DispatchPolicy,
47
- DispatchTarget,
48
55
  ElectricAgentsEntity,
49
56
  ElectricAgentsEntityType,
50
57
  RegisterEntityTypeRequest,
@@ -53,6 +60,7 @@ import type {
53
60
  TypedSpawnRequest,
54
61
  } from './electric-agents-types.js'
55
62
  import type { EntityBridgeCoordinator } from './entity-bridge-manager.js'
63
+ import type { Principal } from './principal.js'
56
64
 
57
65
  type SpawnPersistResult = [
58
66
  PromiseSettledResult<void>,
@@ -65,6 +73,10 @@ type WriteTokenValidator = (
65
73
  token: string
66
74
  ) => boolean
67
75
 
76
+ function createInitialQueuePosition(date: Date): string {
77
+ return `${String(date.getTime()).padStart(16, `0`)}:a0`
78
+ }
79
+
68
80
  type ForkSubtreeOptions = {
69
81
  rootInstanceId?: string
70
82
  waitTimeoutMs?: number
@@ -91,33 +103,6 @@ type ForkResult = {
91
103
  const DEFAULT_FORK_WAIT_TIMEOUT_MS = 120_000
92
104
  const DEFAULT_FORK_WAIT_POLL_MS = 250
93
105
 
94
- function applyTypeDefaultSubscriptionScope(
95
- policy: DispatchPolicy,
96
- typeDefault: DispatchPolicy | undefined
97
- ): DispatchPolicy {
98
- const target = policy.targets[0]
99
- const defaultTarget = typeDefault?.targets[0]
100
- if (!target || !defaultTarget?.subscription_id) return policy
101
- if (!sameDispatchDestination(target, defaultTarget)) return policy
102
- if (target.subscription_id === defaultTarget.subscription_id) return policy
103
-
104
- return {
105
- targets: [{ ...target, subscription_id: defaultTarget.subscription_id }],
106
- }
107
- }
108
-
109
- function sameDispatchDestination(
110
- a: DispatchTarget,
111
- b: DispatchTarget
112
- ): boolean {
113
- if (a.type !== b.type) return false
114
- if (a.type === `runner` && b.type === `runner`) {
115
- return a.runnerId === b.runnerId
116
- }
117
- if (a.type === `webhook` && b.type === `webhook`) return a.url === b.url
118
- return false
119
- }
120
-
121
106
  function sleep(ms: number): Promise<void> {
122
107
  return new Promise((resolve) => setTimeout(resolve, ms))
123
108
  }
@@ -239,6 +224,13 @@ export class EntityManager {
239
224
  400
240
225
  )
241
226
  }
227
+ if (req.name === `principal`) {
228
+ throw new ElectricAgentsError(
229
+ ErrCodeInvalidRequest,
230
+ `Entity type "principal" is built in and cannot be registered or updated`,
231
+ 400
232
+ )
233
+ }
242
234
  if (req.name.startsWith(`_`)) {
243
235
  throw new ElectricAgentsError(
244
236
  ErrCodeInvalidRequest,
@@ -290,6 +282,13 @@ export class EntityManager {
290
282
  }
291
283
 
292
284
  async deleteEntityType(name: string): Promise<void> {
285
+ if (name === `principal`) {
286
+ throw new ElectricAgentsError(
287
+ ErrCodeInvalidRequest,
288
+ `Entity type "principal" is built in and cannot be deleted`,
289
+ 400
290
+ )
291
+ }
293
292
  const existing = await this.registry.getEntityType(name)
294
293
  if (!existing) {
295
294
  throw new ElectricAgentsError(
@@ -302,6 +301,59 @@ export class EntityManager {
302
301
  await this.registry.deleteEntityType(name)
303
302
  }
304
303
 
304
+ async ensurePrincipalEntityType(): Promise<ElectricAgentsEntityType> {
305
+ const now = new Date().toISOString()
306
+ return await this.registry.ensureEntityType({
307
+ name: `principal`,
308
+ description: `built-in principal entity`,
309
+ inbox_schemas: { update_identity: principalUpdateIdentityMessageSchema },
310
+ state_schemas: { identity: principalIdentityStateSchema },
311
+ revision: 1,
312
+ created_at: now,
313
+ updated_at: now,
314
+ })
315
+ }
316
+
317
+ async ensurePrincipal(principal: Principal): Promise<ElectricAgentsEntity> {
318
+ const existing = await this.registry.getEntity(principal.url)
319
+ if (existing) return existing
320
+ await this.ensurePrincipalEntityType()
321
+ try {
322
+ const entity = await this.spawn(`principal`, {
323
+ instance_id: principal.key,
324
+ args: { kind: principal.kind, id: principal.id, key: principal.key },
325
+ tags: { principal_kind: principal.kind, principal_id: principal.id },
326
+ created_by: principal.url,
327
+ })
328
+ const now = new Date().toISOString()
329
+ await this.streamClient.append(
330
+ entity.streams.main,
331
+ this.encodeChangeEvent({
332
+ type: `identity`,
333
+ key: `self`,
334
+ value: {
335
+ kind: principal.kind,
336
+ id: principal.id,
337
+ key: principal.key,
338
+ url: principal.url,
339
+ created_at: now,
340
+ updated_at: now,
341
+ },
342
+ })
343
+ )
344
+ return entity
345
+ } catch (error) {
346
+ if (
347
+ error instanceof ElectricAgentsError &&
348
+ error.code === ErrCodeDuplicateURL
349
+ ) {
350
+ const raced = await this.registry.getEntity(principal.url)
351
+ if (raced) return raced
352
+ }
353
+ throw error
354
+ }
355
+ }
356
+
305
357
  // ==========================================================================
306
358
  // Spawn
307
359
  // ==========================================================================
@@ -328,6 +380,17 @@ export class EntityManager {
328
380
  typeName: string,
329
381
  req: TypedSpawnRequest
330
382
  ): Promise<ElectricAgentsEntity & { txid: number }> {
383
+ if (
384
+ typeName === `principal` &&
385
+ req.created_by !== principalUrl(req.instance_id)
386
+ ) {
387
+ throw new ElectricAgentsError(
388
+ ErrCodeInvalidRequest,
389
+ `Principal entities are built in and can only be materialized by the system`,
390
+ 400
391
+ )
392
+ }
393
+
331
394
  if (typeName.startsWith(`_`)) {
332
395
  throw new ElectricAgentsError(
333
396
  ErrCodeInvalidRequest,
@@ -375,7 +438,10 @@ export class EntityManager {
375
438
 
376
439
  const writeToken = randomUUID()
377
440
 
378
- const entityURL = `/${typeName}/${instanceId}`
441
+ const entityURL =
442
+ typeName === `principal`
443
+ ? principalUrl(instanceId)
444
+ : `/${typeName}/${instanceId}`
379
445
  const mainPath = `${entityURL}/main`
380
446
  const errorPath = `${entityURL}/error`
381
447
 
@@ -433,6 +499,7 @@ export class EntityManager {
433
499
  inbox_schemas: entityType.inbox_schemas,
434
500
  state_schemas: entityType.state_schemas,
435
501
  created_at: now,
502
+ created_by: req.created_by ?? parentEntity?.created_by,
436
503
  updated_at: now,
437
504
  }
438
505
  if (req.parent) {
@@ -473,7 +540,7 @@ export class EntityManager {
473
540
  const inboxEvent = entityStateSchema.inbox.insert({
474
541
  key: `msg-in-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
475
542
  value: {
476
- from: req.parent ?? `spawn`,
543
+ from: req.created_by ?? req.parent ?? `spawn`,
477
544
  payload: req.initialMessage,
478
545
  timestamp: msgNow,
479
546
  },
@@ -1371,10 +1438,10 @@ export class EntityManager {
1371
1438
  changed = true
1372
1439
  }
1373
1440
  }
1374
- if (typeof next.from === `string`) {
1375
- const forkFrom = entityUrlMap.get(next.from)
1376
- if (forkFrom) {
1377
- next.from = forkFrom
1441
+ if (typeof next.senderUrl === `string`) {
1442
+ const forkSender = entityUrlMap.get(next.senderUrl)
1443
+ if (forkSender) {
1444
+ next.senderUrl = forkSender
1378
1445
  changed = true
1379
1446
  }
1380
1447
  }
@@ -1490,6 +1557,10 @@ export class EntityManager {
1490
1557
  const fireAtRaw = manifest.fireAt
1491
1558
  const producerId = manifest.producerId
1492
1559
  const targetUrl = manifest.targetUrl
1560
+ const senderUrl =
1561
+ typeof manifest.senderUrl === `string`
1562
+ ? manifest.senderUrl
1563
+ : ownerEntityUrl
1493
1564
  if (
1494
1565
  typeof fireAtRaw !== `string` ||
1495
1566
  typeof producerId !== `string` ||
@@ -1514,8 +1585,7 @@ export class EntityManager {
1514
1585
  manifestKey,
1515
1586
  {
1516
1587
  entityUrl: targetUrl,
1517
- from:
1518
- typeof manifest.from === `string` ? manifest.from : ownerEntityUrl,
1588
+ from: senderUrl,
1519
1589
  payload: manifest.payload,
1520
1590
  key: `scheduled-${producerId}`,
1521
1591
  type:
@@ -1532,6 +1602,7 @@ export class EntityManager {
1532
1602
  kind: `schedule`,
1533
1603
  scheduleType: `future_send`,
1534
1604
  targetUrl,
1605
+ senderUrl,
1535
1606
  fireAt: fireAt.toISOString(),
1536
1607
  producerId,
1537
1608
  status: `pending`,
@@ -1584,10 +1655,23 @@ export class EntityManager {
1584
1655
  from: req.from,
1585
1656
  payload: req.payload,
1586
1657
  timestamp: now,
1658
+ mode: req.mode ?? `immediate`,
1659
+ status:
1660
+ req.mode === `queued` || req.mode === `paused`
1661
+ ? `pending`
1662
+ : `processed`,
1587
1663
  }
1588
1664
  if (req.type) {
1589
1665
  value.message_type = req.type
1590
1666
  }
1667
+ if (req.position) {
1668
+ value.position = req.position
1669
+ } else if (value.mode === `queued` || value.mode === `paused`) {
1670
+ value.position = createInitialQueuePosition(new Date(now))
1671
+ }
1672
+ if (value.status === `processed`) {
1673
+ value.processed_at = now
1674
+ }
1591
1675
 
1592
1676
  const envelope = entityStateSchema.inbox.insert({
1593
1677
  key,
@@ -1604,6 +1688,17 @@ export class EntityManager {
1604
1688
  }
1605
1689
 
1606
1690
  await this.streamClient.append(entity.streams.main, encoded)
1691
+ if (entity.type === `principal` && req.type === `update_identity`) {
1692
+ const identity = (req.payload as { identity?: unknown })?.identity
1693
+ await this.streamClient.append(
1694
+ entity.streams.main,
1695
+ this.encodeChangeEvent({
1696
+ type: `identity`,
1697
+ key: `self`,
1698
+ value: identity,
1699
+ })
1700
+ )
1701
+ }
1607
1702
  } catch (err) {
1608
1703
  if (this.isClosedStreamError(err)) {
1609
1704
  throw new ElectricAgentsError(
@@ -1616,6 +1711,69 @@ export class EntityManager {
1616
1711
  }
1617
1712
  }
1618
1713
 
1714
+ async updateInboxMessage(
1715
+ entityUrl: string,
1716
+ key: string,
1717
+ req: {
1718
+ payload?: unknown
1719
+ position?: string
1720
+ mode?: `immediate` | `queued` | `paused` | `steer`
1721
+ status?: `pending` | `processed` | `cancelled`
1722
+ }
1723
+ ): Promise<void> {
1724
+ const entity = await this.registry.getEntity(entityUrl)
1725
+ if (!entity) {
1726
+ throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404)
1727
+ }
1728
+ if (entity.status === `stopped`) {
1729
+ throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409)
1730
+ }
1731
+
1732
+ const now = new Date().toISOString()
1733
+ const value: Record<string, unknown> = {}
1734
+ if (`payload` in req) value.payload = req.payload
1735
+ if (req.position !== undefined) value.position = req.position
1736
+ if (req.mode !== undefined) value.mode = req.mode
1737
+ if (req.status !== undefined) {
1738
+ value.status = req.status
1739
+ if (req.status === `processed`) value.processed_at = now
1740
+ if (req.status === `cancelled`) value.cancelled_at = now
1741
+ }
1742
+
1743
+ if (Object.keys(value).length === 0) {
1744
+ throw new ElectricAgentsError(
1745
+ ErrCodeInvalidRequest,
1746
+ `No inbox fields to update`,
1747
+ 400
1748
+ )
1749
+ }
1750
+
1751
+ const envelope = entityStateSchema.inbox.update({
1752
+ key,
1753
+ value,
1754
+ } as any)
1755
+ await this.streamClient.append(
1756
+ entity.streams.main,
1757
+ this.encodeChangeEvent(envelope as Record<string, unknown>)
1758
+ )
1759
+ }
1760
+
1761
+ async deleteInboxMessage(entityUrl: string, key: string): Promise<void> {
1762
+ const entity = await this.registry.getEntity(entityUrl)
1763
+ if (!entity) {
1764
+ throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404)
1765
+ }
1766
+ if (entity.status === `stopped`) {
1767
+ throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409)
1768
+ }
1769
+
1770
+ const envelope = entityStateSchema.inbox.delete({ key } as any)
1771
+ await this.streamClient.append(
1772
+ entity.streams.main,
1773
+ this.encodeChangeEvent(envelope as Record<string, unknown>)
1774
+ )
1775
+ }
1776
+
1619
1777
  // ==========================================================================
1620
1778
  // Tag Updates
1621
1779
  // ==========================================================================
@@ -1830,7 +1988,7 @@ export class EntityManager {
1830
1988
  payload: unknown
1831
1989
  targetUrl?: string
1832
1990
  fireAt: string
1833
- from?: string
1991
+ senderUrl?: string
1834
1992
  messageType?: string
1835
1993
  }
1836
1994
  ): Promise<{ txid: string }> {
@@ -1839,7 +1997,7 @@ export class EntityManager {
1839
1997
  }
1840
1998
 
1841
1999
  const targetUrl = req.targetUrl ?? ownerEntityUrl
1842
- const from = req.from ?? ownerEntityUrl
2000
+ const from = req.senderUrl ?? ownerEntityUrl
1843
2001
  const fireAt = new Date(req.fireAt)
1844
2002
  if (Number.isNaN(fireAt.getTime())) {
1845
2003
  throw new ElectricAgentsError(
@@ -1883,9 +2041,9 @@ export class EntityManager {
1883
2041
  scheduleType: `future_send`,
1884
2042
  fireAt: fireAt.toISOString(),
1885
2043
  targetUrl,
2044
+ senderUrl: from,
1886
2045
  payload: req.payload,
1887
2046
  producerId,
1888
- ...(req.from ? { from: req.from } : {}),
1889
2047
  ...(req.messageType ? { messageType: req.messageType } : {}),
1890
2048
  status: `pending`,
1891
2049
  },
@@ -1906,9 +2064,9 @@ export class EntityManager {
1906
2064
  scheduleType: `future_send`,
1907
2065
  fireAt: fireAt.toISOString(),
1908
2066
  targetUrl,
2067
+ senderUrl: from,
1909
2068
  payload: req.payload,
1910
2069
  producerId,
1911
- ...(req.from ? { from: req.from } : {}),
1912
2070
  ...(req.messageType ? { messageType: req.messageType } : {}),
1913
2071
  status: `pending`,
1914
2072
  },
@@ -1987,6 +2145,8 @@ export class EntityManager {
1987
2145
  payload: req.payload,
1988
2146
  key: req.key,
1989
2147
  type: req.type,
2148
+ mode: req.mode,
2149
+ position: req.position,
1990
2150
  },
1991
2151
  fireAt
1992
2152
  )
@@ -2308,6 +2468,14 @@ export class EntityManager {
2308
2468
  state_schemas?: Record<string, Record<string, unknown>>
2309
2469
  }
2310
2470
  ): Promise<ElectricAgentsEntityType> {
2471
+ if (typeName === `principal`) {
2472
+ throw new ElectricAgentsError(
2473
+ ErrCodeInvalidRequest,
2474
+ `Entity type "principal" is built in and cannot be amended`,
2475
+ 400
2476
+ )
2477
+ }
2478
+
2311
2479
  // Validate each provided schema via validateSchemaSubset.
2312
2480
  this.validateSchemaMap(schemas.inbox_schemas)
2313
2481
  this.validateSchemaMap(schemas.state_schemas)
@@ -2400,8 +2568,10 @@ export class EntityManager {
2400
2568
  streams: entity.streams,
2401
2569
  tags: entity.tags,
2402
2570
  spawnArgs: entity.spawn_args,
2571
+ createdBy: entity.created_by,
2403
2572
  },
2404
- triggerEvent: `message_received`,
2573
+ principal: principalFromCreatedBy(entity.created_by),
2574
+ triggerEvent: `inbox`,
2405
2575
  }
2406
2576
  }
2407
2577
 
@@ -2491,6 +2661,18 @@ export class EntityManager {
2491
2661
  400
2492
2662
  )
2493
2663
  }
2664
+ if (
2665
+ entity.type === `principal` &&
2666
+ req.type === `update_identity` &&
2667
+ !isBuiltInSystemPrincipalUrl(req.from)
2668
+ ) {
2669
+ throw new ElectricAgentsError(
2670
+ ErrCodeUnauthorized,
2671
+ `Only built-in system principals can update principal identity`,
2672
+ 403
2673
+ )
2674
+ }
2675
+
2494
2676
  if (req.payload === undefined) {
2495
2677
  throw new ElectricAgentsError(
2496
2678
  ErrCodeInvalidRequest,
@@ -415,6 +415,30 @@ export class PostgresRegistry {
415
415
  })
416
416
  }
417
417
 
418
+ async ensureEntityType(
419
+ et: ElectricAgentsEntityType
420
+ ): Promise<ElectricAgentsEntityType> {
421
+ const existing = await this.getEntityType(et.name)
422
+ if (existing) return existing
423
+ await this.db
424
+ .insert(entityTypes)
425
+ .values({
426
+ tenantId: this.tenantId,
427
+ name: et.name,
428
+ description: et.description,
429
+ creationSchema: et.creation_schema ?? null,
430
+ inboxSchemas: et.inbox_schemas ?? null,
431
+ stateSchemas: et.state_schemas ?? null,
432
+ serveEndpoint: et.serve_endpoint ?? null,
433
+ defaultDispatchPolicy: et.default_dispatch_policy ?? null,
434
+ revision: et.revision,
435
+ createdAt: et.created_at,
436
+ updatedAt: et.updated_at,
437
+ })
438
+ .onConflictDoNothing()
439
+ return (await this.getEntityType(et.name))!
440
+ }
441
+
418
442
  async getEntityType(name: string): Promise<ElectricAgentsEntityType | null> {
419
443
  const rows = await this.db
420
444
  .select()
@@ -471,6 +495,7 @@ export class PostgresRegistry {
471
495
  tagsIndex: buildTagsIndex(entity.tags),
472
496
  spawnArgs: entity.spawn_args ?? {},
473
497
  parent: entity.parent ?? null,
498
+ createdBy: entity.created_by ?? null,
474
499
  typeRevision: entity.type_revision ?? null,
475
500
  inboxSchemas: entity.inbox_schemas ?? null,
476
501
  stateSchemas: entity.state_schemas ?? null,
@@ -544,11 +569,14 @@ export class PostgresRegistry {
544
569
  parent?: string
545
570
  limit?: number
546
571
  offset?: number
572
+ created_by?: string
547
573
  }): Promise<{ entities: Array<ElectricAgentsEntity>; total: number }> {
548
574
  const conditions = [eq(entities.tenantId, this.tenantId)]
549
575
  if (filter?.type) conditions.push(eq(entities.type, filter.type))
550
576
  if (filter?.status) conditions.push(eq(entities.status, filter.status))
551
577
  if (filter?.parent) conditions.push(eq(entities.parent, filter.parent))
578
+ if (filter?.created_by)
579
+ conditions.push(eq(entities.createdBy, filter.created_by))
552
580
 
553
581
  const whereClause = and(...conditions)
554
582
 
@@ -1054,6 +1082,7 @@ export class PostgresRegistry {
1054
1082
  tags: (row.tags as EntityTags | null | undefined) ?? {},
1055
1083
  spawn_args: row.spawnArgs as Record<string, unknown> | undefined,
1056
1084
  parent: row.parent ?? undefined,
1085
+ created_by: row.createdBy ?? undefined,
1057
1086
  type_revision: row.typeRevision ?? undefined,
1058
1087
  inbox_schemas: row.inboxSchemas as
1059
1088
  | Record<string, Record<string, unknown>>