@electric-ax/agents-server 0.4.14 → 0.4.16
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 +1530 -256
- package/dist/index.cjs +1517 -232
- package/dist/index.d.cts +1359 -212
- package/dist/index.d.ts +1359 -212
- package/dist/index.js +1519 -234
- package/drizzle/0010_sandbox_profiles.sql +5 -0
- 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/meta/_journal.json +28 -0
- package/package.json +7 -7
- package/src/db/schema.ts +200 -0
- package/src/electric-agents-types.ts +147 -2
- package/src/entity-bridge-manager.ts +57 -6
- package/src/entity-manager.ts +411 -62
- package/src/entity-projector.ts +79 -17
- package/src/entity-registry.ts +681 -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 +362 -20
- package/src/routing/entity-types-router.ts +244 -15
- package/src/routing/hooks.ts +1 -0
- package/src/routing/observations-router.ts +2 -1
- package/src/routing/runners-router.ts +10 -0
- package/src/routing/sandbox.ts +173 -0
- package/src/runtime.ts +34 -0
- package/src/sandbox-choice-schema.ts +28 -0
- package/src/scheduler.ts +2 -0
- package/src/server.ts +5 -0
- package/src/stream-client.ts +17 -1
- package/src/utils/server-utils.ts +192 -12
- package/src/wake-registry.ts +30 -11
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,7 +39,9 @@ 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
|
|
44
|
+
sandbox?: { profile: string } | null
|
|
41
45
|
parent?: string | null
|
|
42
46
|
type_revision?: number | null
|
|
43
47
|
inbox_schemas?: Record<string, Record<string, unknown>> | null
|
|
@@ -52,7 +56,9 @@ const ENTITY_SHAPE_COLUMNS = [
|
|
|
52
56
|
`type`,
|
|
53
57
|
`status`,
|
|
54
58
|
`tags`,
|
|
59
|
+
`created_by`,
|
|
55
60
|
`spawn_args`,
|
|
61
|
+
`sandbox`,
|
|
56
62
|
`parent`,
|
|
57
63
|
`type_revision`,
|
|
58
64
|
`inbox_schemas`,
|
|
@@ -87,6 +93,16 @@ function sourceRefFromStreamPath(streamPath: string): string | null {
|
|
|
87
93
|
return match?.[1] ?? null
|
|
88
94
|
}
|
|
89
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
|
+
|
|
90
106
|
function sameMember(
|
|
91
107
|
left: EntityMembershipRow | undefined,
|
|
92
108
|
right: EntityMembershipRow
|
|
@@ -108,6 +124,7 @@ function toMemberRow(entity: EntityShapeRow): EntityMembershipRow {
|
|
|
108
124
|
status: entity.status,
|
|
109
125
|
tags: entity.tags,
|
|
110
126
|
spawn_args: entity.spawn_args ?? {},
|
|
127
|
+
sandbox: entity.sandbox ?? null,
|
|
111
128
|
parent: entity.parent ?? null,
|
|
112
129
|
type_revision: entity.type_revision ?? null,
|
|
113
130
|
inbox_schemas: entity.inbox_schemas ?? null,
|
|
@@ -122,6 +139,9 @@ class ProjectedEntityBridge {
|
|
|
122
139
|
readonly sourceRef: string
|
|
123
140
|
readonly tags: EntityTags
|
|
124
141
|
readonly streamUrl: string
|
|
142
|
+
private readonly principalUrl?: string
|
|
143
|
+
private readonly principalKind?: string
|
|
144
|
+
private readonly permissionBypass: boolean
|
|
125
145
|
|
|
126
146
|
private currentMembers = new Map<string, EntityMembershipRow>()
|
|
127
147
|
private producer: IdempotentProducer | null = null
|
|
@@ -129,12 +149,16 @@ class ProjectedEntityBridge {
|
|
|
129
149
|
|
|
130
150
|
constructor(
|
|
131
151
|
row: EntityBridgeRow,
|
|
152
|
+
private registry: PostgresRegistry,
|
|
132
153
|
private streamClient: StreamClient
|
|
133
154
|
) {
|
|
134
155
|
this.tenantId = row.tenantId
|
|
135
156
|
this.sourceRef = row.sourceRef
|
|
136
157
|
this.tags = normalizeTags(row.tags)
|
|
137
158
|
this.streamUrl = row.streamUrl
|
|
159
|
+
this.principalUrl = row.principalUrl
|
|
160
|
+
this.principalKind = row.principalKind
|
|
161
|
+
this.permissionBypass = isBuiltInSystemPrincipalUrl(row.principalUrl)
|
|
138
162
|
}
|
|
139
163
|
|
|
140
164
|
async start(initialEntities: Iterable<EntityShapeRow>): Promise<void> {
|
|
@@ -156,7 +180,7 @@ class ProjectedEntityBridge {
|
|
|
156
180
|
}
|
|
157
181
|
)
|
|
158
182
|
await this.loadCurrentMembers()
|
|
159
|
-
this.reconcile(initialEntities)
|
|
183
|
+
await this.reconcile(initialEntities)
|
|
160
184
|
}
|
|
161
185
|
|
|
162
186
|
async stop(): Promise<void> {
|
|
@@ -172,13 +196,14 @@ class ProjectedEntityBridge {
|
|
|
172
196
|
}
|
|
173
197
|
}
|
|
174
198
|
|
|
175
|
-
reconcile(entities: Iterable<EntityShapeRow>): void {
|
|
199
|
+
async reconcile(entities: Iterable<EntityShapeRow>): Promise<void> {
|
|
176
200
|
if (this.stopped) return
|
|
177
201
|
|
|
178
202
|
const staleMembers = new Map(this.currentMembers)
|
|
179
203
|
for (const entity of entities) {
|
|
180
204
|
if (entity.tenant_id !== this.tenantId) continue
|
|
181
205
|
if (!entityMatchesTags(entity, this.tags)) continue
|
|
206
|
+
if (!(await this.canReadEntity(entity))) continue
|
|
182
207
|
staleMembers.delete(entity.url)
|
|
183
208
|
this.upsertEntity(entity)
|
|
184
209
|
}
|
|
@@ -189,11 +214,14 @@ class ProjectedEntityBridge {
|
|
|
189
214
|
}
|
|
190
215
|
}
|
|
191
216
|
|
|
192
|
-
applyEntity(entity: EntityShapeRow): void {
|
|
217
|
+
async applyEntity(entity: EntityShapeRow): Promise<void> {
|
|
193
218
|
if (this.stopped) return
|
|
194
219
|
if (entity.tenant_id !== this.tenantId) return
|
|
195
220
|
|
|
196
|
-
if (
|
|
221
|
+
if (
|
|
222
|
+
!entityMatchesTags(entity, this.tags) ||
|
|
223
|
+
!(await this.canReadEntity(entity))
|
|
224
|
+
) {
|
|
197
225
|
const existing = this.currentMembers.get(entity.url)
|
|
198
226
|
if (!existing) return
|
|
199
227
|
this.append(`delete`, existing)
|
|
@@ -228,6 +256,16 @@ class ProjectedEntityBridge {
|
|
|
228
256
|
}
|
|
229
257
|
}
|
|
230
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
|
+
|
|
231
269
|
private async ensureStream(): Promise<void> {
|
|
232
270
|
if (!(await this.streamClient.exists(this.streamUrl))) {
|
|
233
271
|
await this.streamClient.create(this.streamUrl, {
|
|
@@ -374,7 +412,9 @@ export class EntityProjector {
|
|
|
374
412
|
async register(
|
|
375
413
|
tenantId: string,
|
|
376
414
|
registry: PostgresRegistry,
|
|
377
|
-
tagsInput: unknown
|
|
415
|
+
tagsInput: unknown,
|
|
416
|
+
principalUrl: string,
|
|
417
|
+
principalKind: string
|
|
378
418
|
): Promise<{ sourceRef: string; streamUrl: string }> {
|
|
379
419
|
if (!this.electricUrl) {
|
|
380
420
|
throw new Error(
|
|
@@ -385,12 +425,18 @@ export class EntityProjector {
|
|
|
385
425
|
await this.start()
|
|
386
426
|
this.registries.set(tenantId, registry)
|
|
387
427
|
const tags = normalizeTags(assertTags(tagsInput))
|
|
388
|
-
const sourceRef =
|
|
428
|
+
const sourceRef = principalScopedSourceRef(
|
|
429
|
+
sourceRefForTags(tags),
|
|
430
|
+
principalUrl,
|
|
431
|
+
principalKind
|
|
432
|
+
)
|
|
389
433
|
const streamUrl = getEntitiesStreamPath(sourceRef)
|
|
390
434
|
const row = await registry.upsertEntityBridge({
|
|
391
435
|
sourceRef,
|
|
392
436
|
tags,
|
|
393
437
|
streamUrl,
|
|
438
|
+
principalUrl,
|
|
439
|
+
principalKind,
|
|
394
440
|
})
|
|
395
441
|
await registry.touchEntityBridge(sourceRef)
|
|
396
442
|
await this.ensureProjection(row)
|
|
@@ -433,8 +479,12 @@ export class EntityProjector {
|
|
|
433
479
|
}
|
|
434
480
|
}
|
|
435
481
|
|
|
436
|
-
async onEntityChanged(
|
|
437
|
-
|
|
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
|
+
}
|
|
438
488
|
}
|
|
439
489
|
|
|
440
490
|
async loadTenantBridges(
|
|
@@ -520,18 +570,20 @@ export class EntityProjector {
|
|
|
520
570
|
}
|
|
521
571
|
if (message.headers.control === `up-to-date`) {
|
|
522
572
|
this.upToDate = true
|
|
523
|
-
this.reconcileAll()
|
|
573
|
+
await this.reconcileAll()
|
|
524
574
|
this.readyResolve?.()
|
|
525
575
|
}
|
|
526
576
|
continue
|
|
527
577
|
}
|
|
528
578
|
|
|
529
579
|
if (!isChangeMessage(message)) continue
|
|
530
|
-
this.applyChangeMessage(message)
|
|
580
|
+
await this.applyChangeMessage(message)
|
|
531
581
|
}
|
|
532
582
|
}
|
|
533
583
|
|
|
534
|
-
private applyChangeMessage(
|
|
584
|
+
private async applyChangeMessage(
|
|
585
|
+
message: ChangeMessage<EntityShapeRow>
|
|
586
|
+
): Promise<void> {
|
|
535
587
|
const entity = message.value
|
|
536
588
|
const key = entityKey(entity.tenant_id, entity.url)
|
|
537
589
|
if (message.headers.operation === `delete`) {
|
|
@@ -547,7 +599,7 @@ export class EntityProjector {
|
|
|
547
599
|
this.entities.set(key, entity)
|
|
548
600
|
if (this.upToDate) {
|
|
549
601
|
for (const projection of this.projectionsForTenant(entity.tenant_id)) {
|
|
550
|
-
projection.applyEntity(entity)
|
|
602
|
+
await projection.applyEntity(entity)
|
|
551
603
|
}
|
|
552
604
|
}
|
|
553
605
|
}
|
|
@@ -639,7 +691,11 @@ export class EntityProjector {
|
|
|
639
691
|
}
|
|
640
692
|
throw error
|
|
641
693
|
}
|
|
642
|
-
const projection = new ProjectedEntityBridge(
|
|
694
|
+
const projection = new ProjectedEntityBridge(
|
|
695
|
+
row,
|
|
696
|
+
this.registryForTenant(row.tenantId),
|
|
697
|
+
streamClient
|
|
698
|
+
)
|
|
643
699
|
await projection.start(this.entitiesForTenant(row.tenantId))
|
|
644
700
|
this.projections.set(key, projection)
|
|
645
701
|
})().finally(() => {
|
|
@@ -662,9 +718,9 @@ export class EntityProjector {
|
|
|
662
718
|
)
|
|
663
719
|
}
|
|
664
720
|
|
|
665
|
-
private reconcileAll(): void {
|
|
721
|
+
private async reconcileAll(): Promise<void> {
|
|
666
722
|
for (const projection of this.projections.values()) {
|
|
667
|
-
projection.reconcile(this.entitiesForTenant(projection.tenantId))
|
|
723
|
+
await projection.reconcile(this.entitiesForTenant(projection.tenantId))
|
|
668
724
|
}
|
|
669
725
|
}
|
|
670
726
|
|
|
@@ -730,14 +786,20 @@ export class EntityProjectorTenantFacade implements EntityBridgeCoordinator {
|
|
|
730
786
|
|
|
731
787
|
async stop(): Promise<void> {}
|
|
732
788
|
|
|
733
|
-
async register(
|
|
789
|
+
async register(
|
|
790
|
+
tagsInput: unknown,
|
|
791
|
+
principalUrl: string,
|
|
792
|
+
principalKind: string
|
|
793
|
+
): Promise<{
|
|
734
794
|
sourceRef: string
|
|
735
795
|
streamUrl: string
|
|
736
796
|
}> {
|
|
737
797
|
return await this.projector.register(
|
|
738
798
|
this.tenantId,
|
|
739
799
|
this.registry,
|
|
740
|
-
tagsInput
|
|
800
|
+
tagsInput,
|
|
801
|
+
principalUrl,
|
|
802
|
+
principalKind
|
|
741
803
|
)
|
|
742
804
|
}
|
|
743
805
|
|