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