@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
package/src/entity-projector.ts
CHANGED
|
@@ -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'
|
|
@@ -15,6 +16,7 @@ import { PostgresRegistry } from './entity-registry.js'
|
|
|
15
16
|
import { electricUrlWithPath } from './utils/electric-url.js'
|
|
16
17
|
import { serverLog } from './utils/log.js'
|
|
17
18
|
import { isUnregisteredTenantError } from './tenant.js'
|
|
19
|
+
import { isBuiltInSystemPrincipalUrl } from './principal.js'
|
|
18
20
|
import type { DrizzleDB } from './db/index.js'
|
|
19
21
|
import type { EntityBridgeCoordinator } from './entity-bridge-manager.js'
|
|
20
22
|
import type { EntityBridgeRow } from './entity-registry.js'
|
|
@@ -37,6 +39,7 @@ interface EntityShapeRow extends Row<unknown> {
|
|
|
37
39
|
type: string
|
|
38
40
|
status: `spawning` | `running` | `idle` | `stopped`
|
|
39
41
|
tags: EntityTags
|
|
42
|
+
created_by?: string | null
|
|
40
43
|
spawn_args?: Record<string, unknown> | null
|
|
41
44
|
sandbox?: { profile: string } | null
|
|
42
45
|
parent?: string | null
|
|
@@ -53,6 +56,7 @@ const ENTITY_SHAPE_COLUMNS = [
|
|
|
53
56
|
`type`,
|
|
54
57
|
`status`,
|
|
55
58
|
`tags`,
|
|
59
|
+
`created_by`,
|
|
56
60
|
`spawn_args`,
|
|
57
61
|
`sandbox`,
|
|
58
62
|
`parent`,
|
|
@@ -89,6 +93,16 @@ function sourceRefFromStreamPath(streamPath: string): string | null {
|
|
|
89
93
|
return match?.[1] ?? null
|
|
90
94
|
}
|
|
91
95
|
|
|
96
|
+
function principalScopedSourceRef(
|
|
97
|
+
tagSourceRef: string,
|
|
98
|
+
principalUrl: string,
|
|
99
|
+
principalKind: string
|
|
100
|
+
): string {
|
|
101
|
+
return `${tagSourceRef}-${hashString(
|
|
102
|
+
JSON.stringify({ principalKind, principalUrl })
|
|
103
|
+
)}`
|
|
104
|
+
}
|
|
105
|
+
|
|
92
106
|
function sameMember(
|
|
93
107
|
left: EntityMembershipRow | undefined,
|
|
94
108
|
right: EntityMembershipRow
|
|
@@ -125,6 +139,9 @@ class ProjectedEntityBridge {
|
|
|
125
139
|
readonly sourceRef: string
|
|
126
140
|
readonly tags: EntityTags
|
|
127
141
|
readonly streamUrl: string
|
|
142
|
+
private readonly principalUrl?: string
|
|
143
|
+
private readonly principalKind?: string
|
|
144
|
+
private readonly permissionBypass: boolean
|
|
128
145
|
|
|
129
146
|
private currentMembers = new Map<string, EntityMembershipRow>()
|
|
130
147
|
private producer: IdempotentProducer | null = null
|
|
@@ -132,12 +149,16 @@ class ProjectedEntityBridge {
|
|
|
132
149
|
|
|
133
150
|
constructor(
|
|
134
151
|
row: EntityBridgeRow,
|
|
152
|
+
private registry: PostgresRegistry,
|
|
135
153
|
private streamClient: StreamClient
|
|
136
154
|
) {
|
|
137
155
|
this.tenantId = row.tenantId
|
|
138
156
|
this.sourceRef = row.sourceRef
|
|
139
157
|
this.tags = normalizeTags(row.tags)
|
|
140
158
|
this.streamUrl = row.streamUrl
|
|
159
|
+
this.principalUrl = row.principalUrl
|
|
160
|
+
this.principalKind = row.principalKind
|
|
161
|
+
this.permissionBypass = isBuiltInSystemPrincipalUrl(row.principalUrl)
|
|
141
162
|
}
|
|
142
163
|
|
|
143
164
|
async start(initialEntities: Iterable<EntityShapeRow>): Promise<void> {
|
|
@@ -159,7 +180,7 @@ class ProjectedEntityBridge {
|
|
|
159
180
|
}
|
|
160
181
|
)
|
|
161
182
|
await this.loadCurrentMembers()
|
|
162
|
-
this.reconcile(initialEntities)
|
|
183
|
+
await this.reconcile(initialEntities)
|
|
163
184
|
}
|
|
164
185
|
|
|
165
186
|
async stop(): Promise<void> {
|
|
@@ -175,13 +196,14 @@ class ProjectedEntityBridge {
|
|
|
175
196
|
}
|
|
176
197
|
}
|
|
177
198
|
|
|
178
|
-
reconcile(entities: Iterable<EntityShapeRow>): void {
|
|
199
|
+
async reconcile(entities: Iterable<EntityShapeRow>): Promise<void> {
|
|
179
200
|
if (this.stopped) return
|
|
180
201
|
|
|
181
202
|
const staleMembers = new Map(this.currentMembers)
|
|
182
203
|
for (const entity of entities) {
|
|
183
204
|
if (entity.tenant_id !== this.tenantId) continue
|
|
184
205
|
if (!entityMatchesTags(entity, this.tags)) continue
|
|
206
|
+
if (!(await this.canReadEntity(entity))) continue
|
|
185
207
|
staleMembers.delete(entity.url)
|
|
186
208
|
this.upsertEntity(entity)
|
|
187
209
|
}
|
|
@@ -192,11 +214,14 @@ class ProjectedEntityBridge {
|
|
|
192
214
|
}
|
|
193
215
|
}
|
|
194
216
|
|
|
195
|
-
applyEntity(entity: EntityShapeRow): void {
|
|
217
|
+
async applyEntity(entity: EntityShapeRow): Promise<void> {
|
|
196
218
|
if (this.stopped) return
|
|
197
219
|
if (entity.tenant_id !== this.tenantId) return
|
|
198
220
|
|
|
199
|
-
if (
|
|
221
|
+
if (
|
|
222
|
+
!entityMatchesTags(entity, this.tags) ||
|
|
223
|
+
!(await this.canReadEntity(entity))
|
|
224
|
+
) {
|
|
200
225
|
const existing = this.currentMembers.get(entity.url)
|
|
201
226
|
if (!existing) return
|
|
202
227
|
this.append(`delete`, existing)
|
|
@@ -231,6 +256,16 @@ class ProjectedEntityBridge {
|
|
|
231
256
|
}
|
|
232
257
|
}
|
|
233
258
|
|
|
259
|
+
private async canReadEntity(entity: EntityShapeRow): Promise<boolean> {
|
|
260
|
+
if (this.permissionBypass) return true
|
|
261
|
+
if (!this.principalUrl || !this.principalKind) return false
|
|
262
|
+
if (entity.created_by === this.principalUrl) return true
|
|
263
|
+
return await this.registry.hasEntityPermission(entity.url, `read`, {
|
|
264
|
+
principalUrl: this.principalUrl,
|
|
265
|
+
principalKind: this.principalKind,
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
|
|
234
269
|
private async ensureStream(): Promise<void> {
|
|
235
270
|
if (!(await this.streamClient.exists(this.streamUrl))) {
|
|
236
271
|
await this.streamClient.create(this.streamUrl, {
|
|
@@ -377,7 +412,9 @@ export class EntityProjector {
|
|
|
377
412
|
async register(
|
|
378
413
|
tenantId: string,
|
|
379
414
|
registry: PostgresRegistry,
|
|
380
|
-
tagsInput: unknown
|
|
415
|
+
tagsInput: unknown,
|
|
416
|
+
principalUrl: string,
|
|
417
|
+
principalKind: string
|
|
381
418
|
): Promise<{ sourceRef: string; streamUrl: string }> {
|
|
382
419
|
if (!this.electricUrl) {
|
|
383
420
|
throw new Error(
|
|
@@ -388,12 +425,18 @@ export class EntityProjector {
|
|
|
388
425
|
await this.start()
|
|
389
426
|
this.registries.set(tenantId, registry)
|
|
390
427
|
const tags = normalizeTags(assertTags(tagsInput))
|
|
391
|
-
const sourceRef =
|
|
428
|
+
const sourceRef = principalScopedSourceRef(
|
|
429
|
+
sourceRefForTags(tags),
|
|
430
|
+
principalUrl,
|
|
431
|
+
principalKind
|
|
432
|
+
)
|
|
392
433
|
const streamUrl = getEntitiesStreamPath(sourceRef)
|
|
393
434
|
const row = await registry.upsertEntityBridge({
|
|
394
435
|
sourceRef,
|
|
395
436
|
tags,
|
|
396
437
|
streamUrl,
|
|
438
|
+
principalUrl,
|
|
439
|
+
principalKind,
|
|
397
440
|
})
|
|
398
441
|
await registry.touchEntityBridge(sourceRef)
|
|
399
442
|
await this.ensureProjection(row)
|
|
@@ -436,8 +479,12 @@ export class EntityProjector {
|
|
|
436
479
|
}
|
|
437
480
|
}
|
|
438
481
|
|
|
439
|
-
async onEntityChanged(
|
|
440
|
-
|
|
482
|
+
async onEntityChanged(tenantId: string, entityUrl: string): Promise<void> {
|
|
483
|
+
const entity = this.entities.get(entityKey(tenantId, entityUrl))
|
|
484
|
+
if (!entity) return
|
|
485
|
+
for (const projection of this.projectionsForTenant(tenantId)) {
|
|
486
|
+
await projection.applyEntity(entity)
|
|
487
|
+
}
|
|
441
488
|
}
|
|
442
489
|
|
|
443
490
|
async loadTenantBridges(
|
|
@@ -523,18 +570,20 @@ export class EntityProjector {
|
|
|
523
570
|
}
|
|
524
571
|
if (message.headers.control === `up-to-date`) {
|
|
525
572
|
this.upToDate = true
|
|
526
|
-
this.reconcileAll()
|
|
573
|
+
await this.reconcileAll()
|
|
527
574
|
this.readyResolve?.()
|
|
528
575
|
}
|
|
529
576
|
continue
|
|
530
577
|
}
|
|
531
578
|
|
|
532
579
|
if (!isChangeMessage(message)) continue
|
|
533
|
-
this.applyChangeMessage(message)
|
|
580
|
+
await this.applyChangeMessage(message)
|
|
534
581
|
}
|
|
535
582
|
}
|
|
536
583
|
|
|
537
|
-
private applyChangeMessage(
|
|
584
|
+
private async applyChangeMessage(
|
|
585
|
+
message: ChangeMessage<EntityShapeRow>
|
|
586
|
+
): Promise<void> {
|
|
538
587
|
const entity = message.value
|
|
539
588
|
const key = entityKey(entity.tenant_id, entity.url)
|
|
540
589
|
if (message.headers.operation === `delete`) {
|
|
@@ -550,7 +599,7 @@ export class EntityProjector {
|
|
|
550
599
|
this.entities.set(key, entity)
|
|
551
600
|
if (this.upToDate) {
|
|
552
601
|
for (const projection of this.projectionsForTenant(entity.tenant_id)) {
|
|
553
|
-
projection.applyEntity(entity)
|
|
602
|
+
await projection.applyEntity(entity)
|
|
554
603
|
}
|
|
555
604
|
}
|
|
556
605
|
}
|
|
@@ -642,7 +691,11 @@ export class EntityProjector {
|
|
|
642
691
|
}
|
|
643
692
|
throw error
|
|
644
693
|
}
|
|
645
|
-
const projection = new ProjectedEntityBridge(
|
|
694
|
+
const projection = new ProjectedEntityBridge(
|
|
695
|
+
row,
|
|
696
|
+
this.registryForTenant(row.tenantId),
|
|
697
|
+
streamClient
|
|
698
|
+
)
|
|
646
699
|
await projection.start(this.entitiesForTenant(row.tenantId))
|
|
647
700
|
this.projections.set(key, projection)
|
|
648
701
|
})().finally(() => {
|
|
@@ -665,9 +718,9 @@ export class EntityProjector {
|
|
|
665
718
|
)
|
|
666
719
|
}
|
|
667
720
|
|
|
668
|
-
private reconcileAll(): void {
|
|
721
|
+
private async reconcileAll(): Promise<void> {
|
|
669
722
|
for (const projection of this.projections.values()) {
|
|
670
|
-
projection.reconcile(this.entitiesForTenant(projection.tenantId))
|
|
723
|
+
await projection.reconcile(this.entitiesForTenant(projection.tenantId))
|
|
671
724
|
}
|
|
672
725
|
}
|
|
673
726
|
|
|
@@ -733,14 +786,20 @@ export class EntityProjectorTenantFacade implements EntityBridgeCoordinator {
|
|
|
733
786
|
|
|
734
787
|
async stop(): Promise<void> {}
|
|
735
788
|
|
|
736
|
-
async register(
|
|
789
|
+
async register(
|
|
790
|
+
tagsInput: unknown,
|
|
791
|
+
principalUrl: string,
|
|
792
|
+
principalKind: string
|
|
793
|
+
): Promise<{
|
|
737
794
|
sourceRef: string
|
|
738
795
|
streamUrl: string
|
|
739
796
|
}> {
|
|
740
797
|
return await this.projector.register(
|
|
741
798
|
this.tenantId,
|
|
742
799
|
this.registry,
|
|
743
|
-
tagsInput
|
|
800
|
+
tagsInput,
|
|
801
|
+
principalUrl,
|
|
802
|
+
principalKind
|
|
744
803
|
)
|
|
745
804
|
}
|
|
746
805
|
|