@electric-ax/agents-server 0.4.15 → 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 +1176 -232
- package/dist/index.cjs +1179 -226
- package/dist/index.d.cts +1146 -167
- package/dist/index.d.ts +1146 -167
- package/dist/index.js +1181 -228
- 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 +21 -0
- package/package.json +7 -7
- package/src/db/schema.ts +198 -0
- package/src/electric-agents-types.ts +76 -2
- package/src/entity-bridge-manager.ts +57 -6
- package/src/entity-manager.ts +78 -60
- package/src/entity-projector.ts +76 -17
- package/src/entity-registry.ts +608 -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 +344 -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/runtime.ts +34 -0
- package/src/scheduler.ts +2 -0
- package/src/server.ts +5 -0
- package/src/utils/server-utils.ts +191 -11
- package/src/wake-registry.ts +8 -0
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
ErrCodeNotFound,
|
|
17
17
|
ErrCodeUnknownEntityType,
|
|
18
18
|
ErrCodeInvalidRequest,
|
|
19
|
+
ErrCodeUnauthorized,
|
|
19
20
|
toPublicEntity,
|
|
20
21
|
} from '../electric-agents-types.js'
|
|
21
22
|
import {
|
|
@@ -27,8 +28,19 @@ import {
|
|
|
27
28
|
unlinkEntityDispatchSubscription,
|
|
28
29
|
} from './dispatch-policy.js'
|
|
29
30
|
import { ElectricAgentsError } from '../entity-manager.js'
|
|
31
|
+
import {
|
|
32
|
+
canAccessEntity,
|
|
33
|
+
canAccessEntityType,
|
|
34
|
+
isPermissionBypassPrincipal,
|
|
35
|
+
principalSubject,
|
|
36
|
+
} from '../permissions.js'
|
|
30
37
|
import { routeBody, withSchema } from './schema.js'
|
|
31
|
-
import type {
|
|
38
|
+
import type {
|
|
39
|
+
ElectricAgentsEntity,
|
|
40
|
+
ElectricAgentsEntityType,
|
|
41
|
+
EntityPermission,
|
|
42
|
+
SendRequest,
|
|
43
|
+
} from '../electric-agents-types.js'
|
|
32
44
|
import type { JsonRouteRequest } from './schema.js'
|
|
33
45
|
import type { RouterType } from 'itty-router'
|
|
34
46
|
import type { TenantContext } from './context.js'
|
|
@@ -36,9 +48,11 @@ import type { EventSourceSubscriptionInput } from '@electric-ax/agents-runtime'
|
|
|
36
48
|
|
|
37
49
|
interface AgentsRouteRequest extends JsonRouteRequest {
|
|
38
50
|
entityRoute?: ExistingEntityRoute
|
|
51
|
+
spawnRoute?: SpawnableEntityRoute
|
|
39
52
|
}
|
|
40
53
|
|
|
41
54
|
type ExistingEntityRoute = { entityUrl: string; entity: ElectricAgentsEntity }
|
|
55
|
+
type SpawnableEntityRoute = { entityType: ElectricAgentsEntityType }
|
|
42
56
|
type AgentsRouteArgs = [TenantContext]
|
|
43
57
|
type AgentsRouteResult = Response | undefined
|
|
44
58
|
|
|
@@ -78,6 +92,41 @@ const wakeConditionSchema = Type.Union([
|
|
|
78
92
|
}),
|
|
79
93
|
])
|
|
80
94
|
|
|
95
|
+
const permissionSubjectSchema = Type.Object(
|
|
96
|
+
{
|
|
97
|
+
subject_kind: Type.Union([
|
|
98
|
+
Type.Literal(`principal`),
|
|
99
|
+
Type.Literal(`principal_kind`),
|
|
100
|
+
]),
|
|
101
|
+
subject_value: Type.String(),
|
|
102
|
+
},
|
|
103
|
+
{ additionalProperties: false }
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
const entityPermissionSchema = Type.Union([
|
|
107
|
+
Type.Literal(`read`),
|
|
108
|
+
Type.Literal(`write`),
|
|
109
|
+
Type.Literal(`delete`),
|
|
110
|
+
Type.Literal(`signal`),
|
|
111
|
+
Type.Literal(`fork`),
|
|
112
|
+
Type.Literal(`schedule`),
|
|
113
|
+
Type.Literal(`spawn`),
|
|
114
|
+
Type.Literal(`manage`),
|
|
115
|
+
])
|
|
116
|
+
|
|
117
|
+
const entityPermissionGrantInputSchema = Type.Object(
|
|
118
|
+
{
|
|
119
|
+
...permissionSubjectSchema.properties,
|
|
120
|
+
permission: entityPermissionSchema,
|
|
121
|
+
propagation: Type.Optional(
|
|
122
|
+
Type.Union([Type.Literal(`self`), Type.Literal(`descendants`)])
|
|
123
|
+
),
|
|
124
|
+
copy_to_children: Type.Optional(Type.Boolean()),
|
|
125
|
+
expires_at: Type.Optional(Type.String()),
|
|
126
|
+
},
|
|
127
|
+
{ additionalProperties: false }
|
|
128
|
+
)
|
|
129
|
+
|
|
81
130
|
const spawnBodySchema = Type.Object({
|
|
82
131
|
args: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
83
132
|
tags: Type.Optional(stringRecordSchema),
|
|
@@ -85,6 +134,7 @@ const spawnBodySchema = Type.Object({
|
|
|
85
134
|
dispatch_policy: Type.Optional(dispatchPolicySchema),
|
|
86
135
|
sandbox: Type.Optional(sandboxChoiceSchema),
|
|
87
136
|
initialMessage: Type.Optional(Type.Unknown()),
|
|
137
|
+
grants: Type.Optional(Type.Array(entityPermissionGrantInputSchema)),
|
|
88
138
|
wake: Type.Optional(
|
|
89
139
|
Type.Object({
|
|
90
140
|
subscriberUrl: Type.String(),
|
|
@@ -112,8 +162,30 @@ const sendBodySchema = Type.Object({
|
|
|
112
162
|
position: Type.Optional(Type.String()),
|
|
113
163
|
afterMs: Type.Optional(Type.Number()),
|
|
114
164
|
from: Type.Optional(Type.String()),
|
|
165
|
+
from_principal: Type.Optional(Type.String()),
|
|
166
|
+
from_agent: Type.Optional(Type.String()),
|
|
115
167
|
})
|
|
116
168
|
|
|
169
|
+
function agentUrlForPrincipal(principal: {
|
|
170
|
+
kind: string
|
|
171
|
+
id: string
|
|
172
|
+
key: string
|
|
173
|
+
}): string | null {
|
|
174
|
+
if (principal.kind === `agent`) return `/${principal.id}`
|
|
175
|
+
if (principal.key.startsWith(`entity:`)) {
|
|
176
|
+
return `/${principal.key.slice(`entity:`.length)}`
|
|
177
|
+
}
|
|
178
|
+
return null
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function agentUrlPath(value: string): string {
|
|
182
|
+
try {
|
|
183
|
+
return new URL(value).pathname
|
|
184
|
+
} catch {
|
|
185
|
+
return value
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
117
189
|
const inboxMessageBodySchema = Type.Object({
|
|
118
190
|
payload: Type.Optional(Type.Unknown()),
|
|
119
191
|
position: Type.Optional(Type.String()),
|
|
@@ -215,6 +287,9 @@ type ScheduleBody = Static<typeof scheduleBodySchema>
|
|
|
215
287
|
type EventSourceSubscriptionBody = Static<
|
|
216
288
|
typeof eventSourceSubscriptionBodySchema
|
|
217
289
|
>
|
|
290
|
+
type EntityPermissionGrantInput = Static<
|
|
291
|
+
typeof entityPermissionGrantInputSchema
|
|
292
|
+
>
|
|
218
293
|
type AttachmentSubjectType = `inbox` | `run` | `text` | `tool_call` | `context`
|
|
219
294
|
type AttachmentRole = `input` | `output`
|
|
220
295
|
type ParsedAttachmentForm = {
|
|
@@ -248,88 +323,137 @@ entitiesRouter.put(
|
|
|
248
323
|
`/:type/:instanceId`,
|
|
249
324
|
withSpawnableEntityType,
|
|
250
325
|
withSchema(spawnBodySchema),
|
|
326
|
+
withSpawnPermission,
|
|
251
327
|
spawnEntity
|
|
252
328
|
)
|
|
253
|
-
entitiesRouter.get(
|
|
254
|
-
|
|
255
|
-
|
|
329
|
+
entitiesRouter.get(
|
|
330
|
+
`/:type/:instanceId`,
|
|
331
|
+
withExistingEntity,
|
|
332
|
+
withEntityPermission(`read`),
|
|
333
|
+
getEntity
|
|
334
|
+
)
|
|
335
|
+
entitiesRouter.head(
|
|
336
|
+
`/:type/:instanceId`,
|
|
337
|
+
withExistingEntity,
|
|
338
|
+
withEntityPermission(`read`),
|
|
339
|
+
headEntity
|
|
340
|
+
)
|
|
341
|
+
entitiesRouter.delete(
|
|
342
|
+
`/:type/:instanceId`,
|
|
343
|
+
withExistingEntity,
|
|
344
|
+
withEntityPermission(`delete`),
|
|
345
|
+
killEntity
|
|
346
|
+
)
|
|
256
347
|
entitiesRouter.post(
|
|
257
348
|
`/:type/:instanceId/signal`,
|
|
258
349
|
withExistingEntity,
|
|
259
350
|
withSchema(signalBodySchema),
|
|
351
|
+
withEntityPermission(`signal`),
|
|
260
352
|
signalEntity
|
|
261
353
|
)
|
|
262
354
|
entitiesRouter.post(
|
|
263
355
|
`/:type/:instanceId/send`,
|
|
264
356
|
withExistingEntity,
|
|
265
357
|
withSchema(sendBodySchema),
|
|
358
|
+
withEntityPermission(`write`),
|
|
266
359
|
sendEntity
|
|
267
360
|
)
|
|
268
361
|
entitiesRouter.post(
|
|
269
362
|
`/:type/:instanceId/attachments`,
|
|
270
363
|
withExistingEntity,
|
|
364
|
+
withEntityPermission(`write`),
|
|
271
365
|
createAttachment
|
|
272
366
|
)
|
|
273
367
|
entitiesRouter.get(
|
|
274
368
|
`/:type/:instanceId/attachments/:attachmentId`,
|
|
275
369
|
withExistingEntity,
|
|
370
|
+
withEntityPermission(`read`),
|
|
276
371
|
readAttachment
|
|
277
372
|
)
|
|
278
373
|
entitiesRouter.delete(
|
|
279
374
|
`/:type/:instanceId/attachments/:attachmentId`,
|
|
280
375
|
withExistingEntity,
|
|
376
|
+
withEntityPermission(`write`),
|
|
281
377
|
deleteAttachment
|
|
282
378
|
)
|
|
283
379
|
entitiesRouter.patch(
|
|
284
380
|
`/:type/:instanceId/inbox/:messageKey`,
|
|
285
381
|
withExistingEntity,
|
|
286
382
|
withSchema(inboxMessageBodySchema),
|
|
383
|
+
withEntityPermission(`write`),
|
|
287
384
|
updateInboxMessage
|
|
288
385
|
)
|
|
289
386
|
entitiesRouter.delete(
|
|
290
387
|
`/:type/:instanceId/inbox/:messageKey`,
|
|
291
388
|
withExistingEntity,
|
|
389
|
+
withEntityPermission(`write`),
|
|
292
390
|
deleteInboxMessage
|
|
293
391
|
)
|
|
294
392
|
entitiesRouter.post(
|
|
295
393
|
`/:type/:instanceId/fork`,
|
|
296
394
|
withExistingEntity,
|
|
297
395
|
withSchema(forkBodySchema),
|
|
396
|
+
withEntityPermission(`fork`),
|
|
298
397
|
forkEntity
|
|
299
398
|
)
|
|
300
399
|
entitiesRouter.post(
|
|
301
400
|
`/:type/:instanceId/tags/:tagKey`,
|
|
302
401
|
withExistingEntity,
|
|
303
402
|
withSchema(setTagBodySchema),
|
|
403
|
+
withEntityPermission(`write`),
|
|
304
404
|
setTag
|
|
305
405
|
)
|
|
306
406
|
entitiesRouter.delete(
|
|
307
407
|
`/:type/:instanceId/tags/:tagKey`,
|
|
308
408
|
withExistingEntity,
|
|
409
|
+
withEntityPermission(`write`),
|
|
309
410
|
deleteTag
|
|
310
411
|
)
|
|
311
412
|
entitiesRouter.put(
|
|
312
413
|
`/:type/:instanceId/schedules/:scheduleId`,
|
|
313
414
|
withExistingEntity,
|
|
314
415
|
withSchema(scheduleBodySchema),
|
|
416
|
+
withEntityPermission(`schedule`),
|
|
315
417
|
upsertSchedule
|
|
316
418
|
)
|
|
317
419
|
entitiesRouter.delete(
|
|
318
420
|
`/:type/:instanceId/schedules/:scheduleId`,
|
|
319
421
|
withExistingEntity,
|
|
422
|
+
withEntityPermission(`schedule`),
|
|
320
423
|
deleteSchedule
|
|
321
424
|
)
|
|
322
425
|
entitiesRouter.put(
|
|
323
426
|
`/:type/:instanceId/event-source-subscriptions/:subscriptionId`,
|
|
324
427
|
withExistingEntity,
|
|
325
428
|
withSchema(eventSourceSubscriptionBodySchema),
|
|
429
|
+
withEntityPermission(`write`),
|
|
326
430
|
upsertEventSourceSubscription
|
|
327
431
|
)
|
|
328
432
|
entitiesRouter.delete(
|
|
329
433
|
`/:type/:instanceId/event-source-subscriptions/:subscriptionId`,
|
|
330
434
|
withExistingEntity,
|
|
435
|
+
withEntityPermission(`write`),
|
|
331
436
|
deleteEventSourceSubscription
|
|
332
437
|
)
|
|
438
|
+
entitiesRouter.get(
|
|
439
|
+
`/:type/:instanceId/grants`,
|
|
440
|
+
withExistingEntity,
|
|
441
|
+
withEntityPermission(`manage`),
|
|
442
|
+
listEntityPermissionGrants
|
|
443
|
+
)
|
|
444
|
+
entitiesRouter.post(
|
|
445
|
+
`/:type/:instanceId/grants`,
|
|
446
|
+
withExistingEntity,
|
|
447
|
+
withSchema(entityPermissionGrantInputSchema),
|
|
448
|
+
withEntityPermission(`manage`),
|
|
449
|
+
createEntityPermissionGrant
|
|
450
|
+
)
|
|
451
|
+
entitiesRouter.delete(
|
|
452
|
+
`/:type/:instanceId/grants/:grantId`,
|
|
453
|
+
withExistingEntity,
|
|
454
|
+
withEntityPermission(`manage`),
|
|
455
|
+
deleteEntityPermissionGrant
|
|
456
|
+
)
|
|
333
457
|
|
|
334
458
|
function entityUrlFromSegments(
|
|
335
459
|
type: string,
|
|
@@ -503,6 +627,31 @@ function rejectPrincipalEntityMutation(
|
|
|
503
627
|
)
|
|
504
628
|
}
|
|
505
629
|
|
|
630
|
+
function parseExpiresAt(value: string | undefined): Date | undefined {
|
|
631
|
+
if (value === undefined) return undefined
|
|
632
|
+
const expiresAt = new Date(value)
|
|
633
|
+
if (Number.isNaN(expiresAt.getTime())) {
|
|
634
|
+
throw new ElectricAgentsError(
|
|
635
|
+
ErrCodeInvalidRequest,
|
|
636
|
+
`Invalid expires_at timestamp`,
|
|
637
|
+
400
|
|
638
|
+
)
|
|
639
|
+
}
|
|
640
|
+
return expiresAt
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function parseGrantId(request: AgentsRouteRequest): number {
|
|
644
|
+
const grantId = Number.parseInt(String(request.params.grantId), 10)
|
|
645
|
+
if (!Number.isSafeInteger(grantId) || grantId <= 0) {
|
|
646
|
+
throw new ElectricAgentsError(
|
|
647
|
+
ErrCodeInvalidRequest,
|
|
648
|
+
`Invalid grant id`,
|
|
649
|
+
400
|
|
650
|
+
)
|
|
651
|
+
}
|
|
652
|
+
return grantId
|
|
653
|
+
}
|
|
654
|
+
|
|
506
655
|
async function withExistingEntity(
|
|
507
656
|
request: AgentsRouteRequest,
|
|
508
657
|
ctx: TenantContext
|
|
@@ -574,9 +723,96 @@ async function withSpawnableEntityType(
|
|
|
574
723
|
)
|
|
575
724
|
}
|
|
576
725
|
|
|
726
|
+
request.spawnRoute = { entityType }
|
|
577
727
|
return undefined
|
|
578
728
|
}
|
|
579
729
|
|
|
730
|
+
function withEntityPermission(permission: EntityPermission) {
|
|
731
|
+
return async (
|
|
732
|
+
request: AgentsRouteRequest,
|
|
733
|
+
ctx: TenantContext
|
|
734
|
+
): Promise<AgentsRouteResult> => {
|
|
735
|
+
const { entity } = requireExistingEntityRoute(request)
|
|
736
|
+
if (await canAccessEntity(ctx, entity, permission, request as Request)) {
|
|
737
|
+
return undefined
|
|
738
|
+
}
|
|
739
|
+
return apiError(
|
|
740
|
+
401,
|
|
741
|
+
ErrCodeUnauthorized,
|
|
742
|
+
`Principal is not allowed to ${permission} ${entity.url}`
|
|
743
|
+
)
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
async function withSpawnPermission(
|
|
748
|
+
request: AgentsRouteRequest,
|
|
749
|
+
ctx: TenantContext
|
|
750
|
+
): Promise<AgentsRouteResult> {
|
|
751
|
+
const parsed = routeBody<SpawnBody>(request)
|
|
752
|
+
const entityType = request.spawnRoute?.entityType
|
|
753
|
+
if (!entityType) {
|
|
754
|
+
throw new Error(`spawnable entity type middleware did not run`)
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (
|
|
758
|
+
!(await canAccessEntityType(ctx, entityType, `spawn`, request as Request))
|
|
759
|
+
) {
|
|
760
|
+
return apiError(
|
|
761
|
+
401,
|
|
762
|
+
ErrCodeUnauthorized,
|
|
763
|
+
`Principal is not allowed to spawn ${entityType.name}`
|
|
764
|
+
)
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (!parsed.parent) return undefined
|
|
768
|
+
|
|
769
|
+
const parent = await ctx.entityManager.registry.getEntity(parsed.parent)
|
|
770
|
+
if (!parent) {
|
|
771
|
+
return apiError(404, ErrCodeNotFound, `Parent entity not found`)
|
|
772
|
+
}
|
|
773
|
+
if (await canAccessEntity(ctx, parent, `spawn`, request as Request)) {
|
|
774
|
+
return await validateParentedSpawnGrants(request, ctx, parent, parsed)
|
|
775
|
+
}
|
|
776
|
+
return apiError(
|
|
777
|
+
401,
|
|
778
|
+
ErrCodeUnauthorized,
|
|
779
|
+
`Principal is not allowed to spawn children from ${parent.url}`
|
|
780
|
+
)
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
async function validateParentedSpawnGrants(
|
|
784
|
+
request: AgentsRouteRequest,
|
|
785
|
+
ctx: TenantContext,
|
|
786
|
+
parent: ElectricAgentsEntity,
|
|
787
|
+
parsed: SpawnBody
|
|
788
|
+
): Promise<AgentsRouteResult> {
|
|
789
|
+
const needsParentManage = (parsed.grants ?? []).some(
|
|
790
|
+
requiresParentManageForInitialGrant
|
|
791
|
+
)
|
|
792
|
+
if (!needsParentManage) return undefined
|
|
793
|
+
|
|
794
|
+
if (await canAccessEntity(ctx, parent, `manage`, request as Request)) {
|
|
795
|
+
return undefined
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return apiError(
|
|
799
|
+
401,
|
|
800
|
+
ErrCodeUnauthorized,
|
|
801
|
+
`Principal is not allowed to delegate broad grants from ${parent.url}`
|
|
802
|
+
)
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function requiresParentManageForInitialGrant(
|
|
806
|
+
grant: EntityPermissionGrantInput
|
|
807
|
+
): boolean {
|
|
808
|
+
return (
|
|
809
|
+
grant.permission === `manage` ||
|
|
810
|
+
grant.subject_kind === `principal_kind` ||
|
|
811
|
+
grant.propagation === `descendants` ||
|
|
812
|
+
grant.copy_to_children === true
|
|
813
|
+
)
|
|
814
|
+
}
|
|
815
|
+
|
|
580
816
|
async function listEntities(
|
|
581
817
|
{ query }: AgentsRouteRequest,
|
|
582
818
|
ctx: TenantContext
|
|
@@ -586,10 +822,61 @@ async function listEntities(
|
|
|
586
822
|
status: firstQueryValue(query.status),
|
|
587
823
|
parent: firstQueryValue(query.parent),
|
|
588
824
|
created_by: firstQueryValue(query.created_by),
|
|
825
|
+
readableBy: {
|
|
826
|
+
...principalSubject(ctx.principal),
|
|
827
|
+
bypass: isPermissionBypassPrincipal(ctx),
|
|
828
|
+
},
|
|
589
829
|
})
|
|
590
830
|
return json(entities.map((entity) => toPublicEntity(entity)))
|
|
591
831
|
}
|
|
592
832
|
|
|
833
|
+
async function listEntityPermissionGrants(
|
|
834
|
+
request: AgentsRouteRequest,
|
|
835
|
+
ctx: TenantContext
|
|
836
|
+
): Promise<Response> {
|
|
837
|
+
const { entityUrl } = requireExistingEntityRoute(request)
|
|
838
|
+
const grants =
|
|
839
|
+
await ctx.entityManager.registry.listEntityPermissionGrants(entityUrl)
|
|
840
|
+
return json({ grants })
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
async function createEntityPermissionGrant(
|
|
844
|
+
request: AgentsRouteRequest,
|
|
845
|
+
ctx: TenantContext
|
|
846
|
+
): Promise<Response> {
|
|
847
|
+
const { entityUrl } = requireExistingEntityRoute(request)
|
|
848
|
+
const parsed = routeBody<EntityPermissionGrantInput>(request)
|
|
849
|
+
const grant = await ctx.entityManager.registry.createEntityPermissionGrant({
|
|
850
|
+
entityUrl,
|
|
851
|
+
permission: parsed.permission,
|
|
852
|
+
subjectKind: parsed.subject_kind,
|
|
853
|
+
subjectValue: parsed.subject_value,
|
|
854
|
+
propagation: parsed.propagation,
|
|
855
|
+
copyToChildren: parsed.copy_to_children,
|
|
856
|
+
expiresAt: parseExpiresAt(parsed.expires_at),
|
|
857
|
+
createdBy: ctx.principal.url,
|
|
858
|
+
})
|
|
859
|
+
await ctx.entityBridgeManager.onEntityChanged(entityUrl)
|
|
860
|
+
return json(grant, { status: 201 })
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
async function deleteEntityPermissionGrant(
|
|
864
|
+
request: AgentsRouteRequest,
|
|
865
|
+
ctx: TenantContext
|
|
866
|
+
): Promise<Response> {
|
|
867
|
+
const { entityUrl } = requireExistingEntityRoute(request)
|
|
868
|
+
const deleted = await ctx.entityManager.registry.deleteEntityPermissionGrant(
|
|
869
|
+
entityUrl,
|
|
870
|
+
parseGrantId(request)
|
|
871
|
+
)
|
|
872
|
+
if (deleted) {
|
|
873
|
+
await ctx.entityBridgeManager.onEntityChanged(entityUrl)
|
|
874
|
+
}
|
|
875
|
+
return deleted
|
|
876
|
+
? status(204)
|
|
877
|
+
: apiError(404, ErrCodeNotFound, `Grant not found`)
|
|
878
|
+
}
|
|
879
|
+
|
|
593
880
|
async function upsertSchedule(
|
|
594
881
|
request: AgentsRouteRequest,
|
|
595
882
|
ctx: TenantContext
|
|
@@ -805,6 +1092,7 @@ async function forkEntity(
|
|
|
805
1092
|
const result = await ctx.entityManager.forkSubtree(entityUrl, {
|
|
806
1093
|
rootInstanceId: parsed.instance_id,
|
|
807
1094
|
waitTimeoutMs: parsed.waitTimeoutMs,
|
|
1095
|
+
createdBy: ctx.principal.url,
|
|
808
1096
|
...(parsed.fork_pointer && {
|
|
809
1097
|
forkPointer: {
|
|
810
1098
|
offset: parsed.fork_pointer.offset,
|
|
@@ -837,6 +1125,26 @@ async function sendEntity(
|
|
|
837
1125
|
`Request from must match Electric-Principal`
|
|
838
1126
|
)
|
|
839
1127
|
}
|
|
1128
|
+
if (
|
|
1129
|
+
parsed.from_principal !== undefined &&
|
|
1130
|
+
parsed.from_principal !== principal.url
|
|
1131
|
+
) {
|
|
1132
|
+
return apiError(
|
|
1133
|
+
400,
|
|
1134
|
+
ErrCodeInvalidRequest,
|
|
1135
|
+
`Request from_principal must match Electric-Principal`
|
|
1136
|
+
)
|
|
1137
|
+
}
|
|
1138
|
+
if (parsed.from_agent !== undefined) {
|
|
1139
|
+
const principalAgentUrl = agentUrlForPrincipal(principal)
|
|
1140
|
+
if (agentUrlPath(parsed.from_agent) !== principalAgentUrl) {
|
|
1141
|
+
return apiError(
|
|
1142
|
+
400,
|
|
1143
|
+
ErrCodeInvalidRequest,
|
|
1144
|
+
`Request from_agent must match authenticated agent principal`
|
|
1145
|
+
)
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
840
1148
|
await ctx.entityManager.ensurePrincipal(principal)
|
|
841
1149
|
const { entityUrl, entity } = requireExistingEntityRoute(request)
|
|
842
1150
|
|
|
@@ -845,28 +1153,25 @@ async function sendEntity(
|
|
|
845
1153
|
: await backfillEntityDispatchPolicy(ctx, entity)
|
|
846
1154
|
await linkEntityDispatchSubscription(ctx, dispatchEntity)
|
|
847
1155
|
|
|
1156
|
+
const sendReq: SendRequest = {
|
|
1157
|
+
from: principal.url,
|
|
1158
|
+
from_principal: principal.url,
|
|
1159
|
+
from_agent: parsed.from_agent,
|
|
1160
|
+
payload: parsed.payload,
|
|
1161
|
+
key: parsed.key,
|
|
1162
|
+
type: parsed.type,
|
|
1163
|
+
mode: parsed.mode,
|
|
1164
|
+
position: parsed.position,
|
|
1165
|
+
}
|
|
1166
|
+
|
|
848
1167
|
if (parsed.afterMs && parsed.afterMs > 0) {
|
|
849
1168
|
await ctx.entityManager.enqueueDelayedSend(
|
|
850
1169
|
entityUrl,
|
|
851
|
-
|
|
852
|
-
from: principal.url,
|
|
853
|
-
payload: parsed.payload,
|
|
854
|
-
key: parsed.key,
|
|
855
|
-
type: parsed.type,
|
|
856
|
-
mode: parsed.mode,
|
|
857
|
-
position: parsed.position,
|
|
858
|
-
},
|
|
1170
|
+
sendReq,
|
|
859
1171
|
new Date(Date.now() + parsed.afterMs)
|
|
860
1172
|
)
|
|
861
1173
|
} else {
|
|
862
|
-
await ctx.entityManager.send(entityUrl,
|
|
863
|
-
from: principal.url,
|
|
864
|
-
payload: parsed.payload,
|
|
865
|
-
key: parsed.key,
|
|
866
|
-
type: parsed.type,
|
|
867
|
-
mode: parsed.mode,
|
|
868
|
-
position: parsed.position,
|
|
869
|
-
})
|
|
1174
|
+
await ctx.entityManager.send(entityUrl, sendReq)
|
|
870
1175
|
}
|
|
871
1176
|
|
|
872
1177
|
return status(204)
|
|
@@ -991,6 +1296,25 @@ async function spawnEntity(
|
|
|
991
1296
|
wake: parsed.wake,
|
|
992
1297
|
created_by: principal.url,
|
|
993
1298
|
})
|
|
1299
|
+
if (parsed.parent) {
|
|
1300
|
+
await ctx.entityManager.registry.copyEntityPermissionGrantsForSpawn(
|
|
1301
|
+
parsed.parent,
|
|
1302
|
+
entity.url,
|
|
1303
|
+
principal.url
|
|
1304
|
+
)
|
|
1305
|
+
}
|
|
1306
|
+
for (const grant of parsed.grants ?? []) {
|
|
1307
|
+
await ctx.entityManager.registry.createEntityPermissionGrant({
|
|
1308
|
+
entityUrl: entity.url,
|
|
1309
|
+
permission: grant.permission,
|
|
1310
|
+
subjectKind: grant.subject_kind,
|
|
1311
|
+
subjectValue: grant.subject_value,
|
|
1312
|
+
propagation: grant.propagation,
|
|
1313
|
+
copyToChildren: grant.copy_to_children,
|
|
1314
|
+
expiresAt: parseExpiresAt(grant.expires_at),
|
|
1315
|
+
createdBy: principal.url,
|
|
1316
|
+
})
|
|
1317
|
+
}
|
|
994
1318
|
const linkBeforeInitialMessage =
|
|
995
1319
|
parsed.initialMessage !== undefined &&
|
|
996
1320
|
shouldLinkDispatchBeforeInitialMessage(dispatchPolicy)
|