@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
|
@@ -11,10 +11,12 @@ import { Router, json, status } from 'itty-router'
|
|
|
11
11
|
import { apiError } from '../electric-agents-http.js'
|
|
12
12
|
import { parsePrincipalKey, principalUrl } from '../principal.js'
|
|
13
13
|
import { dispatchPolicySchema } from '../dispatch-policy-schema.js'
|
|
14
|
+
import { sandboxChoiceSchema } from '../sandbox-choice-schema.js'
|
|
14
15
|
import {
|
|
15
16
|
ErrCodeNotFound,
|
|
16
17
|
ErrCodeUnknownEntityType,
|
|
17
18
|
ErrCodeInvalidRequest,
|
|
19
|
+
ErrCodeUnauthorized,
|
|
18
20
|
toPublicEntity,
|
|
19
21
|
} from '../electric-agents-types.js'
|
|
20
22
|
import {
|
|
@@ -26,8 +28,19 @@ import {
|
|
|
26
28
|
unlinkEntityDispatchSubscription,
|
|
27
29
|
} from './dispatch-policy.js'
|
|
28
30
|
import { ElectricAgentsError } from '../entity-manager.js'
|
|
31
|
+
import {
|
|
32
|
+
canAccessEntity,
|
|
33
|
+
canAccessEntityType,
|
|
34
|
+
isPermissionBypassPrincipal,
|
|
35
|
+
principalSubject,
|
|
36
|
+
} from '../permissions.js'
|
|
29
37
|
import { routeBody, withSchema } from './schema.js'
|
|
30
|
-
import type {
|
|
38
|
+
import type {
|
|
39
|
+
ElectricAgentsEntity,
|
|
40
|
+
ElectricAgentsEntityType,
|
|
41
|
+
EntityPermission,
|
|
42
|
+
SendRequest,
|
|
43
|
+
} from '../electric-agents-types.js'
|
|
31
44
|
import type { JsonRouteRequest } from './schema.js'
|
|
32
45
|
import type { RouterType } from 'itty-router'
|
|
33
46
|
import type { TenantContext } from './context.js'
|
|
@@ -35,9 +48,11 @@ import type { EventSourceSubscriptionInput } from '@electric-ax/agents-runtime'
|
|
|
35
48
|
|
|
36
49
|
interface AgentsRouteRequest extends JsonRouteRequest {
|
|
37
50
|
entityRoute?: ExistingEntityRoute
|
|
51
|
+
spawnRoute?: SpawnableEntityRoute
|
|
38
52
|
}
|
|
39
53
|
|
|
40
54
|
type ExistingEntityRoute = { entityUrl: string; entity: ElectricAgentsEntity }
|
|
55
|
+
type SpawnableEntityRoute = { entityType: ElectricAgentsEntityType }
|
|
41
56
|
type AgentsRouteArgs = [TenantContext]
|
|
42
57
|
type AgentsRouteResult = Response | undefined
|
|
43
58
|
|
|
@@ -77,12 +92,49 @@ const wakeConditionSchema = Type.Union([
|
|
|
77
92
|
}),
|
|
78
93
|
])
|
|
79
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
|
+
|
|
80
130
|
const spawnBodySchema = Type.Object({
|
|
81
131
|
args: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
82
132
|
tags: Type.Optional(stringRecordSchema),
|
|
83
133
|
parent: Type.Optional(Type.String()),
|
|
84
134
|
dispatch_policy: Type.Optional(dispatchPolicySchema),
|
|
135
|
+
sandbox: Type.Optional(sandboxChoiceSchema),
|
|
85
136
|
initialMessage: Type.Optional(Type.Unknown()),
|
|
137
|
+
grants: Type.Optional(Type.Array(entityPermissionGrantInputSchema)),
|
|
86
138
|
wake: Type.Optional(
|
|
87
139
|
Type.Object({
|
|
88
140
|
subscriberUrl: Type.String(),
|
|
@@ -110,8 +162,30 @@ const sendBodySchema = Type.Object({
|
|
|
110
162
|
position: Type.Optional(Type.String()),
|
|
111
163
|
afterMs: Type.Optional(Type.Number()),
|
|
112
164
|
from: Type.Optional(Type.String()),
|
|
165
|
+
from_principal: Type.Optional(Type.String()),
|
|
166
|
+
from_agent: Type.Optional(Type.String()),
|
|
113
167
|
})
|
|
114
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
|
+
|
|
115
189
|
const inboxMessageBodySchema = Type.Object({
|
|
116
190
|
payload: Type.Optional(Type.Unknown()),
|
|
117
191
|
position: Type.Optional(Type.String()),
|
|
@@ -135,6 +209,15 @@ const inboxMessageBodySchema = Type.Object({
|
|
|
135
209
|
const forkBodySchema = Type.Object({
|
|
136
210
|
instance_id: Type.Optional(Type.String()),
|
|
137
211
|
waitTimeoutMs: Type.Optional(Type.Number()),
|
|
212
|
+
// Optional anchor pointing at an event on the source root's `main`
|
|
213
|
+
// stream. Wire shape is snake_case; the route handler translates to
|
|
214
|
+
// camelCase before forwarding to entity-manager.
|
|
215
|
+
fork_pointer: Type.Optional(
|
|
216
|
+
Type.Object({
|
|
217
|
+
offset: Type.Union([Type.String(), Type.Null()]),
|
|
218
|
+
sub_offset: Type.Number(),
|
|
219
|
+
})
|
|
220
|
+
),
|
|
138
221
|
})
|
|
139
222
|
|
|
140
223
|
const setTagBodySchema = Type.Object({
|
|
@@ -204,6 +287,9 @@ type ScheduleBody = Static<typeof scheduleBodySchema>
|
|
|
204
287
|
type EventSourceSubscriptionBody = Static<
|
|
205
288
|
typeof eventSourceSubscriptionBodySchema
|
|
206
289
|
>
|
|
290
|
+
type EntityPermissionGrantInput = Static<
|
|
291
|
+
typeof entityPermissionGrantInputSchema
|
|
292
|
+
>
|
|
207
293
|
type AttachmentSubjectType = `inbox` | `run` | `text` | `tool_call` | `context`
|
|
208
294
|
type AttachmentRole = `input` | `output`
|
|
209
295
|
type ParsedAttachmentForm = {
|
|
@@ -237,88 +323,137 @@ entitiesRouter.put(
|
|
|
237
323
|
`/:type/:instanceId`,
|
|
238
324
|
withSpawnableEntityType,
|
|
239
325
|
withSchema(spawnBodySchema),
|
|
326
|
+
withSpawnPermission,
|
|
240
327
|
spawnEntity
|
|
241
328
|
)
|
|
242
|
-
entitiesRouter.get(
|
|
243
|
-
|
|
244
|
-
|
|
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
|
+
)
|
|
245
347
|
entitiesRouter.post(
|
|
246
348
|
`/:type/:instanceId/signal`,
|
|
247
349
|
withExistingEntity,
|
|
248
350
|
withSchema(signalBodySchema),
|
|
351
|
+
withEntityPermission(`signal`),
|
|
249
352
|
signalEntity
|
|
250
353
|
)
|
|
251
354
|
entitiesRouter.post(
|
|
252
355
|
`/:type/:instanceId/send`,
|
|
253
356
|
withExistingEntity,
|
|
254
357
|
withSchema(sendBodySchema),
|
|
358
|
+
withEntityPermission(`write`),
|
|
255
359
|
sendEntity
|
|
256
360
|
)
|
|
257
361
|
entitiesRouter.post(
|
|
258
362
|
`/:type/:instanceId/attachments`,
|
|
259
363
|
withExistingEntity,
|
|
364
|
+
withEntityPermission(`write`),
|
|
260
365
|
createAttachment
|
|
261
366
|
)
|
|
262
367
|
entitiesRouter.get(
|
|
263
368
|
`/:type/:instanceId/attachments/:attachmentId`,
|
|
264
369
|
withExistingEntity,
|
|
370
|
+
withEntityPermission(`read`),
|
|
265
371
|
readAttachment
|
|
266
372
|
)
|
|
267
373
|
entitiesRouter.delete(
|
|
268
374
|
`/:type/:instanceId/attachments/:attachmentId`,
|
|
269
375
|
withExistingEntity,
|
|
376
|
+
withEntityPermission(`write`),
|
|
270
377
|
deleteAttachment
|
|
271
378
|
)
|
|
272
379
|
entitiesRouter.patch(
|
|
273
380
|
`/:type/:instanceId/inbox/:messageKey`,
|
|
274
381
|
withExistingEntity,
|
|
275
382
|
withSchema(inboxMessageBodySchema),
|
|
383
|
+
withEntityPermission(`write`),
|
|
276
384
|
updateInboxMessage
|
|
277
385
|
)
|
|
278
386
|
entitiesRouter.delete(
|
|
279
387
|
`/:type/:instanceId/inbox/:messageKey`,
|
|
280
388
|
withExistingEntity,
|
|
389
|
+
withEntityPermission(`write`),
|
|
281
390
|
deleteInboxMessage
|
|
282
391
|
)
|
|
283
392
|
entitiesRouter.post(
|
|
284
393
|
`/:type/:instanceId/fork`,
|
|
285
394
|
withExistingEntity,
|
|
286
395
|
withSchema(forkBodySchema),
|
|
396
|
+
withEntityPermission(`fork`),
|
|
287
397
|
forkEntity
|
|
288
398
|
)
|
|
289
399
|
entitiesRouter.post(
|
|
290
400
|
`/:type/:instanceId/tags/:tagKey`,
|
|
291
401
|
withExistingEntity,
|
|
292
402
|
withSchema(setTagBodySchema),
|
|
403
|
+
withEntityPermission(`write`),
|
|
293
404
|
setTag
|
|
294
405
|
)
|
|
295
406
|
entitiesRouter.delete(
|
|
296
407
|
`/:type/:instanceId/tags/:tagKey`,
|
|
297
408
|
withExistingEntity,
|
|
409
|
+
withEntityPermission(`write`),
|
|
298
410
|
deleteTag
|
|
299
411
|
)
|
|
300
412
|
entitiesRouter.put(
|
|
301
413
|
`/:type/:instanceId/schedules/:scheduleId`,
|
|
302
414
|
withExistingEntity,
|
|
303
415
|
withSchema(scheduleBodySchema),
|
|
416
|
+
withEntityPermission(`schedule`),
|
|
304
417
|
upsertSchedule
|
|
305
418
|
)
|
|
306
419
|
entitiesRouter.delete(
|
|
307
420
|
`/:type/:instanceId/schedules/:scheduleId`,
|
|
308
421
|
withExistingEntity,
|
|
422
|
+
withEntityPermission(`schedule`),
|
|
309
423
|
deleteSchedule
|
|
310
424
|
)
|
|
311
425
|
entitiesRouter.put(
|
|
312
426
|
`/:type/:instanceId/event-source-subscriptions/:subscriptionId`,
|
|
313
427
|
withExistingEntity,
|
|
314
428
|
withSchema(eventSourceSubscriptionBodySchema),
|
|
429
|
+
withEntityPermission(`write`),
|
|
315
430
|
upsertEventSourceSubscription
|
|
316
431
|
)
|
|
317
432
|
entitiesRouter.delete(
|
|
318
433
|
`/:type/:instanceId/event-source-subscriptions/:subscriptionId`,
|
|
319
434
|
withExistingEntity,
|
|
435
|
+
withEntityPermission(`write`),
|
|
320
436
|
deleteEventSourceSubscription
|
|
321
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
|
+
)
|
|
322
457
|
|
|
323
458
|
function entityUrlFromSegments(
|
|
324
459
|
type: string,
|
|
@@ -492,6 +627,31 @@ function rejectPrincipalEntityMutation(
|
|
|
492
627
|
)
|
|
493
628
|
}
|
|
494
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
|
+
|
|
495
655
|
async function withExistingEntity(
|
|
496
656
|
request: AgentsRouteRequest,
|
|
497
657
|
ctx: TenantContext
|
|
@@ -563,9 +723,96 @@ async function withSpawnableEntityType(
|
|
|
563
723
|
)
|
|
564
724
|
}
|
|
565
725
|
|
|
726
|
+
request.spawnRoute = { entityType }
|
|
566
727
|
return undefined
|
|
567
728
|
}
|
|
568
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
|
+
|
|
569
816
|
async function listEntities(
|
|
570
817
|
{ query }: AgentsRouteRequest,
|
|
571
818
|
ctx: TenantContext
|
|
@@ -575,10 +822,61 @@ async function listEntities(
|
|
|
575
822
|
status: firstQueryValue(query.status),
|
|
576
823
|
parent: firstQueryValue(query.parent),
|
|
577
824
|
created_by: firstQueryValue(query.created_by),
|
|
825
|
+
readableBy: {
|
|
826
|
+
...principalSubject(ctx.principal),
|
|
827
|
+
bypass: isPermissionBypassPrincipal(ctx),
|
|
828
|
+
},
|
|
578
829
|
})
|
|
579
830
|
return json(entities.map((entity) => toPublicEntity(entity)))
|
|
580
831
|
}
|
|
581
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
|
+
|
|
582
880
|
async function upsertSchedule(
|
|
583
881
|
request: AgentsRouteRequest,
|
|
584
882
|
ctx: TenantContext
|
|
@@ -794,6 +1092,13 @@ async function forkEntity(
|
|
|
794
1092
|
const result = await ctx.entityManager.forkSubtree(entityUrl, {
|
|
795
1093
|
rootInstanceId: parsed.instance_id,
|
|
796
1094
|
waitTimeoutMs: parsed.waitTimeoutMs,
|
|
1095
|
+
createdBy: ctx.principal.url,
|
|
1096
|
+
...(parsed.fork_pointer && {
|
|
1097
|
+
forkPointer: {
|
|
1098
|
+
offset: parsed.fork_pointer.offset,
|
|
1099
|
+
subOffset: parsed.fork_pointer.sub_offset,
|
|
1100
|
+
},
|
|
1101
|
+
}),
|
|
797
1102
|
})
|
|
798
1103
|
for (const forkedEntity of result.entities) {
|
|
799
1104
|
await linkEntityDispatchSubscription(ctx, forkedEntity)
|
|
@@ -820,6 +1125,26 @@ async function sendEntity(
|
|
|
820
1125
|
`Request from must match Electric-Principal`
|
|
821
1126
|
)
|
|
822
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
|
+
}
|
|
823
1148
|
await ctx.entityManager.ensurePrincipal(principal)
|
|
824
1149
|
const { entityUrl, entity } = requireExistingEntityRoute(request)
|
|
825
1150
|
|
|
@@ -828,28 +1153,25 @@ async function sendEntity(
|
|
|
828
1153
|
: await backfillEntityDispatchPolicy(ctx, entity)
|
|
829
1154
|
await linkEntityDispatchSubscription(ctx, dispatchEntity)
|
|
830
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
|
+
|
|
831
1167
|
if (parsed.afterMs && parsed.afterMs > 0) {
|
|
832
1168
|
await ctx.entityManager.enqueueDelayedSend(
|
|
833
1169
|
entityUrl,
|
|
834
|
-
|
|
835
|
-
from: principal.url,
|
|
836
|
-
payload: parsed.payload,
|
|
837
|
-
key: parsed.key,
|
|
838
|
-
type: parsed.type,
|
|
839
|
-
mode: parsed.mode,
|
|
840
|
-
position: parsed.position,
|
|
841
|
-
},
|
|
1170
|
+
sendReq,
|
|
842
1171
|
new Date(Date.now() + parsed.afterMs)
|
|
843
1172
|
)
|
|
844
1173
|
} else {
|
|
845
|
-
await ctx.entityManager.send(entityUrl,
|
|
846
|
-
from: principal.url,
|
|
847
|
-
payload: parsed.payload,
|
|
848
|
-
key: parsed.key,
|
|
849
|
-
type: parsed.type,
|
|
850
|
-
mode: parsed.mode,
|
|
851
|
-
position: parsed.position,
|
|
852
|
-
})
|
|
1174
|
+
await ctx.entityManager.send(entityUrl, sendReq)
|
|
853
1175
|
}
|
|
854
1176
|
|
|
855
1177
|
return status(204)
|
|
@@ -969,10 +1291,30 @@ async function spawnEntity(
|
|
|
969
1291
|
tags: parsed.tags,
|
|
970
1292
|
parent: parsed.parent,
|
|
971
1293
|
dispatch_policy: dispatchPolicy,
|
|
1294
|
+
sandbox: parsed.sandbox,
|
|
972
1295
|
initialMessage: undefined,
|
|
973
1296
|
wake: parsed.wake,
|
|
974
1297
|
created_by: principal.url,
|
|
975
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
|
+
}
|
|
976
1318
|
const linkBeforeInitialMessage =
|
|
977
1319
|
parsed.initialMessage !== undefined &&
|
|
978
1320
|
shouldLinkDispatchBeforeInitialMessage(dispatchPolicy)
|