@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/server.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { apiError } from './electric-agents-http.js'
|
|
|
16
16
|
import {
|
|
17
17
|
ErrCodeInvalidRequest,
|
|
18
18
|
ErrCodeUnauthorized,
|
|
19
|
+
type AuthorizeRequest,
|
|
19
20
|
} from './electric-agents-types.js'
|
|
20
21
|
import { ElectricAgentsError } from './entity-manager.js'
|
|
21
22
|
import { serverLog } from './utils/log.js'
|
|
@@ -67,6 +68,7 @@ export interface ElectricAgentsServerOptions {
|
|
|
67
68
|
authenticateRequest?: (
|
|
68
69
|
request: Request
|
|
69
70
|
) => Promise<Principal | null> | Principal | null
|
|
71
|
+
authorizeRequest?: AuthorizeRequest
|
|
70
72
|
allowDevPrincipalFallback?: boolean
|
|
71
73
|
eventSources?: EventSourceCatalog
|
|
72
74
|
ensureEventSourceWakeSource?: (sourceUrl: string) => Promise<void> | void
|
|
@@ -453,6 +455,9 @@ export class ElectricAgentsServer {
|
|
|
453
455
|
this.options.ensureEventSourceWakeSource,
|
|
454
456
|
}
|
|
455
457
|
: {}),
|
|
458
|
+
...(this.options.authorizeRequest
|
|
459
|
+
? { authorizeRequest: this.options.authorizeRequest }
|
|
460
|
+
: {}),
|
|
456
461
|
isShuttingDown: () => this.shuttingDown,
|
|
457
462
|
mockAgent: this.mockAgentBootstrap
|
|
458
463
|
? { runtime: this.mockAgentBootstrap.runtime }
|
package/src/stream-client.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
FetchError,
|
|
5
5
|
IdempotentProducer,
|
|
6
6
|
} from '@durable-streams/client'
|
|
7
|
+
import type { EventPointer } from '@electric-ax/agents-runtime'
|
|
7
8
|
import { ErrCodeNotFound } from './electric-agents-types.js'
|
|
8
9
|
import { ATTR, injectTraceHeaders, withSpan } from './tracing.js'
|
|
9
10
|
import type { HeadersRecord, MaybePromise } from '@durable-streams/client'
|
|
@@ -242,7 +243,11 @@ export class StreamClient {
|
|
|
242
243
|
})
|
|
243
244
|
}
|
|
244
245
|
|
|
245
|
-
async fork(
|
|
246
|
+
async fork(
|
|
247
|
+
path: string,
|
|
248
|
+
sourcePath: string,
|
|
249
|
+
opts?: { forkPointer?: EventPointer }
|
|
250
|
+
): Promise<void> {
|
|
246
251
|
return await withSpan(`stream.fork`, async (span) => {
|
|
247
252
|
span.setAttributes({
|
|
248
253
|
[ATTR.STREAM_PATH]: path,
|
|
@@ -252,6 +257,17 @@ export class StreamClient {
|
|
|
252
257
|
'content-type': `application/json`,
|
|
253
258
|
'Stream-Forked-From': new URL(this.streamUrl(sourcePath)).pathname,
|
|
254
259
|
}
|
|
260
|
+
if (opts?.forkPointer) {
|
|
261
|
+
// The durable-streams server returns 400 if Stream-Fork-Sub-Offset
|
|
262
|
+
// > 0 without an accompanying Stream-Fork-Offset. When our
|
|
263
|
+
// pointer's offset is `null` (anchor at stream start), send the
|
|
264
|
+
// explicit zero-offset string to satisfy that constraint.
|
|
265
|
+
const ZERO_OFFSET = `0000000000000000_0000000000000000`
|
|
266
|
+
headers[`Stream-Fork-Offset`] = opts.forkPointer.offset ?? ZERO_OFFSET
|
|
267
|
+
if (opts.forkPointer.subOffset > 0) {
|
|
268
|
+
headers[`Stream-Fork-Sub-Offset`] = String(opts.forkPointer.subOffset)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
255
271
|
injectTraceHeaders(headers)
|
|
256
272
|
|
|
257
273
|
const response = await fetch(this.streamUrl(path), {
|
|
@@ -96,6 +96,8 @@ export function buildElectricProxyTarget(options: {
|
|
|
96
96
|
electricSecret?: string
|
|
97
97
|
tenantId: string
|
|
98
98
|
principalUrl?: string
|
|
99
|
+
principalKind?: string
|
|
100
|
+
permissionBypass?: boolean
|
|
99
101
|
}): URL {
|
|
100
102
|
const targetPath = options.incomingUrl.pathname.replace(
|
|
101
103
|
`/_electric/electric`,
|
|
@@ -119,23 +121,59 @@ export function buildElectricProxyTarget(options: {
|
|
|
119
121
|
if (table === `entities`) {
|
|
120
122
|
target.searchParams.set(
|
|
121
123
|
`columns`,
|
|
122
|
-
`"tenant_id","url","type","status","dispatch_policy","tags","spawn_args","parent","type_revision","inbox_schemas","state_schemas","created_at","updated_at"`
|
|
124
|
+
`"tenant_id","url","type","status","dispatch_policy","tags","spawn_args","sandbox","parent","created_by","type_revision","inbox_schemas","state_schemas","created_at","updated_at"`
|
|
125
|
+
)
|
|
126
|
+
applyShapeWhere(
|
|
127
|
+
target,
|
|
128
|
+
buildReadableEntitiesWhere({
|
|
129
|
+
tenantId: options.tenantId,
|
|
130
|
+
principalUrl: options.principalUrl ?? ``,
|
|
131
|
+
principalKind: options.principalKind ?? ``,
|
|
132
|
+
permissionBypass: options.permissionBypass,
|
|
133
|
+
})
|
|
123
134
|
)
|
|
124
|
-
applyTenantShapeWhere(target, options.tenantId)
|
|
125
135
|
} else if (table === `entity_types`) {
|
|
126
136
|
target.searchParams.set(
|
|
127
137
|
`columns`,
|
|
128
138
|
`"tenant_id","name","description","creation_schema","inbox_schemas","state_schemas","serve_endpoint","default_dispatch_policy","revision","created_at","updated_at"`
|
|
129
139
|
)
|
|
130
|
-
|
|
140
|
+
applyShapeWhere(
|
|
141
|
+
target,
|
|
142
|
+
buildSpawnableEntityTypesWhere({
|
|
143
|
+
tenantId: options.tenantId,
|
|
144
|
+
principalUrl: options.principalUrl ?? ``,
|
|
145
|
+
principalKind: options.principalKind ?? ``,
|
|
146
|
+
permissionBypass: options.permissionBypass,
|
|
147
|
+
})
|
|
148
|
+
)
|
|
131
149
|
} else if (table === `runners`) {
|
|
132
150
|
target.searchParams.set(
|
|
133
151
|
`columns`,
|
|
134
|
-
`"tenant_id","id","owner_principal","label","kind","admin_status","wake_stream","created_at","updated_at"`
|
|
152
|
+
`"tenant_id","id","owner_principal","label","kind","admin_status","wake_stream","sandbox_profiles","created_at","updated_at"`
|
|
135
153
|
)
|
|
136
154
|
applyTenantShapeWhere(target, options.tenantId, [
|
|
137
155
|
`owner_principal = ${sqlStringLiteral(options.principalUrl ?? ``)}`,
|
|
138
156
|
])
|
|
157
|
+
} else if (table === `users`) {
|
|
158
|
+
target.searchParams.set(
|
|
159
|
+
`columns`,
|
|
160
|
+
`"tenant_id","id","display_name","email","avatar_url","created_at","updated_at"`
|
|
161
|
+
)
|
|
162
|
+
applyTenantShapeWhere(target, options.tenantId)
|
|
163
|
+
} else if (table === `entity_effective_permissions`) {
|
|
164
|
+
target.searchParams.set(
|
|
165
|
+
`columns`,
|
|
166
|
+
`"tenant_id","id","entity_url","source_entity_url","source_grant_id","permission","subject_kind","subject_value","expires_at","created_at"`
|
|
167
|
+
)
|
|
168
|
+
applyShapeWhere(
|
|
169
|
+
target,
|
|
170
|
+
buildCurrentPrincipalEntityEffectivePermissionsWhere({
|
|
171
|
+
tenantId: options.tenantId,
|
|
172
|
+
principalUrl: options.principalUrl ?? ``,
|
|
173
|
+
principalKind: options.principalKind ?? ``,
|
|
174
|
+
permissionBypass: options.permissionBypass,
|
|
175
|
+
})
|
|
176
|
+
)
|
|
139
177
|
} else if (table === `runner_runtime_diagnostics`) {
|
|
140
178
|
target.searchParams.set(
|
|
141
179
|
`columns`,
|
|
@@ -149,24 +187,154 @@ export function buildElectricProxyTarget(options: {
|
|
|
149
187
|
`columns`,
|
|
150
188
|
`"tenant_id","entity_url","pending_source_streams","pending_reason","pending_since","outstanding_wake_id","outstanding_wake_target","outstanding_wake_created_at","active_consumer_id","active_runner_id","active_epoch","active_claimed_at","active_lease_expires_at","last_wake_id","last_claimed_at","last_released_at","last_completed_at","last_error","updated_at"`
|
|
151
189
|
)
|
|
152
|
-
|
|
190
|
+
applyShapeWhere(
|
|
191
|
+
target,
|
|
192
|
+
buildReadableEntityUrlWhere({
|
|
193
|
+
tenantId: options.tenantId,
|
|
194
|
+
principalUrl: options.principalUrl ?? ``,
|
|
195
|
+
principalKind: options.principalKind ?? ``,
|
|
196
|
+
permissionBypass: options.permissionBypass,
|
|
197
|
+
})
|
|
198
|
+
)
|
|
153
199
|
} else if (table === `wake_notifications`) {
|
|
154
200
|
target.searchParams.set(
|
|
155
201
|
`columns`,
|
|
156
202
|
`"tenant_id","wake_id","entity_url","target_type","target_runner_id","target_webhook_url","target_worker_pool_id","runner_wake_stream","runner_wake_stream_offset","notification_public","delivery_status","claim_status","created_at","delivered_at","claimed_at","resolved_at"`
|
|
157
203
|
)
|
|
158
|
-
|
|
204
|
+
applyShapeWhere(
|
|
205
|
+
target,
|
|
206
|
+
buildReadableEntityUrlWhere({
|
|
207
|
+
tenantId: options.tenantId,
|
|
208
|
+
principalUrl: options.principalUrl ?? ``,
|
|
209
|
+
principalKind: options.principalKind ?? ``,
|
|
210
|
+
permissionBypass: options.permissionBypass,
|
|
211
|
+
})
|
|
212
|
+
)
|
|
159
213
|
} else if (table === `consumer_claims`) {
|
|
160
214
|
target.searchParams.set(
|
|
161
215
|
`columns`,
|
|
162
216
|
`"tenant_id","consumer_id","epoch","wake_id","entity_url","stream_path","runner_id","status","claimed_at","last_heartbeat_at","lease_expires_at","released_at","acked_streams","updated_at"`
|
|
163
217
|
)
|
|
164
|
-
|
|
218
|
+
applyShapeWhere(
|
|
219
|
+
target,
|
|
220
|
+
buildReadableEntityUrlWhere({
|
|
221
|
+
tenantId: options.tenantId,
|
|
222
|
+
principalUrl: options.principalUrl ?? ``,
|
|
223
|
+
principalKind: options.principalKind ?? ``,
|
|
224
|
+
permissionBypass: options.permissionBypass,
|
|
225
|
+
})
|
|
226
|
+
)
|
|
165
227
|
}
|
|
166
228
|
|
|
167
229
|
return target
|
|
168
230
|
}
|
|
169
231
|
|
|
232
|
+
export function buildReadableEntitiesWhere(options: {
|
|
233
|
+
tenantId: string
|
|
234
|
+
principalUrl: string
|
|
235
|
+
principalKind: string
|
|
236
|
+
permissionBypass?: boolean
|
|
237
|
+
}): string {
|
|
238
|
+
const tenant = sqlStringLiteral(options.tenantId)
|
|
239
|
+
if (options.permissionBypass) {
|
|
240
|
+
return `tenant_id = ${tenant}`
|
|
241
|
+
}
|
|
242
|
+
const principalUrl = sqlStringLiteral(options.principalUrl)
|
|
243
|
+
const principalKind = sqlStringLiteral(options.principalKind)
|
|
244
|
+
return [
|
|
245
|
+
`tenant_id = ${tenant}`,
|
|
246
|
+
`AND (`,
|
|
247
|
+
` created_by = ${principalUrl}`,
|
|
248
|
+
` OR url IN (`,
|
|
249
|
+
` SELECT entity_url`,
|
|
250
|
+
` FROM entity_effective_permissions`,
|
|
251
|
+
` WHERE tenant_id = ${tenant}`,
|
|
252
|
+
` AND permission IN ('read', 'manage')`,
|
|
253
|
+
` AND (`,
|
|
254
|
+
` (subject_kind = 'principal' AND subject_value = ${principalUrl})`,
|
|
255
|
+
` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
|
|
256
|
+
` )`,
|
|
257
|
+
` )`,
|
|
258
|
+
`)`,
|
|
259
|
+
].join(`\n`)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function buildReadableEntityUrlWhere(options: {
|
|
263
|
+
tenantId: string
|
|
264
|
+
principalUrl: string
|
|
265
|
+
principalKind: string
|
|
266
|
+
permissionBypass?: boolean
|
|
267
|
+
}): string {
|
|
268
|
+
const tenant = sqlStringLiteral(options.tenantId)
|
|
269
|
+
if (options.permissionBypass) {
|
|
270
|
+
return `tenant_id = ${tenant}`
|
|
271
|
+
}
|
|
272
|
+
return [
|
|
273
|
+
`tenant_id = ${tenant}`,
|
|
274
|
+
`AND entity_url IN (`,
|
|
275
|
+
` SELECT url`,
|
|
276
|
+
` FROM entities`,
|
|
277
|
+
` WHERE ${indentWhere(
|
|
278
|
+
buildReadableEntitiesWhere(options),
|
|
279
|
+
` `
|
|
280
|
+
).trimStart()}`,
|
|
281
|
+
`)`,
|
|
282
|
+
].join(`\n`)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function buildCurrentPrincipalEntityEffectivePermissionsWhere(options: {
|
|
286
|
+
tenantId: string
|
|
287
|
+
principalUrl: string
|
|
288
|
+
principalKind: string
|
|
289
|
+
permissionBypass?: boolean
|
|
290
|
+
}): string {
|
|
291
|
+
const tenant = sqlStringLiteral(options.tenantId)
|
|
292
|
+
if (options.permissionBypass) {
|
|
293
|
+
return `tenant_id = ${tenant}`
|
|
294
|
+
}
|
|
295
|
+
const principalUrl = sqlStringLiteral(options.principalUrl)
|
|
296
|
+
const principalKind = sqlStringLiteral(options.principalKind)
|
|
297
|
+
return [
|
|
298
|
+
`tenant_id = ${tenant}`,
|
|
299
|
+
`AND (`,
|
|
300
|
+
` (subject_kind = 'principal' AND subject_value = ${principalUrl})`,
|
|
301
|
+
` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
|
|
302
|
+
`)`,
|
|
303
|
+
`AND entity_url IN (`,
|
|
304
|
+
` SELECT url`,
|
|
305
|
+
` FROM entities`,
|
|
306
|
+
` WHERE ${buildReadableEntitiesWhere(options)}`,
|
|
307
|
+
`)`,
|
|
308
|
+
].join(`\n`)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function buildSpawnableEntityTypesWhere(options: {
|
|
312
|
+
tenantId: string
|
|
313
|
+
principalUrl: string
|
|
314
|
+
principalKind: string
|
|
315
|
+
permissionBypass?: boolean
|
|
316
|
+
}): string {
|
|
317
|
+
const tenant = sqlStringLiteral(options.tenantId)
|
|
318
|
+
if (options.permissionBypass) {
|
|
319
|
+
return `tenant_id = ${tenant}`
|
|
320
|
+
}
|
|
321
|
+
const principalUrl = sqlStringLiteral(options.principalUrl)
|
|
322
|
+
const principalKind = sqlStringLiteral(options.principalKind)
|
|
323
|
+
return [
|
|
324
|
+
`tenant_id = ${tenant}`,
|
|
325
|
+
`AND name IN (`,
|
|
326
|
+
` SELECT entity_type`,
|
|
327
|
+
` FROM entity_type_permission_grants`,
|
|
328
|
+
` WHERE tenant_id = ${tenant}`,
|
|
329
|
+
` AND permission IN ('spawn', 'manage')`,
|
|
330
|
+
` AND (`,
|
|
331
|
+
` (subject_kind = 'principal' AND subject_value = ${principalUrl})`,
|
|
332
|
+
` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
|
|
333
|
+
` )`,
|
|
334
|
+
`)`,
|
|
335
|
+
].join(`\n`)
|
|
336
|
+
}
|
|
337
|
+
|
|
170
338
|
export async function forwardFetchRequest(options: {
|
|
171
339
|
request: {
|
|
172
340
|
method: string
|
|
@@ -248,17 +416,29 @@ function applyTenantShapeWhere(
|
|
|
248
416
|
tenantId: string,
|
|
249
417
|
extraConditions: Array<string> = []
|
|
250
418
|
): void {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
...extraConditions
|
|
254
|
-
|
|
419
|
+
applyShapeWhere(
|
|
420
|
+
target,
|
|
421
|
+
[`tenant_id = ${sqlStringLiteral(tenantId)}`, ...extraConditions].join(
|
|
422
|
+
` AND `
|
|
423
|
+
)
|
|
424
|
+
)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function applyShapeWhere(target: URL, enforcedWhere: string): void {
|
|
255
428
|
const existingWhere = target.searchParams.get(`where`)
|
|
256
429
|
target.searchParams.set(
|
|
257
430
|
`where`,
|
|
258
|
-
existingWhere ? `${
|
|
431
|
+
existingWhere ? `${enforcedWhere} AND (${existingWhere})` : enforcedWhere
|
|
259
432
|
)
|
|
260
433
|
}
|
|
261
434
|
|
|
262
435
|
function sqlStringLiteral(value: string): string {
|
|
263
436
|
return `'${value.replace(/'/g, `''`)}'`
|
|
264
437
|
}
|
|
438
|
+
|
|
439
|
+
function indentWhere(where: string, prefix: string): string {
|
|
440
|
+
return where
|
|
441
|
+
.split(`\n`)
|
|
442
|
+
.map((line) => `${prefix}${line}`)
|
|
443
|
+
.join(`\n`)
|
|
444
|
+
}
|
package/src/wake-registry.ts
CHANGED
|
@@ -41,6 +41,12 @@ export interface WakeEvalResult {
|
|
|
41
41
|
collection: string
|
|
42
42
|
kind: `insert` | `update` | `delete`
|
|
43
43
|
key: string
|
|
44
|
+
from?: string
|
|
45
|
+
from_principal?: string
|
|
46
|
+
from_agent?: string
|
|
47
|
+
payload?: unknown
|
|
48
|
+
timestamp?: string
|
|
49
|
+
message_type?: string
|
|
44
50
|
}>
|
|
45
51
|
}
|
|
46
52
|
runFinishedStatus?: `completed` | `failed`
|
|
@@ -885,11 +891,7 @@ export class WakeRegistry {
|
|
|
885
891
|
reg: WakeRegistration,
|
|
886
892
|
event: Record<string, unknown>
|
|
887
893
|
): {
|
|
888
|
-
change:
|
|
889
|
-
collection: string
|
|
890
|
-
kind: `insert` | `update` | `delete`
|
|
891
|
-
key: string
|
|
892
|
-
}
|
|
894
|
+
change: WakeEvalResult[`wakeMessage`][`changes`][number]
|
|
893
895
|
runFinishedStatus?: `completed` | `failed`
|
|
894
896
|
} | null {
|
|
895
897
|
if (reg.condition === `runFinished`) {
|
|
@@ -935,12 +937,29 @@ export class WakeRegistry {
|
|
|
935
937
|
return null
|
|
936
938
|
}
|
|
937
939
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
key: (event.key as string) || ``,
|
|
943
|
-
},
|
|
940
|
+
const change: WakeEvalResult[`wakeMessage`][`changes`][number] = {
|
|
941
|
+
collection: eventType,
|
|
942
|
+
kind,
|
|
943
|
+
key: (event.key as string) || ``,
|
|
944
944
|
}
|
|
945
|
+
|
|
946
|
+
if (eventType === `inbox`) {
|
|
947
|
+
const value = event.value as Record<string, unknown> | undefined
|
|
948
|
+
if (typeof value?.from === `string`) change.from = value.from
|
|
949
|
+
if (typeof value?.from_principal === `string`) {
|
|
950
|
+
change.from_principal = value.from_principal
|
|
951
|
+
}
|
|
952
|
+
if (typeof value?.from_agent === `string`) {
|
|
953
|
+
change.from_agent = value.from_agent
|
|
954
|
+
}
|
|
955
|
+
if (`payload` in (value ?? {})) change.payload = value?.payload
|
|
956
|
+
if (typeof value?.timestamp === `string`)
|
|
957
|
+
change.timestamp = value.timestamp
|
|
958
|
+
if (typeof value?.message_type === `string`) {
|
|
959
|
+
change.message_type = value.message_type
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
return { change }
|
|
945
964
|
}
|
|
946
965
|
}
|