@electric-ax/agents-server 0.4.15 → 0.4.17
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/dist/entrypoint.js +1230 -235
- package/dist/index.cjs +1233 -229
- package/dist/index.d.cts +1319 -318
- package/dist/index.d.ts +1319 -318
- package/dist/index.js +1235 -231
- package/drizzle/0011_entity_permissions.sql +100 -0
- package/drizzle/0012_horton_user_manage_permission.sql +25 -0
- package/drizzle/0013_worker_user_manage_permission.sql +25 -0
- package/drizzle/0014_entity_type_slash_commands.sql +1 -0
- package/drizzle/meta/_journal.json +28 -0
- package/package.json +7 -7
- package/src/db/schema.ts +199 -0
- package/src/electric-agents-types.ts +80 -2
- package/src/entity-bridge-manager.ts +57 -6
- package/src/entity-manager.ts +124 -61
- package/src/entity-projector.ts +76 -17
- package/src/entity-registry.ts +615 -5
- package/src/index.ts +11 -0
- package/src/permissions.ts +239 -0
- package/src/routing/context.ts +2 -0
- package/src/routing/durable-streams-router.ts +125 -4
- package/src/routing/electric-proxy-router.ts +4 -0
- package/src/routing/entities-router.ts +347 -20
- package/src/routing/entity-types-router.ts +267 -15
- package/src/routing/hooks.ts +1 -0
- package/src/routing/observations-router.ts +2 -1
- package/src/runtime.ts +34 -0
- package/src/scheduler.ts +2 -0
- package/src/server.ts +5 -0
- package/src/utils/server-utils.ts +192 -12
- package/src/wake-registry.ts +8 -0
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
assertTags,
|
|
4
4
|
buildTagsIndex,
|
|
5
5
|
getEntitiesStreamPath,
|
|
6
|
+
hashString,
|
|
6
7
|
normalizeTags,
|
|
7
8
|
sourceRefForTags,
|
|
8
9
|
} from '@electric-ax/agents-runtime'
|
|
@@ -13,7 +14,9 @@ import {
|
|
|
13
14
|
} from '@electric-sql/client'
|
|
14
15
|
import { serverLog } from './utils/log.js'
|
|
15
16
|
import { electricUrlWithPath } from './utils/electric-url.js'
|
|
17
|
+
import { buildReadableEntitiesWhere } from './utils/server-utils.js'
|
|
16
18
|
import { DEFAULT_TENANT_ID } from './tenant.js'
|
|
19
|
+
import { isBuiltInSystemPrincipalUrl } from './principal.js'
|
|
17
20
|
import type { EntityBridgeRow, PostgresRegistry } from './entity-registry.js'
|
|
18
21
|
import type { StreamClient } from './stream-client.js'
|
|
19
22
|
import type {
|
|
@@ -30,7 +33,11 @@ import type {
|
|
|
30
33
|
export interface EntityBridgeCoordinator {
|
|
31
34
|
start(): Promise<void>
|
|
32
35
|
stop(): Promise<void>
|
|
33
|
-
register(
|
|
36
|
+
register(
|
|
37
|
+
tagsInput: unknown,
|
|
38
|
+
principalUrl: string,
|
|
39
|
+
principalKind: string
|
|
40
|
+
): Promise<{
|
|
34
41
|
sourceRef: string
|
|
35
42
|
streamUrl: string
|
|
36
43
|
}>
|
|
@@ -115,19 +122,44 @@ function sqlStringLiteral(value: string): string {
|
|
|
115
122
|
|
|
116
123
|
function buildTenantTagsWhereClause(
|
|
117
124
|
tenantId: string,
|
|
118
|
-
tags: EntityTags
|
|
125
|
+
tags: EntityTags,
|
|
126
|
+
principalUrl?: string,
|
|
127
|
+
principalKind?: string,
|
|
128
|
+
permissionBypass?: boolean
|
|
119
129
|
): string {
|
|
120
|
-
|
|
130
|
+
const readableWhere =
|
|
131
|
+
principalUrl && principalKind
|
|
132
|
+
? buildReadableEntitiesWhere({
|
|
133
|
+
tenantId,
|
|
134
|
+
principalUrl,
|
|
135
|
+
principalKind,
|
|
136
|
+
permissionBypass,
|
|
137
|
+
})
|
|
138
|
+
: `tenant_id = ${sqlStringLiteral(tenantId)} AND FALSE`
|
|
139
|
+
return `${readableWhere} AND (${buildTagsWhereClause(tags)})`
|
|
121
140
|
}
|
|
122
141
|
|
|
123
142
|
function shapeEntityKey(message: ChangeMessage<EntityShapeRow>): string {
|
|
124
143
|
return message.value.url
|
|
125
144
|
}
|
|
126
145
|
|
|
146
|
+
function principalScopedSourceRef(
|
|
147
|
+
tagSourceRef: string,
|
|
148
|
+
principalUrl: string,
|
|
149
|
+
principalKind: string
|
|
150
|
+
): string {
|
|
151
|
+
return `${tagSourceRef}-${hashString(
|
|
152
|
+
JSON.stringify({ principalKind, principalUrl })
|
|
153
|
+
)}`
|
|
154
|
+
}
|
|
155
|
+
|
|
127
156
|
class EntityBridge {
|
|
128
157
|
readonly sourceRef: string
|
|
129
158
|
readonly tags: EntityTags
|
|
130
159
|
readonly streamUrl: string
|
|
160
|
+
private readonly principalUrl?: string
|
|
161
|
+
private readonly principalKind?: string
|
|
162
|
+
private readonly permissionBypass: boolean
|
|
131
163
|
|
|
132
164
|
private currentMembers = new Map<string, EntityMembershipRow>()
|
|
133
165
|
private producer: IdempotentProducer | null = null
|
|
@@ -152,6 +184,9 @@ class EntityBridge {
|
|
|
152
184
|
this.sourceRef = row.sourceRef
|
|
153
185
|
this.tags = normalizeTags(row.tags)
|
|
154
186
|
this.streamUrl = row.streamUrl
|
|
187
|
+
this.principalUrl = row.principalUrl
|
|
188
|
+
this.principalKind = row.principalKind
|
|
189
|
+
this.permissionBypass = isBuiltInSystemPrincipalUrl(row.principalUrl)
|
|
155
190
|
this.initialShapeHandle = row.shapeHandle
|
|
156
191
|
this.initialShapeOffset = row.shapeOffset
|
|
157
192
|
}
|
|
@@ -316,7 +351,13 @@ class EntityBridge {
|
|
|
316
351
|
url: electricUrlWithPath(this.electricUrl, `/v1/shape`).toString(),
|
|
317
352
|
params: {
|
|
318
353
|
table: `entities`,
|
|
319
|
-
where: buildTenantTagsWhereClause(
|
|
354
|
+
where: buildTenantTagsWhereClause(
|
|
355
|
+
this.tenantId,
|
|
356
|
+
this.tags,
|
|
357
|
+
this.principalUrl,
|
|
358
|
+
this.principalKind,
|
|
359
|
+
this.permissionBypass
|
|
360
|
+
),
|
|
320
361
|
...(this.electricSecret ? { secret: this.electricSecret } : {}),
|
|
321
362
|
columns: [...ENTITY_SHAPE_COLUMNS],
|
|
322
363
|
replica: `full`,
|
|
@@ -564,7 +605,11 @@ export class EntityBridgeManager implements EntityBridgeCoordinator {
|
|
|
564
605
|
)
|
|
565
606
|
}
|
|
566
607
|
|
|
567
|
-
async register(
|
|
608
|
+
async register(
|
|
609
|
+
tagsInput: unknown,
|
|
610
|
+
principalUrl: string,
|
|
611
|
+
principalKind: string
|
|
612
|
+
): Promise<{
|
|
568
613
|
sourceRef: string
|
|
569
614
|
streamUrl: string
|
|
570
615
|
}> {
|
|
@@ -573,13 +618,19 @@ export class EntityBridgeManager implements EntityBridgeCoordinator {
|
|
|
573
618
|
}
|
|
574
619
|
|
|
575
620
|
const tags = normalizeTags(assertTags(tagsInput))
|
|
576
|
-
const sourceRef =
|
|
621
|
+
const sourceRef = principalScopedSourceRef(
|
|
622
|
+
sourceRefForTags(tags),
|
|
623
|
+
principalUrl,
|
|
624
|
+
principalKind
|
|
625
|
+
)
|
|
577
626
|
const streamUrl = getEntitiesStreamPath(sourceRef)
|
|
578
627
|
|
|
579
628
|
const row = await this.registry.upsertEntityBridge({
|
|
580
629
|
sourceRef,
|
|
581
630
|
tags,
|
|
582
631
|
streamUrl,
|
|
632
|
+
principalUrl,
|
|
633
|
+
principalKind,
|
|
583
634
|
})
|
|
584
635
|
await this.registry.touchEntityBridge(sourceRef)
|
|
585
636
|
await this.ensureBridge(row)
|
package/src/entity-manager.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto'
|
|
2
2
|
import fastq from 'fastq'
|
|
3
3
|
import {
|
|
4
|
+
COMPOSER_INPUT_MESSAGE_TYPE,
|
|
4
5
|
assertTags,
|
|
5
6
|
entityStateSchema,
|
|
6
7
|
getCronStreamPath,
|
|
@@ -11,6 +12,8 @@ import {
|
|
|
11
12
|
manifestSharedStateKey,
|
|
12
13
|
manifestSourceKey,
|
|
13
14
|
resolveCronScheduleSpec,
|
|
15
|
+
validateComposerInputPayload,
|
|
16
|
+
validateSlashCommandDefinitions,
|
|
14
17
|
} from '@electric-ax/agents-runtime'
|
|
15
18
|
import type { EventPointer } from '@electric-ax/agents-runtime'
|
|
16
19
|
import {
|
|
@@ -73,7 +76,6 @@ import type { EntityBridgeCoordinator } from './entity-bridge-manager.js'
|
|
|
73
76
|
import type { Principal } from './principal.js'
|
|
74
77
|
|
|
75
78
|
type SpawnPersistResult = [
|
|
76
|
-
PromiseSettledResult<void>,
|
|
77
79
|
PromiseSettledResult<void>,
|
|
78
80
|
PromiseSettledResult<number>,
|
|
79
81
|
]
|
|
@@ -163,14 +165,14 @@ type ForkSubtreeOptions = {
|
|
|
163
165
|
rootInstanceId?: string
|
|
164
166
|
waitTimeoutMs?: number
|
|
165
167
|
waitPollMs?: number
|
|
168
|
+
createdBy?: string
|
|
166
169
|
/**
|
|
167
170
|
* Optional anchor pointing at an event on the source root's `main` stream.
|
|
168
171
|
* When set: only events at or before the pointer are kept on the root's
|
|
169
172
|
* forked `main`, and the root's manifest is filtered so that descendants
|
|
170
173
|
* spawned after the pointer are dropped from the fork (their now-orphan
|
|
171
174
|
* subtrees are not forked). The pointer applies only to the root's
|
|
172
|
-
* `main` stream
|
|
173
|
-
* regardless.
|
|
175
|
+
* `main` stream; shared-state streams clone at HEAD regardless.
|
|
174
176
|
*/
|
|
175
177
|
forkPointer?: EventPointer
|
|
176
178
|
}
|
|
@@ -431,6 +433,7 @@ export class EntityManager {
|
|
|
431
433
|
this.validateSchema(req.creation_schema)
|
|
432
434
|
this.validateSchemaMap(req.inbox_schemas)
|
|
433
435
|
this.validateSchemaMap(req.state_schemas)
|
|
436
|
+
this.validateSlashCommands(req.slash_commands)
|
|
434
437
|
const defaultDispatchPolicy = req.default_dispatch_policy
|
|
435
438
|
? this.validateDispatchPolicy(req.default_dispatch_policy, {
|
|
436
439
|
label: `default_dispatch_policy`,
|
|
@@ -445,6 +448,7 @@ export class EntityManager {
|
|
|
445
448
|
creation_schema: req.creation_schema,
|
|
446
449
|
inbox_schemas: req.inbox_schemas,
|
|
447
450
|
state_schemas: req.state_schemas,
|
|
451
|
+
slash_commands: req.slash_commands,
|
|
448
452
|
serve_endpoint: req.serve_endpoint,
|
|
449
453
|
default_dispatch_policy: defaultDispatchPolicy,
|
|
450
454
|
revision: existing ? existing.revision + 1 : 1,
|
|
@@ -497,7 +501,10 @@ export class EntityManager {
|
|
|
497
501
|
|
|
498
502
|
async ensurePrincipal(principal: Principal): Promise<ElectricAgentsEntity> {
|
|
499
503
|
const existing = await this.registry.getEntity(principal.url)
|
|
500
|
-
if (existing)
|
|
504
|
+
if (existing) {
|
|
505
|
+
await this.ensureUserPrincipal(principal)
|
|
506
|
+
return existing
|
|
507
|
+
}
|
|
501
508
|
await this.ensurePrincipalEntityType()
|
|
502
509
|
try {
|
|
503
510
|
const entity = await this.spawn(`principal`, {
|
|
@@ -522,6 +529,7 @@ export class EntityManager {
|
|
|
522
529
|
},
|
|
523
530
|
})
|
|
524
531
|
)
|
|
532
|
+
await this.ensureUserPrincipal(principal)
|
|
525
533
|
return entity
|
|
526
534
|
} catch (error) {
|
|
527
535
|
if (
|
|
@@ -529,12 +537,21 @@ export class EntityManager {
|
|
|
529
537
|
error.code === ErrCodeDuplicateURL
|
|
530
538
|
) {
|
|
531
539
|
const raced = await this.registry.getEntity(principal.url)
|
|
532
|
-
if (raced)
|
|
540
|
+
if (raced) {
|
|
541
|
+
await this.ensureUserPrincipal(principal)
|
|
542
|
+
return raced
|
|
543
|
+
}
|
|
533
544
|
}
|
|
534
545
|
throw error
|
|
535
546
|
}
|
|
536
547
|
}
|
|
537
548
|
|
|
549
|
+
private async ensureUserPrincipal(principal: Principal): Promise<void> {
|
|
550
|
+
if (principal.kind === `user`) {
|
|
551
|
+
await this.registry.ensureUserForPrincipal(principal)
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
538
555
|
// ==========================================================================
|
|
539
556
|
// Spawn
|
|
540
557
|
// ==========================================================================
|
|
@@ -624,7 +641,6 @@ export class EntityManager {
|
|
|
624
641
|
? principalUrl(instanceId)
|
|
625
642
|
: `/${typeName}/${instanceId}`
|
|
626
643
|
const mainPath = `${entityURL}/main`
|
|
627
|
-
const errorPath = `${entityURL}/error`
|
|
628
644
|
|
|
629
645
|
const subscriptionId = `${typeName}-handler`
|
|
630
646
|
|
|
@@ -676,7 +692,6 @@ export class EntityManager {
|
|
|
676
692
|
url: entityURL,
|
|
677
693
|
streams: {
|
|
678
694
|
main: mainPath,
|
|
679
|
-
error: errorPath,
|
|
680
695
|
},
|
|
681
696
|
subscription_id: subscriptionId,
|
|
682
697
|
dispatch_policy: dispatchPolicy,
|
|
@@ -725,6 +740,19 @@ export class EntityManager {
|
|
|
725
740
|
createdEvent as Record<string, unknown>,
|
|
726
741
|
]
|
|
727
742
|
|
|
743
|
+
const slashCommandTimestamp = new Date().toISOString()
|
|
744
|
+
for (const command of entityType.slash_commands ?? []) {
|
|
745
|
+
const slashCommandEvent = entityStateSchema.slashCommands.insert({
|
|
746
|
+
key: command.name,
|
|
747
|
+
value: {
|
|
748
|
+
...command,
|
|
749
|
+
source: `static`,
|
|
750
|
+
updated_at: slashCommandTimestamp,
|
|
751
|
+
},
|
|
752
|
+
} as any)
|
|
753
|
+
initialEvents.push(slashCommandEvent as Record<string, unknown>)
|
|
754
|
+
}
|
|
755
|
+
|
|
728
756
|
if (req.initialMessage !== undefined) {
|
|
729
757
|
const msgNow = new Date().toISOString()
|
|
730
758
|
const inboxEvent = entityStateSchema.inbox.insert({
|
|
@@ -732,6 +760,7 @@ export class EntityManager {
|
|
|
732
760
|
value: {
|
|
733
761
|
from: req.created_by ?? req.parent ?? `spawn`,
|
|
734
762
|
payload: req.initialMessage,
|
|
763
|
+
message_type: req.initialMessageType,
|
|
735
764
|
timestamp: msgNow,
|
|
736
765
|
},
|
|
737
766
|
} as any)
|
|
@@ -745,8 +774,8 @@ export class EntityManager {
|
|
|
745
774
|
const queueEnterT0 = performance.now()
|
|
746
775
|
const queueWaiting = this.spawnPersistQueue.length()
|
|
747
776
|
const queueRunning = this.spawnPersistQueue.running()
|
|
748
|
-
const [mainStreamResult,
|
|
749
|
-
|
|
777
|
+
const [mainStreamResult, entityResult] = await this.spawnPersistQueue.push(
|
|
778
|
+
async () => {
|
|
750
779
|
// Create entity first so it's visible in the DB before stream
|
|
751
780
|
// creation can trigger webhooks that look up the entity.
|
|
752
781
|
let entityTxid: number
|
|
@@ -756,41 +785,34 @@ export class EntityManager {
|
|
|
756
785
|
)
|
|
757
786
|
} catch (err) {
|
|
758
787
|
return [
|
|
759
|
-
{ status: `fulfilled`, value: undefined },
|
|
760
788
|
{ status: `fulfilled`, value: undefined },
|
|
761
789
|
{ status: `rejected`, reason: err },
|
|
762
790
|
] as SpawnPersistResult
|
|
763
791
|
}
|
|
764
792
|
|
|
765
|
-
const [mainStreamResult
|
|
793
|
+
const [mainStreamResult] = await Promise.allSettled([
|
|
766
794
|
this.streamClient.create(mainPath, {
|
|
767
795
|
contentType,
|
|
768
796
|
body: initialBody,
|
|
769
797
|
}),
|
|
770
|
-
this.streamClient.create(errorPath, { contentType }),
|
|
771
798
|
])
|
|
772
799
|
|
|
773
800
|
return [
|
|
774
801
|
mainStreamResult,
|
|
775
|
-
errorStreamResult,
|
|
776
802
|
{ status: `fulfilled`, value: entityTxid },
|
|
777
803
|
] as SpawnPersistResult
|
|
778
|
-
}
|
|
804
|
+
}
|
|
805
|
+
)
|
|
779
806
|
const parallelMs = +(performance.now() - queueEnterT0).toFixed(2)
|
|
780
807
|
|
|
781
808
|
if (
|
|
782
809
|
mainStreamResult.status === `rejected` ||
|
|
783
|
-
errorStreamResult.status === `rejected` ||
|
|
784
810
|
entityResult.status === `rejected`
|
|
785
811
|
) {
|
|
786
812
|
const entityReason =
|
|
787
813
|
entityResult.status === `rejected` ? entityResult.reason : null
|
|
788
814
|
const streamReason =
|
|
789
|
-
mainStreamResult.status === `rejected`
|
|
790
|
-
? mainStreamResult.reason
|
|
791
|
-
: errorStreamResult.status === `rejected`
|
|
792
|
-
? errorStreamResult.reason
|
|
793
|
-
: null
|
|
815
|
+
mainStreamResult.status === `rejected` ? mainStreamResult.reason : null
|
|
794
816
|
const isDuplicate = entityReason instanceof EntityAlreadyExistsError
|
|
795
817
|
const isStreamConflict =
|
|
796
818
|
!!streamReason &&
|
|
@@ -805,9 +827,6 @@ export class EntityManager {
|
|
|
805
827
|
if (mainStreamResult.status === `fulfilled`) {
|
|
806
828
|
rollbacks.push(this.streamClient.delete(mainPath))
|
|
807
829
|
}
|
|
808
|
-
if (errorStreamResult.status === `fulfilled`) {
|
|
809
|
-
rollbacks.push(this.streamClient.delete(errorPath))
|
|
810
|
-
}
|
|
811
830
|
if (entityResult.status === `fulfilled`) {
|
|
812
831
|
rollbacks.push(this.registry.deleteEntity(entityURL))
|
|
813
832
|
}
|
|
@@ -834,9 +853,7 @@ export class EntityManager {
|
|
|
834
853
|
const failure =
|
|
835
854
|
mainStreamResult.status === `rejected`
|
|
836
855
|
? mainStreamResult.reason
|
|
837
|
-
:
|
|
838
|
-
? errorStreamResult.reason
|
|
839
|
-
: (entityResult as PromiseRejectedResult).reason
|
|
856
|
+
: (entityResult as PromiseRejectedResult).reason
|
|
840
857
|
if (failure instanceof Error) throw failure
|
|
841
858
|
throw new ElectricAgentsError(
|
|
842
859
|
`SPAWN_FAILED`,
|
|
@@ -1045,7 +1062,8 @@ export class EntityManager {
|
|
|
1045
1062
|
const entityPlans = this.buildForkEntityPlans(
|
|
1046
1063
|
effectiveSubtree,
|
|
1047
1064
|
entityUrlMap,
|
|
1048
|
-
stringMap
|
|
1065
|
+
stringMap,
|
|
1066
|
+
opts.createdBy
|
|
1049
1067
|
)
|
|
1050
1068
|
|
|
1051
1069
|
this.addForkLocks(
|
|
@@ -1077,13 +1095,6 @@ export class EntityManager {
|
|
|
1077
1095
|
: undefined
|
|
1078
1096
|
)
|
|
1079
1097
|
createdStreams.push(plan.fork.streams.main)
|
|
1080
|
-
// `error` always clones at HEAD — no canonical mapping
|
|
1081
|
-
// between main-offset and error-offset.
|
|
1082
|
-
await this.streamClient.fork(
|
|
1083
|
-
plan.fork.streams.error,
|
|
1084
|
-
plan.source.streams.error
|
|
1085
|
-
)
|
|
1086
|
-
createdStreams.push(plan.fork.streams.error)
|
|
1087
1098
|
}
|
|
1088
1099
|
|
|
1089
1100
|
for (const [sourceId, forkId] of sharedStateIdMap) {
|
|
@@ -1711,7 +1722,6 @@ export class EntityManager {
|
|
|
1711
1722
|
for (const [sourceUrl, forkUrl] of entityUrlMap) {
|
|
1712
1723
|
stringMap.set(sourceUrl, forkUrl)
|
|
1713
1724
|
stringMap.set(`${sourceUrl}/main`, `${forkUrl}/main`)
|
|
1714
|
-
stringMap.set(`${sourceUrl}/error`, `${forkUrl}/error`)
|
|
1715
1725
|
}
|
|
1716
1726
|
for (const [sourceId, forkId] of sharedStateIdMap) {
|
|
1717
1727
|
stringMap.set(sourceId, forkId)
|
|
@@ -1726,7 +1736,8 @@ export class EntityManager {
|
|
|
1726
1736
|
private buildForkEntityPlans(
|
|
1727
1737
|
entitiesToFork: Array<ElectricAgentsEntity>,
|
|
1728
1738
|
entityUrlMap: Map<string, string>,
|
|
1729
|
-
stringMap: Map<string, string
|
|
1739
|
+
stringMap: Map<string, string>,
|
|
1740
|
+
createdBy?: string
|
|
1730
1741
|
): Array<ForkEntityPlan> {
|
|
1731
1742
|
const now = Date.now()
|
|
1732
1743
|
return entitiesToFork.map((source) => {
|
|
@@ -1750,12 +1761,12 @@ export class EntityManager {
|
|
|
1750
1761
|
status: `idle`,
|
|
1751
1762
|
streams: {
|
|
1752
1763
|
main: `${forkUrl}/main`,
|
|
1753
|
-
error: `${forkUrl}/error`,
|
|
1754
1764
|
},
|
|
1755
1765
|
subscription_id: `${type}-handler`,
|
|
1756
1766
|
write_token: randomUUID(),
|
|
1757
1767
|
spawn_args: spawnArgs,
|
|
1758
1768
|
parent,
|
|
1769
|
+
created_by: createdBy ?? source.created_by,
|
|
1759
1770
|
created_at: now,
|
|
1760
1771
|
updated_at: now,
|
|
1761
1772
|
}
|
|
@@ -2047,12 +2058,7 @@ export class EntityManager {
|
|
|
2047
2058
|
manifests: Map<string, Record<string, unknown>>
|
|
2048
2059
|
): Promise<void> {
|
|
2049
2060
|
for (const [manifestKey, manifest] of manifests) {
|
|
2050
|
-
await this.
|
|
2051
|
-
entityUrl,
|
|
2052
|
-
manifestKey,
|
|
2053
|
-
`upsert`,
|
|
2054
|
-
manifest
|
|
2055
|
-
)
|
|
2061
|
+
await this.syncManifestLinks(entityUrl, manifestKey, `upsert`, manifest)
|
|
2056
2062
|
|
|
2057
2063
|
const wake = buildManifestWakeRegistration(
|
|
2058
2064
|
entityUrl,
|
|
@@ -2125,6 +2131,7 @@ export class EntityManager {
|
|
|
2125
2131
|
{
|
|
2126
2132
|
entityUrl: targetUrl,
|
|
2127
2133
|
from: senderUrl,
|
|
2134
|
+
from_agent: senderUrl,
|
|
2128
2135
|
payload: manifest.payload,
|
|
2129
2136
|
key: `scheduled-${producerId}`,
|
|
2130
2137
|
type:
|
|
@@ -2191,7 +2198,7 @@ export class EntityManager {
|
|
|
2191
2198
|
`msg-in-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
2192
2199
|
|
|
2193
2200
|
const value: Record<string, unknown> = {
|
|
2194
|
-
from: req.from,
|
|
2201
|
+
from: req.from_principal ?? req.from,
|
|
2195
2202
|
payload: req.payload,
|
|
2196
2203
|
timestamp: now,
|
|
2197
2204
|
mode: req.mode ?? `immediate`,
|
|
@@ -2200,6 +2207,12 @@ export class EntityManager {
|
|
|
2200
2207
|
? `pending`
|
|
2201
2208
|
: `processed`,
|
|
2202
2209
|
}
|
|
2210
|
+
if (req.from_principal) {
|
|
2211
|
+
value.from_principal = req.from_principal
|
|
2212
|
+
}
|
|
2213
|
+
if (req.from_agent) {
|
|
2214
|
+
value.from_agent = req.from_agent
|
|
2215
|
+
}
|
|
2203
2216
|
if (req.type) {
|
|
2204
2217
|
value.message_type = req.type
|
|
2205
2218
|
}
|
|
@@ -2610,14 +2623,21 @@ export class EntityManager {
|
|
|
2610
2623
|
return updated
|
|
2611
2624
|
}
|
|
2612
2625
|
|
|
2613
|
-
async ensureEntitiesMembershipStream(
|
|
2626
|
+
async ensureEntitiesMembershipStream(
|
|
2627
|
+
tags: Record<string, string>,
|
|
2628
|
+
principal: { url: string; kind: string }
|
|
2629
|
+
): Promise<{
|
|
2614
2630
|
sourceRef: string
|
|
2615
2631
|
streamUrl: string
|
|
2616
2632
|
}> {
|
|
2617
2633
|
if (!this.entityBridgeManager) {
|
|
2618
2634
|
throw new Error(`Entity bridge manager not configured`)
|
|
2619
2635
|
}
|
|
2620
|
-
return this.entityBridgeManager.register(
|
|
2636
|
+
return this.entityBridgeManager.register(
|
|
2637
|
+
this.validateTags(tags),
|
|
2638
|
+
principal.url,
|
|
2639
|
+
principal.kind
|
|
2640
|
+
)
|
|
2621
2641
|
}
|
|
2622
2642
|
|
|
2623
2643
|
async writeManifestEntry(
|
|
@@ -2650,12 +2670,12 @@ export class EntityManager {
|
|
|
2650
2670
|
await this.streamClient.appendIdempotent(entity.streams.main, encoded, {
|
|
2651
2671
|
producerId: opts.producerId,
|
|
2652
2672
|
})
|
|
2653
|
-
await this.
|
|
2673
|
+
await this.syncManifestLinks(entityUrl, key, operation, value)
|
|
2654
2674
|
return
|
|
2655
2675
|
}
|
|
2656
2676
|
|
|
2657
2677
|
await this.streamClient.append(entity.streams.main, encoded)
|
|
2658
|
-
await this.
|
|
2678
|
+
await this.syncManifestLinks(entityUrl, key, operation, value)
|
|
2659
2679
|
}
|
|
2660
2680
|
|
|
2661
2681
|
async upsertCronSchedule(
|
|
@@ -2950,6 +2970,8 @@ export class EntityManager {
|
|
|
2950
2970
|
{
|
|
2951
2971
|
entityUrl,
|
|
2952
2972
|
from: req.from,
|
|
2973
|
+
from_principal: req.from_principal,
|
|
2974
|
+
from_agent: req.from_agent,
|
|
2953
2975
|
payload: req.payload,
|
|
2954
2976
|
key: req.key,
|
|
2955
2977
|
type: req.type,
|
|
@@ -3031,7 +3053,7 @@ export class EntityManager {
|
|
|
3031
3053
|
})
|
|
3032
3054
|
}
|
|
3033
3055
|
|
|
3034
|
-
private async
|
|
3056
|
+
private async syncManifestLinks(
|
|
3035
3057
|
entityUrl: string,
|
|
3036
3058
|
manifestKey: string,
|
|
3037
3059
|
operation: `insert` | `update` | `upsert` | `delete`,
|
|
@@ -3044,6 +3066,14 @@ export class EntityManager {
|
|
|
3044
3066
|
manifestKey,
|
|
3045
3067
|
sourceRef
|
|
3046
3068
|
)
|
|
3069
|
+
|
|
3070
|
+
const sharedStateId =
|
|
3071
|
+
operation === `delete` ? undefined : this.extractSharedStateId(value)
|
|
3072
|
+
await this.registry.replaceSharedStateLink(
|
|
3073
|
+
entityUrl,
|
|
3074
|
+
manifestKey,
|
|
3075
|
+
sharedStateId
|
|
3076
|
+
)
|
|
3047
3077
|
}
|
|
3048
3078
|
|
|
3049
3079
|
private extractEntitiesSourceRef(
|
|
@@ -3059,6 +3089,24 @@ export class EntityManager {
|
|
|
3059
3089
|
return undefined
|
|
3060
3090
|
}
|
|
3061
3091
|
|
|
3092
|
+
private extractSharedStateId(
|
|
3093
|
+
manifest?: Record<string, unknown>
|
|
3094
|
+
): string | undefined {
|
|
3095
|
+
if (manifest?.kind === `shared-state` && typeof manifest.id === `string`) {
|
|
3096
|
+
return manifest.id
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
if (manifest?.kind !== `source` || manifest.sourceType !== `db`) {
|
|
3100
|
+
return undefined
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
if (typeof manifest.sourceRef === `string`) {
|
|
3104
|
+
return manifest.sourceRef
|
|
3105
|
+
}
|
|
3106
|
+
const config = isRecord(manifest.config) ? manifest.config : undefined
|
|
3107
|
+
return typeof config?.id === `string` ? config.id : undefined
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3062
3110
|
/**
|
|
3063
3111
|
* Read a child entity's stream and extract concatenated text deltas
|
|
3064
3112
|
* for a specific run, plus any error messages for that run.
|
|
@@ -3334,19 +3382,8 @@ export class EntityManager {
|
|
|
3334
3382
|
return
|
|
3335
3383
|
}
|
|
3336
3384
|
|
|
3337
|
-
const errorCloseEvent = {
|
|
3338
|
-
type: `signal`,
|
|
3339
|
-
key: signalEvent.key,
|
|
3340
|
-
value: signalEvent.value,
|
|
3341
|
-
headers: signalEvent.headers,
|
|
3342
|
-
}
|
|
3343
|
-
const errorSignalData = this.encodeChangeEvent(
|
|
3344
|
-
errorCloseEvent as unknown as Record<string, unknown>
|
|
3345
|
-
)
|
|
3346
|
-
|
|
3347
3385
|
for (const [streamPath, data] of [
|
|
3348
3386
|
[entity.streams.main, signalData],
|
|
3349
|
-
[entity.streams.error, errorSignalData],
|
|
3350
3387
|
] as const) {
|
|
3351
3388
|
try {
|
|
3352
3389
|
await this.streamClient.append(streamPath, data, { close: true })
|
|
@@ -3494,7 +3531,9 @@ export class EntityManager {
|
|
|
3494
3531
|
creation_schema: existing.creation_schema,
|
|
3495
3532
|
inbox_schemas: mergedInbox,
|
|
3496
3533
|
state_schemas: mergedState,
|
|
3534
|
+
slash_commands: existing.slash_commands,
|
|
3497
3535
|
serve_endpoint: existing.serve_endpoint,
|
|
3536
|
+
default_dispatch_policy: existing.default_dispatch_policy,
|
|
3498
3537
|
revision: nextRevision,
|
|
3499
3538
|
created_at: existing.created_at,
|
|
3500
3539
|
updated_at: now,
|
|
@@ -3583,6 +3622,20 @@ export class EntityManager {
|
|
|
3583
3622
|
}
|
|
3584
3623
|
}
|
|
3585
3624
|
|
|
3625
|
+
private validateSlashCommands(input: unknown): void {
|
|
3626
|
+
const validationError = validateSlashCommandDefinitions(input)
|
|
3627
|
+
if (!validationError) {
|
|
3628
|
+
return
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3631
|
+
throw new ElectricAgentsError(
|
|
3632
|
+
ErrCodeSchemaValidationFailed,
|
|
3633
|
+
validationError.message,
|
|
3634
|
+
422,
|
|
3635
|
+
validationError.details
|
|
3636
|
+
)
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3586
3639
|
private async validateSendRequest(
|
|
3587
3640
|
entityUrl: string,
|
|
3588
3641
|
req: SendRequest
|
|
@@ -3599,7 +3652,17 @@ export class EntityManager {
|
|
|
3599
3652
|
)
|
|
3600
3653
|
}
|
|
3601
3654
|
|
|
3602
|
-
if (req.type
|
|
3655
|
+
if (req.type === COMPOSER_INPUT_MESSAGE_TYPE) {
|
|
3656
|
+
const valErr = validateComposerInputPayload(req.payload)
|
|
3657
|
+
if (valErr) {
|
|
3658
|
+
throw new ElectricAgentsError(
|
|
3659
|
+
ErrCodeSchemaValidationFailed,
|
|
3660
|
+
valErr.message,
|
|
3661
|
+
422,
|
|
3662
|
+
valErr.details
|
|
3663
|
+
)
|
|
3664
|
+
}
|
|
3665
|
+
} else if (req.type && entity.type) {
|
|
3603
3666
|
const { inboxSchemas } = await this.getEffectiveSchemas(entity)
|
|
3604
3667
|
if (inboxSchemas) {
|
|
3605
3668
|
const schema = inboxSchemas[req.type]
|