@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/dist/index.cjs
CHANGED
|
@@ -41,8 +41,8 @@ const __electric_ax_agents_runtime = __toESM(require("@electric-ax/agents-runtim
|
|
|
41
41
|
const __durable_streams_client = __toESM(require("@durable-streams/client"));
|
|
42
42
|
const __electric_sql_client = __toESM(require("@electric-sql/client"));
|
|
43
43
|
const pino = __toESM(require("pino"));
|
|
44
|
-
const fastq = __toESM(require("fastq"));
|
|
45
44
|
const __sinclair_typebox = __toESM(require("@sinclair/typebox"));
|
|
45
|
+
const fastq = __toESM(require("fastq"));
|
|
46
46
|
const ajv = __toESM(require("ajv"));
|
|
47
47
|
const __opentelemetry_api = __toESM(require("@opentelemetry/api"));
|
|
48
48
|
const itty_router = __toESM(require("itty-router"));
|
|
@@ -55,11 +55,16 @@ __export(schema_exports, {
|
|
|
55
55
|
entities: () => entities,
|
|
56
56
|
entityBridges: () => entityBridges,
|
|
57
57
|
entityDispatchState: () => entityDispatchState,
|
|
58
|
+
entityEffectivePermissions: () => entityEffectivePermissions,
|
|
59
|
+
entityLineage: () => entityLineage,
|
|
58
60
|
entityManifestSources: () => entityManifestSources,
|
|
61
|
+
entityPermissionGrants: () => entityPermissionGrants,
|
|
62
|
+
entityTypePermissionGrants: () => entityTypePermissionGrants,
|
|
59
63
|
entityTypes: () => entityTypes,
|
|
60
64
|
runnerRuntimeDiagnostics: () => runnerRuntimeDiagnostics,
|
|
61
65
|
runners: () => runners,
|
|
62
66
|
scheduledTasks: () => scheduledTasks,
|
|
67
|
+
sharedStateLinks: () => sharedStateLinks,
|
|
63
68
|
subscriptionWebhooks: () => subscriptionWebhooks,
|
|
64
69
|
tagStreamOutbox: () => tagStreamOutbox,
|
|
65
70
|
users: () => users,
|
|
@@ -90,6 +95,7 @@ const entities = (0, drizzle_orm_pg_core.pgTable)(`entities`, {
|
|
|
90
95
|
tags: (0, drizzle_orm_pg_core.jsonb)(`tags`).notNull().default({}),
|
|
91
96
|
tagsIndex: (0, drizzle_orm_pg_core.text)(`tags_index`).array().notNull().default(drizzle_orm.sql`'{}'::text[]`),
|
|
92
97
|
spawnArgs: (0, drizzle_orm_pg_core.jsonb)(`spawn_args`).default({}),
|
|
98
|
+
sandbox: (0, drizzle_orm_pg_core.jsonb)(`sandbox`),
|
|
93
99
|
parent: (0, drizzle_orm_pg_core.text)(`parent`),
|
|
94
100
|
createdBy: (0, drizzle_orm_pg_core.text)(`created_by`),
|
|
95
101
|
typeRevision: (0, drizzle_orm_pg_core.integer)(`type_revision`),
|
|
@@ -106,6 +112,94 @@ const entities = (0, drizzle_orm_pg_core.pgTable)(`entities`, {
|
|
|
106
112
|
(0, drizzle_orm_pg_core.index)(`entities_tags_index_gin`).using(`gin`, table.tagsIndex),
|
|
107
113
|
(0, drizzle_orm_pg_core.check)(`chk_entities_status`, drizzle_orm.sql`${table.status} IN ('spawning', 'running', 'idle', 'paused', 'stopping', 'stopped', 'killed')`)
|
|
108
114
|
]);
|
|
115
|
+
const entityTypePermissionGrants = (0, drizzle_orm_pg_core.pgTable)(`entity_type_permission_grants`, {
|
|
116
|
+
id: (0, drizzle_orm_pg_core.bigserial)(`id`, { mode: `number` }).primaryKey(),
|
|
117
|
+
tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
|
|
118
|
+
entityType: (0, drizzle_orm_pg_core.text)(`entity_type`).notNull(),
|
|
119
|
+
permission: (0, drizzle_orm_pg_core.text)(`permission`).notNull(),
|
|
120
|
+
subjectKind: (0, drizzle_orm_pg_core.text)(`subject_kind`).notNull(),
|
|
121
|
+
subjectValue: (0, drizzle_orm_pg_core.text)(`subject_value`).notNull(),
|
|
122
|
+
createdBy: (0, drizzle_orm_pg_core.text)(`created_by`),
|
|
123
|
+
expiresAt: (0, drizzle_orm_pg_core.timestamp)(`expires_at`, { withTimezone: true }),
|
|
124
|
+
createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
125
|
+
updatedAt: (0, drizzle_orm_pg_core.timestamp)(`updated_at`, { withTimezone: true }).notNull().defaultNow()
|
|
126
|
+
}, (table) => [
|
|
127
|
+
(0, drizzle_orm_pg_core.index)(`idx_type_permission_grants_lookup`).on(table.tenantId, table.entityType, table.permission, table.subjectKind, table.subjectValue),
|
|
128
|
+
(0, drizzle_orm_pg_core.index)(`idx_type_permission_grants_expiry`).on(table.tenantId, table.expiresAt),
|
|
129
|
+
(0, drizzle_orm_pg_core.check)(`chk_type_permission_grants_permission`, drizzle_orm.sql`${table.permission} IN ('spawn', 'manage')`),
|
|
130
|
+
(0, drizzle_orm_pg_core.check)(`chk_type_permission_grants_subject_kind`, drizzle_orm.sql`${table.subjectKind} IN ('principal', 'principal_kind')`)
|
|
131
|
+
]);
|
|
132
|
+
const entityLineage = (0, drizzle_orm_pg_core.pgTable)(`entity_lineage`, {
|
|
133
|
+
tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
|
|
134
|
+
ancestorUrl: (0, drizzle_orm_pg_core.text)(`ancestor_url`).notNull(),
|
|
135
|
+
descendantUrl: (0, drizzle_orm_pg_core.text)(`descendant_url`).notNull(),
|
|
136
|
+
depth: (0, drizzle_orm_pg_core.integer)(`depth`).notNull(),
|
|
137
|
+
createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow()
|
|
138
|
+
}, (table) => [
|
|
139
|
+
(0, drizzle_orm_pg_core.primaryKey)({ columns: [
|
|
140
|
+
table.tenantId,
|
|
141
|
+
table.ancestorUrl,
|
|
142
|
+
table.descendantUrl
|
|
143
|
+
] }),
|
|
144
|
+
(0, drizzle_orm_pg_core.index)(`idx_entity_lineage_descendant`).on(table.tenantId, table.descendantUrl),
|
|
145
|
+
(0, drizzle_orm_pg_core.check)(`chk_entity_lineage_depth`, drizzle_orm.sql`${table.depth} >= 0`)
|
|
146
|
+
]);
|
|
147
|
+
const entityPermissionGrants = (0, drizzle_orm_pg_core.pgTable)(`entity_permission_grants`, {
|
|
148
|
+
id: (0, drizzle_orm_pg_core.bigserial)(`id`, { mode: `number` }).primaryKey(),
|
|
149
|
+
tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
|
|
150
|
+
entityUrl: (0, drizzle_orm_pg_core.text)(`entity_url`).notNull(),
|
|
151
|
+
permission: (0, drizzle_orm_pg_core.text)(`permission`).notNull(),
|
|
152
|
+
subjectKind: (0, drizzle_orm_pg_core.text)(`subject_kind`).notNull(),
|
|
153
|
+
subjectValue: (0, drizzle_orm_pg_core.text)(`subject_value`).notNull(),
|
|
154
|
+
propagation: (0, drizzle_orm_pg_core.text)(`propagation`).notNull().default(`self`),
|
|
155
|
+
copyToChildren: (0, drizzle_orm_pg_core.boolean)(`copy_to_children`).notNull().default(false),
|
|
156
|
+
createdBy: (0, drizzle_orm_pg_core.text)(`created_by`),
|
|
157
|
+
expiresAt: (0, drizzle_orm_pg_core.timestamp)(`expires_at`, { withTimezone: true }),
|
|
158
|
+
createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
159
|
+
updatedAt: (0, drizzle_orm_pg_core.timestamp)(`updated_at`, { withTimezone: true }).notNull().defaultNow()
|
|
160
|
+
}, (table) => [
|
|
161
|
+
(0, drizzle_orm_pg_core.index)(`idx_entity_permission_grants_entity`).on(table.tenantId, table.entityUrl),
|
|
162
|
+
(0, drizzle_orm_pg_core.index)(`idx_entity_permission_grants_subject`).on(table.tenantId, table.permission, table.subjectKind, table.subjectValue),
|
|
163
|
+
(0, drizzle_orm_pg_core.index)(`idx_entity_permission_grants_expiry`).on(table.tenantId, table.expiresAt),
|
|
164
|
+
(0, drizzle_orm_pg_core.check)(`chk_entity_permission_grants_permission`, drizzle_orm.sql`${table.permission} IN ('read', 'write', 'delete', 'signal', 'fork', 'schedule', 'spawn', 'manage')`),
|
|
165
|
+
(0, drizzle_orm_pg_core.check)(`chk_entity_permission_grants_subject_kind`, drizzle_orm.sql`${table.subjectKind} IN ('principal', 'principal_kind')`),
|
|
166
|
+
(0, drizzle_orm_pg_core.check)(`chk_entity_permission_grants_propagation`, drizzle_orm.sql`${table.propagation} IN ('self', 'descendants')`)
|
|
167
|
+
]);
|
|
168
|
+
const entityEffectivePermissions = (0, drizzle_orm_pg_core.pgTable)(`entity_effective_permissions`, {
|
|
169
|
+
id: (0, drizzle_orm_pg_core.bigserial)(`id`, { mode: `number` }).primaryKey(),
|
|
170
|
+
tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
|
|
171
|
+
entityUrl: (0, drizzle_orm_pg_core.text)(`entity_url`).notNull(),
|
|
172
|
+
sourceEntityUrl: (0, drizzle_orm_pg_core.text)(`source_entity_url`).notNull(),
|
|
173
|
+
sourceGrantId: (0, drizzle_orm_pg_core.bigint)(`source_grant_id`, { mode: `number` }).notNull(),
|
|
174
|
+
permission: (0, drizzle_orm_pg_core.text)(`permission`).notNull(),
|
|
175
|
+
subjectKind: (0, drizzle_orm_pg_core.text)(`subject_kind`).notNull(),
|
|
176
|
+
subjectValue: (0, drizzle_orm_pg_core.text)(`subject_value`).notNull(),
|
|
177
|
+
expiresAt: (0, drizzle_orm_pg_core.timestamp)(`expires_at`, { withTimezone: true }),
|
|
178
|
+
createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow()
|
|
179
|
+
}, (table) => [
|
|
180
|
+
(0, drizzle_orm_pg_core.unique)(`uq_entity_effective_permission`).on(table.tenantId, table.entityUrl, table.sourceGrantId),
|
|
181
|
+
(0, drizzle_orm_pg_core.index)(`idx_entity_effective_permissions_lookup`).on(table.tenantId, table.permission, table.subjectKind, table.subjectValue, table.entityUrl),
|
|
182
|
+
(0, drizzle_orm_pg_core.index)(`idx_entity_effective_permissions_entity`).on(table.tenantId, table.entityUrl),
|
|
183
|
+
(0, drizzle_orm_pg_core.index)(`idx_entity_effective_permissions_expiry`).on(table.tenantId, table.expiresAt),
|
|
184
|
+
(0, drizzle_orm_pg_core.check)(`chk_entity_effective_permissions_permission`, drizzle_orm.sql`${table.permission} IN ('read', 'write', 'delete', 'signal', 'fork', 'schedule', 'spawn', 'manage')`),
|
|
185
|
+
(0, drizzle_orm_pg_core.check)(`chk_entity_effective_permissions_subject_kind`, drizzle_orm.sql`${table.subjectKind} IN ('principal', 'principal_kind')`)
|
|
186
|
+
]);
|
|
187
|
+
const sharedStateLinks = (0, drizzle_orm_pg_core.pgTable)(`shared_state_links`, {
|
|
188
|
+
tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
|
|
189
|
+
sharedStateId: (0, drizzle_orm_pg_core.text)(`shared_state_id`).notNull(),
|
|
190
|
+
ownerEntityUrl: (0, drizzle_orm_pg_core.text)(`owner_entity_url`).notNull(),
|
|
191
|
+
manifestKey: (0, drizzle_orm_pg_core.text)(`manifest_key`).notNull(),
|
|
192
|
+
createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
193
|
+
updatedAt: (0, drizzle_orm_pg_core.timestamp)(`updated_at`, { withTimezone: true }).notNull().defaultNow()
|
|
194
|
+
}, (table) => [
|
|
195
|
+
(0, drizzle_orm_pg_core.primaryKey)({ columns: [
|
|
196
|
+
table.tenantId,
|
|
197
|
+
table.ownerEntityUrl,
|
|
198
|
+
table.manifestKey
|
|
199
|
+
] }),
|
|
200
|
+
(0, drizzle_orm_pg_core.index)(`idx_shared_state_links_shared_state`).on(table.tenantId, table.sharedStateId),
|
|
201
|
+
(0, drizzle_orm_pg_core.index)(`idx_shared_state_links_owner`).on(table.tenantId, table.ownerEntityUrl)
|
|
202
|
+
]);
|
|
109
203
|
const users = (0, drizzle_orm_pg_core.pgTable)(`users`, {
|
|
110
204
|
tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
|
|
111
205
|
id: (0, drizzle_orm_pg_core.text)(`id`).notNull(),
|
|
@@ -131,6 +225,7 @@ const runners = (0, drizzle_orm_pg_core.pgTable)(`runners`, {
|
|
|
131
225
|
kind: (0, drizzle_orm_pg_core.text)(`kind`).notNull().default(`local`),
|
|
132
226
|
adminStatus: (0, drizzle_orm_pg_core.text)(`admin_status`).notNull().default(`enabled`),
|
|
133
227
|
wakeStream: (0, drizzle_orm_pg_core.text)(`wake_stream`).notNull(),
|
|
228
|
+
sandboxProfiles: (0, drizzle_orm_pg_core.jsonb)(`sandbox_profiles`).notNull().default([]),
|
|
134
229
|
createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
135
230
|
updatedAt: (0, drizzle_orm_pg_core.timestamp)(`updated_at`, { withTimezone: true }).notNull().defaultNow()
|
|
136
231
|
}, (table) => [
|
|
@@ -291,12 +386,18 @@ const entityBridges = (0, drizzle_orm_pg_core.pgTable)(`entity_bridges`, {
|
|
|
291
386
|
sourceRef: (0, drizzle_orm_pg_core.text)(`source_ref`).notNull(),
|
|
292
387
|
tags: (0, drizzle_orm_pg_core.jsonb)(`tags`).notNull(),
|
|
293
388
|
streamUrl: (0, drizzle_orm_pg_core.text)(`stream_url`).notNull(),
|
|
389
|
+
principalUrl: (0, drizzle_orm_pg_core.text)(`principal_url`),
|
|
390
|
+
principalKind: (0, drizzle_orm_pg_core.text)(`principal_kind`),
|
|
294
391
|
shapeHandle: (0, drizzle_orm_pg_core.text)(`shape_handle`),
|
|
295
392
|
shapeOffset: (0, drizzle_orm_pg_core.text)(`shape_offset`),
|
|
296
393
|
lastObserverActivityAt: (0, drizzle_orm_pg_core.timestamp)(`last_observer_activity_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
297
394
|
createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
298
395
|
updatedAt: (0, drizzle_orm_pg_core.timestamp)(`updated_at`, { withTimezone: true }).notNull().defaultNow()
|
|
299
|
-
}, (table) => [
|
|
396
|
+
}, (table) => [
|
|
397
|
+
(0, drizzle_orm_pg_core.primaryKey)({ columns: [table.tenantId, table.sourceRef] }),
|
|
398
|
+
(0, drizzle_orm_pg_core.unique)(`uq_entity_bridges_stream_url`).on(table.tenantId, table.streamUrl),
|
|
399
|
+
(0, drizzle_orm_pg_core.index)(`idx_entity_bridges_principal`).on(table.tenantId, table.principalKind, table.principalUrl)
|
|
400
|
+
]);
|
|
300
401
|
const entityManifestSources = (0, drizzle_orm_pg_core.pgTable)(`entity_manifest_sources`, {
|
|
301
402
|
tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
|
|
302
403
|
ownerEntityUrl: (0, drizzle_orm_pg_core.text)(`owner_entity_url`).notNull(),
|
|
@@ -428,6 +529,7 @@ function toPublicEntity(entity) {
|
|
|
428
529
|
dispatch_policy: entity.dispatch_policy,
|
|
429
530
|
tags: entity.tags,
|
|
430
531
|
spawn_args: entity.spawn_args,
|
|
532
|
+
sandbox: entity.sandbox,
|
|
431
533
|
parent: entity.parent,
|
|
432
534
|
created_by: entity.created_by,
|
|
433
535
|
created_at: entity.created_at,
|
|
@@ -483,19 +585,35 @@ function isDuplicateUrlError(err) {
|
|
|
483
585
|
return e.code === `23505`;
|
|
484
586
|
}
|
|
485
587
|
const DEFAULT_RUNNER_LEASE_MS = 3e4;
|
|
588
|
+
const PERMISSION_PRUNE_INTERVAL_MS = 3e4;
|
|
486
589
|
function runnerWakeStream(runnerId) {
|
|
487
590
|
return `/runners/${runnerId}/wake`;
|
|
488
591
|
}
|
|
489
592
|
var PostgresRegistry = class {
|
|
593
|
+
lastPermissionPruneStartedAt = 0;
|
|
594
|
+
permissionPrunePromise = null;
|
|
490
595
|
constructor(db, tenantId = DEFAULT_TENANT_ID) {
|
|
491
596
|
this.db = db;
|
|
492
597
|
this.tenantId = tenantId;
|
|
493
598
|
}
|
|
494
599
|
async initialize() {}
|
|
495
600
|
close() {}
|
|
601
|
+
async ensureUserForPrincipal(principal) {
|
|
602
|
+
if (principal.kind !== `user`) return;
|
|
603
|
+
await this.db.insert(users).values({
|
|
604
|
+
tenantId: this.tenantId,
|
|
605
|
+
id: principal.id
|
|
606
|
+
}).onConflictDoNothing();
|
|
607
|
+
}
|
|
496
608
|
async createRunner(input) {
|
|
497
609
|
const now = new Date();
|
|
498
610
|
const wakeStream = input.wakeStream ?? runnerWakeStream(input.id);
|
|
611
|
+
const sandboxProfilesValue = input.sandboxProfiles ? input.sandboxProfiles.map((p) => ({
|
|
612
|
+
name: p.name,
|
|
613
|
+
label: p.label,
|
|
614
|
+
...p.description !== void 0 && { description: p.description },
|
|
615
|
+
...p.remote !== void 0 && { remote: p.remote }
|
|
616
|
+
})) : void 0;
|
|
499
617
|
await this.db.insert(runners).values({
|
|
500
618
|
tenantId: this.tenantId,
|
|
501
619
|
id: input.id,
|
|
@@ -504,6 +622,7 @@ var PostgresRegistry = class {
|
|
|
504
622
|
kind: input.kind ?? `local`,
|
|
505
623
|
adminStatus: input.adminStatus ?? `enabled`,
|
|
506
624
|
wakeStream,
|
|
625
|
+
...sandboxProfilesValue !== void 0 && { sandboxProfiles: sandboxProfilesValue },
|
|
507
626
|
updatedAt: now
|
|
508
627
|
}).onConflictDoUpdate({
|
|
509
628
|
target: [runners.tenantId, runners.id],
|
|
@@ -513,6 +632,7 @@ var PostgresRegistry = class {
|
|
|
513
632
|
kind: input.kind ?? `local`,
|
|
514
633
|
adminStatus: input.adminStatus ?? `enabled`,
|
|
515
634
|
wakeStream,
|
|
635
|
+
...sandboxProfilesValue !== void 0 && { sandboxProfiles: sandboxProfilesValue },
|
|
516
636
|
updatedAt: now
|
|
517
637
|
}
|
|
518
638
|
});
|
|
@@ -520,6 +640,30 @@ var PostgresRegistry = class {
|
|
|
520
640
|
if (!runner) throw new Error(`Failed to read back runner "${input.id}"`);
|
|
521
641
|
return runner;
|
|
522
642
|
}
|
|
643
|
+
/**
|
|
644
|
+
* Every sandbox profile advertised by a runner in this tenant (one entry
|
|
645
|
+
* per runner that advertises it — names may repeat across runners). Used by
|
|
646
|
+
* spawn validation for unpinned dispatch to learn whether a chosen profile
|
|
647
|
+
* is remote (so a shared sandbox can skip the single-runner guard).
|
|
648
|
+
*/
|
|
649
|
+
async listSandboxProfiles() {
|
|
650
|
+
const rows = await this.db.select({ sandboxProfiles: runners.sandboxProfiles }).from(runners).where((0, drizzle_orm.eq)(runners.tenantId, this.tenantId));
|
|
651
|
+
const profiles = [];
|
|
652
|
+
for (const row of rows) {
|
|
653
|
+
const list = row.sandboxProfiles;
|
|
654
|
+
if (!Array.isArray(list)) continue;
|
|
655
|
+
for (const entry of list) {
|
|
656
|
+
if (!entry || typeof entry.name !== `string`) continue;
|
|
657
|
+
profiles.push({
|
|
658
|
+
name: entry.name,
|
|
659
|
+
label: typeof entry.label === `string` ? entry.label : entry.name,
|
|
660
|
+
...typeof entry.description === `string` && { description: entry.description },
|
|
661
|
+
...typeof entry.remote === `boolean` && { remote: entry.remote }
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return profiles;
|
|
666
|
+
}
|
|
523
667
|
async getRunner(id) {
|
|
524
668
|
const rows = await this.db.select().from(runners).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(runners.tenantId, this.tenantId), (0, drizzle_orm.eq)(runners.id, id))).limit(1);
|
|
525
669
|
return rows[0] ? this.rowToRunner(rows[0]) : null;
|
|
@@ -780,6 +924,7 @@ var PostgresRegistry = class {
|
|
|
780
924
|
tags: (0, __electric_ax_agents_runtime.normalizeTags)(entity.tags),
|
|
781
925
|
tagsIndex: (0, __electric_ax_agents_runtime.buildTagsIndex)(entity.tags),
|
|
782
926
|
spawnArgs: entity.spawn_args ?? {},
|
|
927
|
+
sandbox: entity.sandbox ?? null,
|
|
783
928
|
parent: entity.parent ?? null,
|
|
784
929
|
createdBy: entity.created_by ?? null,
|
|
785
930
|
typeRevision: entity.type_revision ?? null,
|
|
@@ -794,6 +939,59 @@ var PostgresRegistry = class {
|
|
|
794
939
|
pendingSourceStreams: [],
|
|
795
940
|
updatedAt: new Date()
|
|
796
941
|
}).onConflictDoNothing();
|
|
942
|
+
await tx.insert(entityLineage).values({
|
|
943
|
+
tenantId: this.tenantId,
|
|
944
|
+
ancestorUrl: entity.url,
|
|
945
|
+
descendantUrl: entity.url,
|
|
946
|
+
depth: 0
|
|
947
|
+
}).onConflictDoNothing();
|
|
948
|
+
if (entity.parent) await tx.execute(drizzle_orm.sql`
|
|
949
|
+
INSERT INTO ${entityLineage} (
|
|
950
|
+
tenant_id,
|
|
951
|
+
ancestor_url,
|
|
952
|
+
descendant_url,
|
|
953
|
+
depth
|
|
954
|
+
)
|
|
955
|
+
SELECT
|
|
956
|
+
${this.tenantId},
|
|
957
|
+
ancestor_url,
|
|
958
|
+
${entity.url},
|
|
959
|
+
depth + 1
|
|
960
|
+
FROM ${entityLineage}
|
|
961
|
+
WHERE tenant_id = ${this.tenantId}
|
|
962
|
+
AND descendant_url = ${entity.parent}
|
|
963
|
+
ON CONFLICT DO NOTHING
|
|
964
|
+
`);
|
|
965
|
+
await tx.execute(drizzle_orm.sql`
|
|
966
|
+
INSERT INTO ${entityEffectivePermissions} (
|
|
967
|
+
tenant_id,
|
|
968
|
+
entity_url,
|
|
969
|
+
source_entity_url,
|
|
970
|
+
source_grant_id,
|
|
971
|
+
permission,
|
|
972
|
+
subject_kind,
|
|
973
|
+
subject_value,
|
|
974
|
+
expires_at
|
|
975
|
+
)
|
|
976
|
+
SELECT
|
|
977
|
+
${this.tenantId},
|
|
978
|
+
${entity.url},
|
|
979
|
+
grants.entity_url,
|
|
980
|
+
grants.id,
|
|
981
|
+
grants.permission,
|
|
982
|
+
grants.subject_kind,
|
|
983
|
+
grants.subject_value,
|
|
984
|
+
grants.expires_at
|
|
985
|
+
FROM ${entityPermissionGrants} grants
|
|
986
|
+
JOIN ${entityLineage} lineage
|
|
987
|
+
ON lineage.tenant_id = grants.tenant_id
|
|
988
|
+
AND lineage.ancestor_url = grants.entity_url
|
|
989
|
+
AND lineage.descendant_url = ${entity.url}
|
|
990
|
+
WHERE grants.tenant_id = ${this.tenantId}
|
|
991
|
+
AND grants.propagation = 'descendants'
|
|
992
|
+
AND (grants.expires_at IS NULL OR grants.expires_at > now())
|
|
993
|
+
ON CONFLICT DO NOTHING
|
|
994
|
+
`);
|
|
797
995
|
return parseInt(result[0].txid);
|
|
798
996
|
});
|
|
799
997
|
} catch (err) {
|
|
@@ -815,10 +1013,8 @@ var PostgresRegistry = class {
|
|
|
815
1013
|
}
|
|
816
1014
|
async getEntityByStream(streamPath) {
|
|
817
1015
|
const mainSuffix = `/main`;
|
|
818
|
-
const errorSuffix = `/error`;
|
|
819
1016
|
let entityUrl = null;
|
|
820
1017
|
if (streamPath.endsWith(mainSuffix)) entityUrl = streamPath.slice(0, -mainSuffix.length);
|
|
821
|
-
else if (streamPath.endsWith(errorSuffix)) entityUrl = streamPath.slice(0, -errorSuffix.length);
|
|
822
1018
|
if (!entityUrl) return null;
|
|
823
1019
|
return this.getEntity(entityUrl);
|
|
824
1020
|
}
|
|
@@ -828,6 +1024,23 @@ var PostgresRegistry = class {
|
|
|
828
1024
|
if (filter?.status) conditions.push((0, drizzle_orm.eq)(entities.status, filter.status));
|
|
829
1025
|
if (filter?.parent) conditions.push((0, drizzle_orm.eq)(entities.parent, filter.parent));
|
|
830
1026
|
if (filter?.created_by) conditions.push((0, drizzle_orm.eq)(entities.createdBy, filter.created_by));
|
|
1027
|
+
if (filter?.readableBy && !filter.readableBy.bypass) conditions.push(drizzle_orm.sql`(
|
|
1028
|
+
${entities.createdBy} = ${filter.readableBy.principalUrl}
|
|
1029
|
+
OR ${entities.url} IN (
|
|
1030
|
+
SELECT ${entityEffectivePermissions.entityUrl}
|
|
1031
|
+
FROM ${entityEffectivePermissions}
|
|
1032
|
+
WHERE ${entityEffectivePermissions.tenantId} = ${this.tenantId}
|
|
1033
|
+
AND ${entityEffectivePermissions.permission} IN ('read', 'manage')
|
|
1034
|
+
AND (${entityEffectivePermissions.expiresAt} IS NULL OR ${entityEffectivePermissions.expiresAt} > now())
|
|
1035
|
+
AND (
|
|
1036
|
+
(${entityEffectivePermissions.subjectKind} = 'principal'
|
|
1037
|
+
AND ${entityEffectivePermissions.subjectValue} = ${filter.readableBy.principalUrl})
|
|
1038
|
+
OR
|
|
1039
|
+
(${entityEffectivePermissions.subjectKind} = 'principal_kind'
|
|
1040
|
+
AND ${entityEffectivePermissions.subjectValue} = ${filter.readableBy.principalKind})
|
|
1041
|
+
)
|
|
1042
|
+
)
|
|
1043
|
+
)`);
|
|
831
1044
|
const whereClause = (0, drizzle_orm.and)(...conditions);
|
|
832
1045
|
const countResult = await this.db.select({ count: drizzle_orm.sql`count(*)` }).from(entities).where(whereClause);
|
|
833
1046
|
const total = Number(countResult[0].count);
|
|
@@ -840,6 +1053,189 @@ var PostgresRegistry = class {
|
|
|
840
1053
|
total
|
|
841
1054
|
};
|
|
842
1055
|
}
|
|
1056
|
+
async createEntityTypePermissionGrant(input) {
|
|
1057
|
+
const [row] = await this.db.insert(entityTypePermissionGrants).values({
|
|
1058
|
+
tenantId: this.tenantId,
|
|
1059
|
+
entityType: input.entityType,
|
|
1060
|
+
permission: input.permission,
|
|
1061
|
+
subjectKind: input.subjectKind,
|
|
1062
|
+
subjectValue: input.subjectValue,
|
|
1063
|
+
createdBy: input.createdBy ?? null,
|
|
1064
|
+
expiresAt: input.expiresAt ?? null
|
|
1065
|
+
}).returning();
|
|
1066
|
+
return this.rowToEntityTypePermissionGrant(row);
|
|
1067
|
+
}
|
|
1068
|
+
async ensureEntityTypePermissionGrant(input) {
|
|
1069
|
+
const [existing] = await this.db.select().from(entityTypePermissionGrants).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityTypePermissionGrants.tenantId, this.tenantId), (0, drizzle_orm.eq)(entityTypePermissionGrants.entityType, input.entityType), (0, drizzle_orm.eq)(entityTypePermissionGrants.permission, input.permission), (0, drizzle_orm.eq)(entityTypePermissionGrants.subjectKind, input.subjectKind), (0, drizzle_orm.eq)(entityTypePermissionGrants.subjectValue, input.subjectValue), input.expiresAt ? (0, drizzle_orm.eq)(entityTypePermissionGrants.expiresAt, input.expiresAt) : drizzle_orm.sql`${entityTypePermissionGrants.expiresAt} IS NULL`)).limit(1);
|
|
1070
|
+
if (existing) return this.rowToEntityTypePermissionGrant(existing);
|
|
1071
|
+
return await this.createEntityTypePermissionGrant(input);
|
|
1072
|
+
}
|
|
1073
|
+
async listEntityTypePermissionGrants(entityType) {
|
|
1074
|
+
const rows = await this.db.select().from(entityTypePermissionGrants).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityTypePermissionGrants.tenantId, this.tenantId), (0, drizzle_orm.eq)(entityTypePermissionGrants.entityType, entityType))).orderBy(entityTypePermissionGrants.id);
|
|
1075
|
+
return rows.map((row) => this.rowToEntityTypePermissionGrant(row));
|
|
1076
|
+
}
|
|
1077
|
+
async deleteEntityTypePermissionGrant(entityType, grantId) {
|
|
1078
|
+
const rows = await this.db.delete(entityTypePermissionGrants).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityTypePermissionGrants.tenantId, this.tenantId), (0, drizzle_orm.eq)(entityTypePermissionGrants.entityType, entityType), (0, drizzle_orm.eq)(entityTypePermissionGrants.id, grantId))).returning({ id: entityTypePermissionGrants.id });
|
|
1079
|
+
return rows.length > 0;
|
|
1080
|
+
}
|
|
1081
|
+
async hasEntityTypePermission(entityType, permission, subject) {
|
|
1082
|
+
const permissions = [permission, `manage`];
|
|
1083
|
+
const rows = await this.db.select({ id: entityTypePermissionGrants.id }).from(entityTypePermissionGrants).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityTypePermissionGrants.tenantId, this.tenantId), (0, drizzle_orm.eq)(entityTypePermissionGrants.entityType, entityType), (0, drizzle_orm.inArray)(entityTypePermissionGrants.permission, [...permissions]), drizzle_orm.sql`(${entityTypePermissionGrants.expiresAt} IS NULL OR ${entityTypePermissionGrants.expiresAt} > now())`, drizzle_orm.sql`(
|
|
1084
|
+
(${entityTypePermissionGrants.subjectKind} = 'principal'
|
|
1085
|
+
AND ${entityTypePermissionGrants.subjectValue} = ${subject.principalUrl})
|
|
1086
|
+
OR
|
|
1087
|
+
(${entityTypePermissionGrants.subjectKind} = 'principal_kind'
|
|
1088
|
+
AND ${entityTypePermissionGrants.subjectValue} = ${subject.principalKind})
|
|
1089
|
+
)`)).limit(1);
|
|
1090
|
+
return rows.length > 0;
|
|
1091
|
+
}
|
|
1092
|
+
async createEntityPermissionGrant(input) {
|
|
1093
|
+
return await this.db.transaction(async (tx) => {
|
|
1094
|
+
const [row] = await tx.insert(entityPermissionGrants).values({
|
|
1095
|
+
tenantId: this.tenantId,
|
|
1096
|
+
entityUrl: input.entityUrl,
|
|
1097
|
+
permission: input.permission,
|
|
1098
|
+
subjectKind: input.subjectKind,
|
|
1099
|
+
subjectValue: input.subjectValue,
|
|
1100
|
+
propagation: input.propagation ?? `self`,
|
|
1101
|
+
copyToChildren: input.copyToChildren ?? false,
|
|
1102
|
+
createdBy: input.createdBy ?? null,
|
|
1103
|
+
expiresAt: input.expiresAt ?? null
|
|
1104
|
+
}).returning();
|
|
1105
|
+
await this.materializeEntityPermissionGrant(tx, row);
|
|
1106
|
+
return this.rowToEntityPermissionGrant(row);
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
async listEntityPermissionGrants(entityUrl) {
|
|
1110
|
+
const rows = await this.db.select().from(entityPermissionGrants).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityPermissionGrants.tenantId, this.tenantId), (0, drizzle_orm.eq)(entityPermissionGrants.entityUrl, entityUrl))).orderBy(entityPermissionGrants.id);
|
|
1111
|
+
return rows.map((row) => this.rowToEntityPermissionGrant(row));
|
|
1112
|
+
}
|
|
1113
|
+
async deleteEntityPermissionGrant(entityUrl, grantId) {
|
|
1114
|
+
return await this.db.transaction(async (tx) => {
|
|
1115
|
+
await tx.delete(entityEffectivePermissions).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityEffectivePermissions.tenantId, this.tenantId), (0, drizzle_orm.eq)(entityEffectivePermissions.sourceGrantId, grantId)));
|
|
1116
|
+
const rows = await tx.delete(entityPermissionGrants).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityPermissionGrants.tenantId, this.tenantId), (0, drizzle_orm.eq)(entityPermissionGrants.entityUrl, entityUrl), (0, drizzle_orm.eq)(entityPermissionGrants.id, grantId))).returning({ id: entityPermissionGrants.id });
|
|
1117
|
+
return rows.length > 0;
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
async copyEntityPermissionGrantsForSpawn(parentEntityUrl, childEntityUrl, createdBy) {
|
|
1121
|
+
const parentGrants = await this.db.select().from(entityPermissionGrants).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityPermissionGrants.tenantId, this.tenantId), (0, drizzle_orm.eq)(entityPermissionGrants.entityUrl, parentEntityUrl), (0, drizzle_orm.eq)(entityPermissionGrants.copyToChildren, true), drizzle_orm.sql`(${entityPermissionGrants.expiresAt} IS NULL OR ${entityPermissionGrants.expiresAt} > now())`));
|
|
1122
|
+
const copied = [];
|
|
1123
|
+
for (const grant of parentGrants) copied.push(await this.createEntityPermissionGrant({
|
|
1124
|
+
entityUrl: childEntityUrl,
|
|
1125
|
+
permission: grant.permission,
|
|
1126
|
+
subjectKind: grant.subjectKind,
|
|
1127
|
+
subjectValue: grant.subjectValue,
|
|
1128
|
+
propagation: `self`,
|
|
1129
|
+
copyToChildren: grant.copyToChildren,
|
|
1130
|
+
createdBy,
|
|
1131
|
+
expiresAt: grant.expiresAt ?? void 0
|
|
1132
|
+
}));
|
|
1133
|
+
return copied;
|
|
1134
|
+
}
|
|
1135
|
+
async hasEntityPermission(entityUrl, permission, subject) {
|
|
1136
|
+
const permissions = [permission, `manage`];
|
|
1137
|
+
const rows = await this.db.select({ id: entityEffectivePermissions.id }).from(entityEffectivePermissions).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityEffectivePermissions.tenantId, this.tenantId), (0, drizzle_orm.eq)(entityEffectivePermissions.entityUrl, entityUrl), (0, drizzle_orm.inArray)(entityEffectivePermissions.permission, [...permissions]), drizzle_orm.sql`(${entityEffectivePermissions.expiresAt} IS NULL OR ${entityEffectivePermissions.expiresAt} > now())`, drizzle_orm.sql`(
|
|
1138
|
+
(${entityEffectivePermissions.subjectKind} = 'principal'
|
|
1139
|
+
AND ${entityEffectivePermissions.subjectValue} = ${subject.principalUrl})
|
|
1140
|
+
OR
|
|
1141
|
+
(${entityEffectivePermissions.subjectKind} = 'principal_kind'
|
|
1142
|
+
AND ${entityEffectivePermissions.subjectValue} = ${subject.principalKind})
|
|
1143
|
+
)`)).limit(1);
|
|
1144
|
+
return rows.length > 0;
|
|
1145
|
+
}
|
|
1146
|
+
async replaceSharedStateLink(ownerEntityUrl, manifestKey, sharedStateId) {
|
|
1147
|
+
await this.db.delete(sharedStateLinks).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(sharedStateLinks.tenantId, this.tenantId), (0, drizzle_orm.eq)(sharedStateLinks.ownerEntityUrl, ownerEntityUrl), (0, drizzle_orm.eq)(sharedStateLinks.manifestKey, manifestKey)));
|
|
1148
|
+
if (!sharedStateId) return;
|
|
1149
|
+
await this.db.insert(sharedStateLinks).values({
|
|
1150
|
+
tenantId: this.tenantId,
|
|
1151
|
+
ownerEntityUrl,
|
|
1152
|
+
manifestKey,
|
|
1153
|
+
sharedStateId
|
|
1154
|
+
}).onConflictDoUpdate({
|
|
1155
|
+
target: [
|
|
1156
|
+
sharedStateLinks.tenantId,
|
|
1157
|
+
sharedStateLinks.ownerEntityUrl,
|
|
1158
|
+
sharedStateLinks.manifestKey
|
|
1159
|
+
],
|
|
1160
|
+
set: {
|
|
1161
|
+
sharedStateId,
|
|
1162
|
+
updatedAt: new Date()
|
|
1163
|
+
}
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
async listSharedStateLinkedEntityUrls(sharedStateId) {
|
|
1167
|
+
const rows = await this.db.selectDistinct({ ownerEntityUrl: sharedStateLinks.ownerEntityUrl }).from(sharedStateLinks).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(sharedStateLinks.tenantId, this.tenantId), (0, drizzle_orm.eq)(sharedStateLinks.sharedStateId, sharedStateId)));
|
|
1168
|
+
return rows.map((row) => row.ownerEntityUrl);
|
|
1169
|
+
}
|
|
1170
|
+
async pruneExpiredPermissionGrants(now = new Date(), options = {}) {
|
|
1171
|
+
if (this.permissionPrunePromise) return await this.permissionPrunePromise;
|
|
1172
|
+
const startedAt = Date.now();
|
|
1173
|
+
if (!options.force && startedAt - this.lastPermissionPruneStartedAt < PERMISSION_PRUNE_INTERVAL_MS) return;
|
|
1174
|
+
this.lastPermissionPruneStartedAt = startedAt;
|
|
1175
|
+
const promise = this.pruneExpiredPermissionGrantsNow(now);
|
|
1176
|
+
this.permissionPrunePromise = promise;
|
|
1177
|
+
try {
|
|
1178
|
+
await promise;
|
|
1179
|
+
} catch (error) {
|
|
1180
|
+
this.lastPermissionPruneStartedAt = 0;
|
|
1181
|
+
throw error;
|
|
1182
|
+
} finally {
|
|
1183
|
+
if (this.permissionPrunePromise === promise) this.permissionPrunePromise = null;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
async pruneExpiredPermissionGrantsNow(now) {
|
|
1187
|
+
await this.db.transaction(async (tx) => {
|
|
1188
|
+
const expiredEntityGrantIds = await tx.select({ id: entityPermissionGrants.id }).from(entityPermissionGrants).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityPermissionGrants.tenantId, this.tenantId), drizzle_orm.sql`${entityPermissionGrants.expiresAt} IS NOT NULL`, (0, drizzle_orm.lt)(entityPermissionGrants.expiresAt, now)));
|
|
1189
|
+
const ids = expiredEntityGrantIds.map((row) => row.id);
|
|
1190
|
+
if (ids.length > 0) {
|
|
1191
|
+
await tx.delete(entityEffectivePermissions).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityEffectivePermissions.tenantId, this.tenantId), (0, drizzle_orm.inArray)(entityEffectivePermissions.sourceGrantId, ids)));
|
|
1192
|
+
await tx.delete(entityPermissionGrants).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityPermissionGrants.tenantId, this.tenantId), (0, drizzle_orm.inArray)(entityPermissionGrants.id, ids)));
|
|
1193
|
+
}
|
|
1194
|
+
await tx.delete(entityEffectivePermissions).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityEffectivePermissions.tenantId, this.tenantId), drizzle_orm.sql`${entityEffectivePermissions.expiresAt} IS NOT NULL`, (0, drizzle_orm.lt)(entityEffectivePermissions.expiresAt, now)));
|
|
1195
|
+
await tx.delete(entityTypePermissionGrants).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityTypePermissionGrants.tenantId, this.tenantId), drizzle_orm.sql`${entityTypePermissionGrants.expiresAt} IS NOT NULL`, (0, drizzle_orm.lt)(entityTypePermissionGrants.expiresAt, now)));
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
async materializeEntityPermissionGrant(tx, grant) {
|
|
1199
|
+
await tx.delete(entityEffectivePermissions).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityEffectivePermissions.tenantId, this.tenantId), (0, drizzle_orm.eq)(entityEffectivePermissions.sourceGrantId, grant.id)));
|
|
1200
|
+
if (grant.propagation === `descendants`) {
|
|
1201
|
+
await tx.execute(drizzle_orm.sql`
|
|
1202
|
+
INSERT INTO ${entityEffectivePermissions} (
|
|
1203
|
+
tenant_id,
|
|
1204
|
+
entity_url,
|
|
1205
|
+
source_entity_url,
|
|
1206
|
+
source_grant_id,
|
|
1207
|
+
permission,
|
|
1208
|
+
subject_kind,
|
|
1209
|
+
subject_value,
|
|
1210
|
+
expires_at
|
|
1211
|
+
)
|
|
1212
|
+
SELECT
|
|
1213
|
+
${this.tenantId},
|
|
1214
|
+
descendant_url,
|
|
1215
|
+
${grant.entityUrl},
|
|
1216
|
+
${grant.id},
|
|
1217
|
+
${grant.permission},
|
|
1218
|
+
${grant.subjectKind},
|
|
1219
|
+
${grant.subjectValue},
|
|
1220
|
+
${grant.expiresAt}
|
|
1221
|
+
FROM ${entityLineage}
|
|
1222
|
+
WHERE tenant_id = ${this.tenantId}
|
|
1223
|
+
AND ancestor_url = ${grant.entityUrl}
|
|
1224
|
+
ON CONFLICT DO NOTHING
|
|
1225
|
+
`);
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
await tx.insert(entityEffectivePermissions).values({
|
|
1229
|
+
tenantId: this.tenantId,
|
|
1230
|
+
entityUrl: grant.entityUrl,
|
|
1231
|
+
sourceEntityUrl: grant.entityUrl,
|
|
1232
|
+
sourceGrantId: grant.id,
|
|
1233
|
+
permission: grant.permission,
|
|
1234
|
+
subjectKind: grant.subjectKind,
|
|
1235
|
+
subjectValue: grant.subjectValue,
|
|
1236
|
+
expiresAt: grant.expiresAt
|
|
1237
|
+
}).onConflictDoNothing();
|
|
1238
|
+
}
|
|
843
1239
|
async updateStatus(entityUrl, status$4) {
|
|
844
1240
|
const whereClause = isTerminalEntityStatus(status$4) ? this.entityWhere(entityUrl) : (0, drizzle_orm.and)(this.entityWhere(entityUrl), (0, drizzle_orm.ne)(entities.status, `stopped`), (0, drizzle_orm.ne)(entities.status, `killed`));
|
|
845
1241
|
await this.db.update(entities).set({
|
|
@@ -941,7 +1337,9 @@ var PostgresRegistry = class {
|
|
|
941
1337
|
tenantId: this.tenantId,
|
|
942
1338
|
sourceRef: row.sourceRef,
|
|
943
1339
|
tags: (0, __electric_ax_agents_runtime.normalizeTags)(row.tags),
|
|
944
|
-
streamUrl: row.streamUrl
|
|
1340
|
+
streamUrl: row.streamUrl,
|
|
1341
|
+
principalUrl: row.principalUrl,
|
|
1342
|
+
principalKind: row.principalKind
|
|
945
1343
|
}).onConflictDoNothing();
|
|
946
1344
|
const existing = await this.getEntityBridge(row.sourceRef);
|
|
947
1345
|
if (!existing) throw new Error(`Failed to load entity bridge ${row.sourceRef}`);
|
|
@@ -1103,20 +1501,46 @@ var PostgresRegistry = class {
|
|
|
1103
1501
|
updated_at: row.updatedAt
|
|
1104
1502
|
};
|
|
1105
1503
|
}
|
|
1504
|
+
rowToEntityTypePermissionGrant(row) {
|
|
1505
|
+
return {
|
|
1506
|
+
id: row.id,
|
|
1507
|
+
entity_type: row.entityType,
|
|
1508
|
+
permission: row.permission,
|
|
1509
|
+
subject_kind: row.subjectKind,
|
|
1510
|
+
subject_value: row.subjectValue,
|
|
1511
|
+
created_by: row.createdBy ?? void 0,
|
|
1512
|
+
expires_at: row.expiresAt?.toISOString(),
|
|
1513
|
+
created_at: row.createdAt.toISOString(),
|
|
1514
|
+
updated_at: row.updatedAt.toISOString()
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
rowToEntityPermissionGrant(row) {
|
|
1518
|
+
return {
|
|
1519
|
+
id: row.id,
|
|
1520
|
+
entity_url: row.entityUrl,
|
|
1521
|
+
permission: row.permission,
|
|
1522
|
+
subject_kind: row.subjectKind,
|
|
1523
|
+
subject_value: row.subjectValue,
|
|
1524
|
+
propagation: row.propagation,
|
|
1525
|
+
copy_to_children: row.copyToChildren,
|
|
1526
|
+
created_by: row.createdBy ?? void 0,
|
|
1527
|
+
expires_at: row.expiresAt?.toISOString(),
|
|
1528
|
+
created_at: row.createdAt.toISOString(),
|
|
1529
|
+
updated_at: row.updatedAt.toISOString()
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1106
1532
|
rowToEntity(row) {
|
|
1107
1533
|
return {
|
|
1108
1534
|
url: row.url,
|
|
1109
1535
|
type: row.type,
|
|
1110
1536
|
status: assertEntityStatus(row.status),
|
|
1111
|
-
streams: {
|
|
1112
|
-
main: `${row.url}/main`,
|
|
1113
|
-
error: `${row.url}/error`
|
|
1114
|
-
},
|
|
1537
|
+
streams: { main: `${row.url}/main` },
|
|
1115
1538
|
subscription_id: row.subscriptionId,
|
|
1116
1539
|
dispatch_policy: row.dispatchPolicy ?? void 0,
|
|
1117
1540
|
write_token: row.writeToken,
|
|
1118
1541
|
tags: row.tags ?? {},
|
|
1119
1542
|
spawn_args: row.spawnArgs,
|
|
1543
|
+
sandbox: row.sandbox ?? void 0,
|
|
1120
1544
|
parent: row.parent ?? void 0,
|
|
1121
1545
|
created_by: row.createdBy ?? void 0,
|
|
1122
1546
|
type_revision: row.typeRevision ?? void 0,
|
|
@@ -1132,6 +1556,8 @@ var PostgresRegistry = class {
|
|
|
1132
1556
|
sourceRef: row.sourceRef,
|
|
1133
1557
|
tags: row.tags ?? {},
|
|
1134
1558
|
streamUrl: row.streamUrl,
|
|
1559
|
+
principalUrl: row.principalUrl ?? void 0,
|
|
1560
|
+
principalKind: row.principalKind ?? void 0,
|
|
1135
1561
|
shapeHandle: row.shapeHandle ?? void 0,
|
|
1136
1562
|
shapeOffset: row.shapeOffset ?? void 0,
|
|
1137
1563
|
lastObserverActivityAt: row.lastObserverActivityAt,
|
|
@@ -1164,6 +1590,7 @@ var PostgresRegistry = class {
|
|
|
1164
1590
|
kind: assertRunnerKind(row.kind),
|
|
1165
1591
|
admin_status: assertRunnerAdminStatus(row.adminStatus),
|
|
1166
1592
|
wake_stream: row.wakeStream,
|
|
1593
|
+
sandbox_profiles: row.sandboxProfiles ?? [],
|
|
1167
1594
|
created_at: row.createdAt.toISOString(),
|
|
1168
1595
|
updated_at: row.updatedAt.toISOString()
|
|
1169
1596
|
};
|
|
@@ -1285,6 +1712,93 @@ const serverLog = {
|
|
|
1285
1712
|
}
|
|
1286
1713
|
};
|
|
1287
1714
|
|
|
1715
|
+
//#endregion
|
|
1716
|
+
//#region src/principal.ts
|
|
1717
|
+
const ELECTRIC_PRINCIPAL_HEADER = `electric-principal`;
|
|
1718
|
+
const PRINCIPAL_KINDS = new Set([
|
|
1719
|
+
`user`,
|
|
1720
|
+
`agent`,
|
|
1721
|
+
`service`,
|
|
1722
|
+
`system`
|
|
1723
|
+
]);
|
|
1724
|
+
function parsePrincipalKey(input) {
|
|
1725
|
+
const colon = input.indexOf(`:`);
|
|
1726
|
+
if (colon <= 0) throw new Error(`Invalid principal identifier`);
|
|
1727
|
+
const kind = input.slice(0, colon);
|
|
1728
|
+
const id = input.slice(colon + 1);
|
|
1729
|
+
if (!PRINCIPAL_KINDS.has(kind)) throw new Error(`Invalid principal kind`);
|
|
1730
|
+
if (!id || id.includes(`/`)) throw new Error(`Invalid principal id`);
|
|
1731
|
+
const key = `${kind}:${id}`;
|
|
1732
|
+
return {
|
|
1733
|
+
kind,
|
|
1734
|
+
id,
|
|
1735
|
+
key,
|
|
1736
|
+
url: `/principal/${encodeURIComponent(key)}`
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
function principalUrl(key) {
|
|
1740
|
+
return parsePrincipalKey(key).url;
|
|
1741
|
+
}
|
|
1742
|
+
function parsePrincipalUrl(url) {
|
|
1743
|
+
if (!url.startsWith(`/principal/`)) return null;
|
|
1744
|
+
const segment = url.slice(`/principal/`.length);
|
|
1745
|
+
if (!segment || segment.includes(`/`)) return null;
|
|
1746
|
+
try {
|
|
1747
|
+
return parsePrincipalKey(decodeURIComponent(segment));
|
|
1748
|
+
} catch {
|
|
1749
|
+
return null;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
const BUILT_IN_SYSTEM_PRINCIPAL_IDS = new Set([
|
|
1753
|
+
`framework`,
|
|
1754
|
+
`auth-sync`,
|
|
1755
|
+
`dev-local`
|
|
1756
|
+
]);
|
|
1757
|
+
function isBuiltInSystemPrincipalUrl(url) {
|
|
1758
|
+
if (!url?.startsWith(`/principal/`)) return false;
|
|
1759
|
+
try {
|
|
1760
|
+
const principal = parsePrincipalUrl(url);
|
|
1761
|
+
if (!principal) return false;
|
|
1762
|
+
return principal.kind === `system` && BUILT_IN_SYSTEM_PRINCIPAL_IDS.has(principal.id);
|
|
1763
|
+
} catch {
|
|
1764
|
+
return false;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
function principalFromCreatedBy(createdBy) {
|
|
1768
|
+
if (!createdBy) return void 0;
|
|
1769
|
+
const principal = parsePrincipalUrl(createdBy);
|
|
1770
|
+
if (!principal) return {
|
|
1771
|
+
url: createdBy,
|
|
1772
|
+
key: null
|
|
1773
|
+
};
|
|
1774
|
+
return {
|
|
1775
|
+
url: principal.url,
|
|
1776
|
+
key: principal.key,
|
|
1777
|
+
kind: principal.kind,
|
|
1778
|
+
id: principal.id
|
|
1779
|
+
};
|
|
1780
|
+
}
|
|
1781
|
+
const principalIdentityStateSchema = __sinclair_typebox.Type.Object({
|
|
1782
|
+
kind: __sinclair_typebox.Type.Union([
|
|
1783
|
+
__sinclair_typebox.Type.Literal(`user`),
|
|
1784
|
+
__sinclair_typebox.Type.Literal(`agent`),
|
|
1785
|
+
__sinclair_typebox.Type.Literal(`service`),
|
|
1786
|
+
__sinclair_typebox.Type.Literal(`system`)
|
|
1787
|
+
]),
|
|
1788
|
+
id: __sinclair_typebox.Type.String(),
|
|
1789
|
+
key: __sinclair_typebox.Type.String(),
|
|
1790
|
+
url: __sinclair_typebox.Type.String(),
|
|
1791
|
+
updated_at: __sinclair_typebox.Type.String(),
|
|
1792
|
+
display_name: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
1793
|
+
email: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
1794
|
+
avatar_url: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
1795
|
+
auth_provider: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
1796
|
+
auth_subject: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
1797
|
+
claims: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown())),
|
|
1798
|
+
created_at: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
1799
|
+
}, { additionalProperties: false });
|
|
1800
|
+
const principalUpdateIdentityMessageSchema = __sinclair_typebox.Type.Object({ identity: principalIdentityStateSchema }, { additionalProperties: false });
|
|
1801
|
+
|
|
1288
1802
|
//#endregion
|
|
1289
1803
|
//#region src/entity-projector.ts
|
|
1290
1804
|
const ENTITY_SHAPE_COLUMNS = [
|
|
@@ -1293,7 +1807,9 @@ const ENTITY_SHAPE_COLUMNS = [
|
|
|
1293
1807
|
`type`,
|
|
1294
1808
|
`status`,
|
|
1295
1809
|
`tags`,
|
|
1810
|
+
`created_by`,
|
|
1296
1811
|
`spawn_args`,
|
|
1812
|
+
`sandbox`,
|
|
1297
1813
|
`parent`,
|
|
1298
1814
|
`type_revision`,
|
|
1299
1815
|
`inbox_schemas`,
|
|
@@ -1311,6 +1827,12 @@ function sourceRefFromStreamPath(streamPath) {
|
|
|
1311
1827
|
const match = streamPath.match(/^\/_entities\/([^/]+)$/);
|
|
1312
1828
|
return match?.[1] ?? null;
|
|
1313
1829
|
}
|
|
1830
|
+
function principalScopedSourceRef(tagSourceRef, principalUrl$1, principalKind) {
|
|
1831
|
+
return `${tagSourceRef}-${(0, __electric_ax_agents_runtime.hashString)(JSON.stringify({
|
|
1832
|
+
principalKind,
|
|
1833
|
+
principalUrl: principalUrl$1
|
|
1834
|
+
}))}`;
|
|
1835
|
+
}
|
|
1314
1836
|
function sameMember(left, right) {
|
|
1315
1837
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
1316
1838
|
}
|
|
@@ -1327,6 +1849,7 @@ function toMemberRow(entity) {
|
|
|
1327
1849
|
status: entity.status,
|
|
1328
1850
|
tags: entity.tags,
|
|
1329
1851
|
spawn_args: entity.spawn_args ?? {},
|
|
1852
|
+
sandbox: entity.sandbox ?? null,
|
|
1330
1853
|
parent: entity.parent ?? null,
|
|
1331
1854
|
type_revision: entity.type_revision ?? null,
|
|
1332
1855
|
inbox_schemas: entity.inbox_schemas ?? null,
|
|
@@ -1340,15 +1863,22 @@ var ProjectedEntityBridge = class {
|
|
|
1340
1863
|
sourceRef;
|
|
1341
1864
|
tags;
|
|
1342
1865
|
streamUrl;
|
|
1866
|
+
principalUrl;
|
|
1867
|
+
principalKind;
|
|
1868
|
+
permissionBypass;
|
|
1343
1869
|
currentMembers = new Map();
|
|
1344
1870
|
producer = null;
|
|
1345
1871
|
stopped = false;
|
|
1346
|
-
constructor(row, streamClient) {
|
|
1872
|
+
constructor(row, registry, streamClient) {
|
|
1873
|
+
this.registry = registry;
|
|
1347
1874
|
this.streamClient = streamClient;
|
|
1348
1875
|
this.tenantId = row.tenantId;
|
|
1349
1876
|
this.sourceRef = row.sourceRef;
|
|
1350
1877
|
this.tags = (0, __electric_ax_agents_runtime.normalizeTags)(row.tags);
|
|
1351
1878
|
this.streamUrl = row.streamUrl;
|
|
1879
|
+
this.principalUrl = row.principalUrl;
|
|
1880
|
+
this.principalKind = row.principalKind;
|
|
1881
|
+
this.permissionBypass = isBuiltInSystemPrincipalUrl(row.principalUrl);
|
|
1352
1882
|
}
|
|
1353
1883
|
async start(initialEntities) {
|
|
1354
1884
|
await this.ensureStream();
|
|
@@ -1362,7 +1892,7 @@ var ProjectedEntityBridge = class {
|
|
|
1362
1892
|
}
|
|
1363
1893
|
});
|
|
1364
1894
|
await this.loadCurrentMembers();
|
|
1365
|
-
this.reconcile(initialEntities);
|
|
1895
|
+
await this.reconcile(initialEntities);
|
|
1366
1896
|
}
|
|
1367
1897
|
async stop() {
|
|
1368
1898
|
this.stopped = true;
|
|
@@ -1374,12 +1904,13 @@ var ProjectedEntityBridge = class {
|
|
|
1374
1904
|
this.producer = null;
|
|
1375
1905
|
}
|
|
1376
1906
|
}
|
|
1377
|
-
reconcile(entities$1) {
|
|
1907
|
+
async reconcile(entities$1) {
|
|
1378
1908
|
if (this.stopped) return;
|
|
1379
1909
|
const staleMembers = new Map(this.currentMembers);
|
|
1380
1910
|
for (const entity of entities$1) {
|
|
1381
1911
|
if (entity.tenant_id !== this.tenantId) continue;
|
|
1382
1912
|
if (!entityMatchesTags(entity, this.tags)) continue;
|
|
1913
|
+
if (!await this.canReadEntity(entity)) continue;
|
|
1383
1914
|
staleMembers.delete(entity.url);
|
|
1384
1915
|
this.upsertEntity(entity);
|
|
1385
1916
|
}
|
|
@@ -1388,10 +1919,10 @@ var ProjectedEntityBridge = class {
|
|
|
1388
1919
|
this.currentMembers.delete(url);
|
|
1389
1920
|
}
|
|
1390
1921
|
}
|
|
1391
|
-
applyEntity(entity) {
|
|
1922
|
+
async applyEntity(entity) {
|
|
1392
1923
|
if (this.stopped) return;
|
|
1393
1924
|
if (entity.tenant_id !== this.tenantId) return;
|
|
1394
|
-
if (!entityMatchesTags(entity, this.tags)) {
|
|
1925
|
+
if (!entityMatchesTags(entity, this.tags) || !await this.canReadEntity(entity)) {
|
|
1395
1926
|
const existing = this.currentMembers.get(entity.url);
|
|
1396
1927
|
if (!existing) return;
|
|
1397
1928
|
this.append(`delete`, existing);
|
|
@@ -1420,6 +1951,15 @@ var ProjectedEntityBridge = class {
|
|
|
1420
1951
|
this.currentMembers.set(entity.url, next);
|
|
1421
1952
|
}
|
|
1422
1953
|
}
|
|
1954
|
+
async canReadEntity(entity) {
|
|
1955
|
+
if (this.permissionBypass) return true;
|
|
1956
|
+
if (!this.principalUrl || !this.principalKind) return false;
|
|
1957
|
+
if (entity.created_by === this.principalUrl) return true;
|
|
1958
|
+
return await this.registry.hasEntityPermission(entity.url, `read`, {
|
|
1959
|
+
principalUrl: this.principalUrl,
|
|
1960
|
+
principalKind: this.principalKind
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1423
1963
|
async ensureStream() {
|
|
1424
1964
|
if (!await this.streamClient.exists(this.streamUrl)) await this.streamClient.create(this.streamUrl, { contentType: `application/json` });
|
|
1425
1965
|
}
|
|
@@ -1524,17 +2064,19 @@ var EntityProjector = class {
|
|
|
1524
2064
|
this.activeReaders.clear();
|
|
1525
2065
|
await Promise.all(projections.map((projection) => projection.stop()));
|
|
1526
2066
|
}
|
|
1527
|
-
async register(tenantId, registry, tagsInput) {
|
|
2067
|
+
async register(tenantId, registry, tagsInput, principalUrl$1, principalKind) {
|
|
1528
2068
|
if (!this.electricUrl) throw new Error(`[entity-projector] Electric URL is required for entities()`);
|
|
1529
2069
|
await this.start();
|
|
1530
2070
|
this.registries.set(tenantId, registry);
|
|
1531
2071
|
const tags = (0, __electric_ax_agents_runtime.normalizeTags)((0, __electric_ax_agents_runtime.assertTags)(tagsInput));
|
|
1532
|
-
const sourceRef = (0, __electric_ax_agents_runtime.sourceRefForTags)(tags);
|
|
2072
|
+
const sourceRef = principalScopedSourceRef((0, __electric_ax_agents_runtime.sourceRefForTags)(tags), principalUrl$1, principalKind);
|
|
1533
2073
|
const streamUrl = (0, __electric_ax_agents_runtime.getEntitiesStreamPath)(sourceRef);
|
|
1534
2074
|
const row = await registry.upsertEntityBridge({
|
|
1535
2075
|
sourceRef,
|
|
1536
2076
|
tags,
|
|
1537
|
-
streamUrl
|
|
2077
|
+
streamUrl,
|
|
2078
|
+
principalUrl: principalUrl$1,
|
|
2079
|
+
principalKind
|
|
1538
2080
|
});
|
|
1539
2081
|
await registry.touchEntityBridge(sourceRef);
|
|
1540
2082
|
await this.ensureProjection(row);
|
|
@@ -1563,7 +2105,11 @@ var EntityProjector = class {
|
|
|
1563
2105
|
await this.touchSourceRef(tenantId, registry, sourceRef, `read-close`);
|
|
1564
2106
|
};
|
|
1565
2107
|
}
|
|
1566
|
-
async onEntityChanged(
|
|
2108
|
+
async onEntityChanged(tenantId, entityUrl) {
|
|
2109
|
+
const entity = this.entities.get(entityKey(tenantId, entityUrl));
|
|
2110
|
+
if (!entity) return;
|
|
2111
|
+
for (const projection of this.projectionsForTenant(tenantId)) await projection.applyEntity(entity);
|
|
2112
|
+
}
|
|
1567
2113
|
async loadTenantBridges(tenantId, registry = this.registryForTenant(tenantId)) {
|
|
1568
2114
|
if (!this.started || !this.electricUrl) return;
|
|
1569
2115
|
await this.loadPersistedBridgesForTenant(tenantId, registry);
|
|
@@ -1624,16 +2170,16 @@ var EntityProjector = class {
|
|
|
1624
2170
|
}
|
|
1625
2171
|
if (message.headers.control === `up-to-date`) {
|
|
1626
2172
|
this.upToDate = true;
|
|
1627
|
-
this.reconcileAll();
|
|
2173
|
+
await this.reconcileAll();
|
|
1628
2174
|
this.readyResolve?.();
|
|
1629
2175
|
}
|
|
1630
2176
|
continue;
|
|
1631
2177
|
}
|
|
1632
2178
|
if (!(0, __electric_sql_client.isChangeMessage)(message)) continue;
|
|
1633
|
-
this.applyChangeMessage(message);
|
|
2179
|
+
await this.applyChangeMessage(message);
|
|
1634
2180
|
}
|
|
1635
2181
|
}
|
|
1636
|
-
applyChangeMessage(message) {
|
|
2182
|
+
async applyChangeMessage(message) {
|
|
1637
2183
|
const entity = message.value;
|
|
1638
2184
|
const key = entityKey(entity.tenant_id, entity.url);
|
|
1639
2185
|
if (message.headers.operation === `delete`) {
|
|
@@ -1642,7 +2188,7 @@ var EntityProjector = class {
|
|
|
1642
2188
|
return;
|
|
1643
2189
|
}
|
|
1644
2190
|
this.entities.set(key, entity);
|
|
1645
|
-
if (this.upToDate) for (const projection of this.projectionsForTenant(entity.tenant_id)) projection.applyEntity(entity);
|
|
2191
|
+
if (this.upToDate) for (const projection of this.projectionsForTenant(entity.tenant_id)) await projection.applyEntity(entity);
|
|
1646
2192
|
}
|
|
1647
2193
|
async loadPersistedBridges() {
|
|
1648
2194
|
const registry = new PostgresRegistry(this.db);
|
|
@@ -1705,7 +2251,7 @@ var EntityProjector = class {
|
|
|
1705
2251
|
}
|
|
1706
2252
|
throw error;
|
|
1707
2253
|
}
|
|
1708
|
-
const projection = new ProjectedEntityBridge(row, streamClient);
|
|
2254
|
+
const projection = new ProjectedEntityBridge(row, this.registryForTenant(row.tenantId), streamClient);
|
|
1709
2255
|
await projection.start(this.entitiesForTenant(row.tenantId));
|
|
1710
2256
|
this.projections.set(key, projection);
|
|
1711
2257
|
})().finally(() => {
|
|
@@ -1720,8 +2266,8 @@ var EntityProjector = class {
|
|
|
1720
2266
|
projectionsForTenant(tenantId) {
|
|
1721
2267
|
return [...this.projections.values()].filter((projection) => projection.tenantId === tenantId);
|
|
1722
2268
|
}
|
|
1723
|
-
reconcileAll() {
|
|
1724
|
-
for (const projection of this.projections.values()) projection.reconcile(this.entitiesForTenant(projection.tenantId));
|
|
2269
|
+
async reconcileAll() {
|
|
2270
|
+
for (const projection of this.projections.values()) await projection.reconcile(this.entitiesForTenant(projection.tenantId));
|
|
1725
2271
|
}
|
|
1726
2272
|
async touchSourceRef(tenantId, registry, sourceRef, reason) {
|
|
1727
2273
|
try {
|
|
@@ -1763,8 +2309,8 @@ var EntityProjectorTenantFacade = class {
|
|
|
1763
2309
|
await this.projector.start();
|
|
1764
2310
|
}
|
|
1765
2311
|
async stop() {}
|
|
1766
|
-
async register(tagsInput) {
|
|
1767
|
-
return await this.projector.register(this.tenantId, this.registry, tagsInput);
|
|
2312
|
+
async register(tagsInput, principalUrl$1, principalKind) {
|
|
2313
|
+
return await this.projector.register(this.tenantId, this.registry, tagsInput, principalUrl$1, principalKind);
|
|
1768
2314
|
}
|
|
1769
2315
|
async onEntityChanged(entityUrl) {
|
|
1770
2316
|
await this.projector.onEntityChanged(this.tenantId, entityUrl);
|
|
@@ -2007,7 +2553,7 @@ var StreamClient = class {
|
|
|
2007
2553
|
});
|
|
2008
2554
|
});
|
|
2009
2555
|
}
|
|
2010
|
-
async fork(path$2, sourcePath) {
|
|
2556
|
+
async fork(path$2, sourcePath, opts) {
|
|
2011
2557
|
return await withSpan(`stream.fork`, async (span) => {
|
|
2012
2558
|
span.setAttributes({
|
|
2013
2559
|
[ATTR.STREAM_PATH]: path$2,
|
|
@@ -2017,6 +2563,11 @@ var StreamClient = class {
|
|
|
2017
2563
|
"content-type": `application/json`,
|
|
2018
2564
|
"Stream-Forked-From": new URL(this.streamUrl(sourcePath)).pathname
|
|
2019
2565
|
};
|
|
2566
|
+
if (opts?.forkPointer) {
|
|
2567
|
+
const ZERO_OFFSET = `0000000000000000_0000000000000000`;
|
|
2568
|
+
headers[`Stream-Fork-Offset`] = opts.forkPointer.offset ?? ZERO_OFFSET;
|
|
2569
|
+
if (opts.forkPointer.subOffset > 0) headers[`Stream-Fork-Sub-Offset`] = String(opts.forkPointer.subOffset);
|
|
2570
|
+
}
|
|
2020
2571
|
injectTraceHeaders(headers);
|
|
2021
2572
|
const response = await fetch(this.streamUrl(path$2), {
|
|
2022
2573
|
method: `PUT`,
|
|
@@ -2545,91 +3096,101 @@ async function linkStreamToTargetSubscription(ctx, target, entity, subscriptionI
|
|
|
2545
3096
|
}
|
|
2546
3097
|
|
|
2547
3098
|
//#endregion
|
|
2548
|
-
//#region src/
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
3099
|
+
//#region src/routing/sandbox.ts
|
|
3100
|
+
/**
|
|
3101
|
+
* Resolve and validate a spawn's sandbox CHOICE into the {@link
|
|
3102
|
+
* EntitySandboxSelection} persisted on the entity. Sibling of
|
|
3103
|
+
* `dispatch-policy.ts`'s `resolveEffectiveDispatchPolicyForSpawn`: kept off the
|
|
3104
|
+
* EntityManager so the spawn path reads as composed resolution steps.
|
|
3105
|
+
*
|
|
3106
|
+
* Profiles are a per-runner concern: each runner advertises what it supports.
|
|
3107
|
+
* When the spawn pins a runner via dispatch_policy, the chosen profile must be
|
|
3108
|
+
* in that runner's advertised set; otherwise we'd persist an unserviceable
|
|
3109
|
+
* choice that fails late at first wake. For unpinned dispatch (webhook /
|
|
3110
|
+
* parent-inherited) we can't pick a target ahead of time, so we fall back to a
|
|
3111
|
+
* tenant-wide "some runner offers this" check — better than nothing.
|
|
3112
|
+
*/
|
|
3113
|
+
async function resolveSandboxForSpawn(registry, dispatchPolicy, requested, parentEntity) {
|
|
3114
|
+
if (!requested) return void 0;
|
|
3115
|
+
const choice = applyInheritedSandbox(requested, parentEntity);
|
|
3116
|
+
if (!choice) return void 0;
|
|
3117
|
+
const chosenName = choice.profile;
|
|
3118
|
+
if (!chosenName) throw new ElectricAgentsError(ErrCodeInvalidRequest, `sandbox requires a "profile" (or "inherit": true with a parent that has a shared sandbox).`, 400);
|
|
3119
|
+
const chosenIsRemote = await resolveChosenProfileRemote(registry, chosenName, dispatchPolicy);
|
|
3120
|
+
assertSharedSandboxColocated(choice.key, chosenIsRemote, dispatchPolicy);
|
|
3121
|
+
const selection = { profile: chosenName };
|
|
3122
|
+
if (choice.key !== void 0) selection.key = choice.key;
|
|
3123
|
+
else if (choice.scope !== void 0) selection.scope = choice.scope;
|
|
3124
|
+
if (choice.persistent !== void 0) selection.persistent = choice.persistent;
|
|
3125
|
+
if (choice.owner === false) selection.owner = false;
|
|
3126
|
+
return selection;
|
|
3127
|
+
}
|
|
3128
|
+
/**
|
|
3129
|
+
* Resolve `inherit` against the parent's *stored* sandbox. `inherit` reuses the
|
|
3130
|
+
* parent's keyed sandbox as a non-owner (attach-only). It's graceful: if the
|
|
3131
|
+
* parent has no shareable (keyed) sandbox the child simply gets none (returns
|
|
3132
|
+
* `undefined`), so `spawn_worker` can always request inheritance without
|
|
3133
|
+
* breaking unkeyed parents. (A running parent wake resolves inherit to its live
|
|
3134
|
+
* explicit key in the runtime instead — this server-side path covers direct API
|
|
3135
|
+
* callers, where only the parent's *stored* explicit key is available.)
|
|
3136
|
+
*
|
|
3137
|
+
* For a non-inherit choice the request passes through unchanged.
|
|
3138
|
+
*
|
|
3139
|
+
* NOTE: `inherit: true` takes the parent's identity AND durability wholesale —
|
|
3140
|
+
* any sibling field on the request (e.g. a caller-supplied `persistent: false`)
|
|
3141
|
+
* is intentionally ignored, because a child attaches to the parent's existing
|
|
3142
|
+
* sandbox and cannot change how that sandbox is torn down. `sandboxChoiceSchema`
|
|
3143
|
+
* permits the `{ inherit: true, persistent: ... }` combination, so the
|
|
3144
|
+
* precedence is resolved here rather than rejected at the schema level.
|
|
3145
|
+
*/
|
|
3146
|
+
function applyInheritedSandbox(requested, parentEntity) {
|
|
3147
|
+
if (!requested.inherit) return requested;
|
|
3148
|
+
const parentKey = parentEntity?.sandbox?.key;
|
|
3149
|
+
if (!parentKey) return void 0;
|
|
2564
3150
|
return {
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
3151
|
+
profile: parentEntity.sandbox.profile,
|
|
3152
|
+
key: parentKey,
|
|
3153
|
+
persistent: parentEntity.sandbox.persistent,
|
|
3154
|
+
owner: false
|
|
2569
3155
|
};
|
|
2570
3156
|
}
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
const
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
} catch {
|
|
2596
|
-
return false;
|
|
2597
|
-
}
|
|
3157
|
+
/**
|
|
3158
|
+
* Validate the chosen profile is advertised by the relevant runner(s) and
|
|
3159
|
+
* determine whether it is a remote (off-host) sandbox, reachable from any
|
|
3160
|
+
* runner. Defaults to host-local (co-location required) unless every relevant
|
|
3161
|
+
* advertisement marks it remote. Throws if the profile is unserviceable.
|
|
3162
|
+
*/
|
|
3163
|
+
async function resolveChosenProfileRemote(registry, chosenName, dispatchPolicy) {
|
|
3164
|
+
const runnerIds = [];
|
|
3165
|
+
for (const target of dispatchPolicy?.targets ?? []) if (target.type === `runner`) runnerIds.push(target.runnerId);
|
|
3166
|
+
if (runnerIds.length > 0) {
|
|
3167
|
+
let allRemote = true;
|
|
3168
|
+
for (const runnerId of runnerIds) {
|
|
3169
|
+
const runner = await registry.getRunner(runnerId);
|
|
3170
|
+
const advertised = runner?.sandbox_profiles ?? [];
|
|
3171
|
+
const match = advertised.find((p) => p.name === chosenName);
|
|
3172
|
+
if (!match) throw new ElectricAgentsError(ErrCodeInvalidRequest, `sandbox profile "${chosenName}" is not advertised by runner "${runnerId}" (advertised: ${advertised.map((p) => p.name).join(`, `) || `(none)`}).`, 400);
|
|
3173
|
+
if (match.remote !== true) allRemote = false;
|
|
3174
|
+
}
|
|
3175
|
+
return allRemote;
|
|
3176
|
+
}
|
|
3177
|
+
const available = await registry.listSandboxProfiles();
|
|
3178
|
+
const matches = available.filter((p) => p.name === chosenName);
|
|
3179
|
+
if (matches.length === 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `sandbox profile "${chosenName}" is not offered by any registered runner (available: ${[...new Set(available.map((p) => p.name))].join(`, `) || `(none)`}).`, 400);
|
|
3180
|
+
return matches.every((p) => p.remote === true);
|
|
2598
3181
|
}
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
return
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
id: principal.id
|
|
2611
|
-
};
|
|
3182
|
+
/**
|
|
3183
|
+
* Co-location: a shared *local* sandbox lives on one host, so every
|
|
3184
|
+
* collaborator must be pinned to the same single runner. Subagents inherit the
|
|
3185
|
+
* parent's dispatch policy, so this holds once the root is pinned. A shared
|
|
3186
|
+
* *remote* sandbox is reachable from any runner, so the guard does not apply.
|
|
3187
|
+
*/
|
|
3188
|
+
function assertSharedSandboxColocated(key, chosenIsRemote, dispatchPolicy) {
|
|
3189
|
+
if (key === void 0 || chosenIsRemote) return;
|
|
3190
|
+
const targets = dispatchPolicy?.targets ?? [];
|
|
3191
|
+
const pinnedToSingleRunner = targets.length === 1 && targets[0]?.type === `runner`;
|
|
3192
|
+
if (!pinnedToSingleRunner) throw new ElectricAgentsError(ErrCodeInvalidRequest, `a shared sandbox (sandbox.key / sandbox.inherit) requires the entity to be pinned to a single runner via dispatch_policy, so all collaborators share one host.`, 400);
|
|
2612
3193
|
}
|
|
2613
|
-
const principalIdentityStateSchema = __sinclair_typebox.Type.Object({
|
|
2614
|
-
kind: __sinclair_typebox.Type.Union([
|
|
2615
|
-
__sinclair_typebox.Type.Literal(`user`),
|
|
2616
|
-
__sinclair_typebox.Type.Literal(`agent`),
|
|
2617
|
-
__sinclair_typebox.Type.Literal(`service`),
|
|
2618
|
-
__sinclair_typebox.Type.Literal(`system`)
|
|
2619
|
-
]),
|
|
2620
|
-
id: __sinclair_typebox.Type.String(),
|
|
2621
|
-
key: __sinclair_typebox.Type.String(),
|
|
2622
|
-
url: __sinclair_typebox.Type.String(),
|
|
2623
|
-
updated_at: __sinclair_typebox.Type.String(),
|
|
2624
|
-
display_name: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
2625
|
-
email: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
2626
|
-
avatar_url: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
2627
|
-
auth_provider: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
2628
|
-
auth_subject: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
2629
|
-
claims: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown())),
|
|
2630
|
-
created_at: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
2631
|
-
}, { additionalProperties: false });
|
|
2632
|
-
const principalUpdateIdentityMessageSchema = __sinclair_typebox.Type.Object({ identity: principalIdentityStateSchema }, { additionalProperties: false });
|
|
2633
3194
|
|
|
2634
3195
|
//#endregion
|
|
2635
3196
|
//#region src/manifest-side-effects.ts
|
|
@@ -2866,7 +3427,10 @@ var EntityManager = class {
|
|
|
2866
3427
|
}
|
|
2867
3428
|
async ensurePrincipal(principal) {
|
|
2868
3429
|
const existing = await this.registry.getEntity(principal.url);
|
|
2869
|
-
if (existing)
|
|
3430
|
+
if (existing) {
|
|
3431
|
+
await this.ensureUserPrincipal(principal);
|
|
3432
|
+
return existing;
|
|
3433
|
+
}
|
|
2870
3434
|
await this.ensurePrincipalEntityType();
|
|
2871
3435
|
try {
|
|
2872
3436
|
const entity = await this.spawn(`principal`, {
|
|
@@ -2895,15 +3459,22 @@ var EntityManager = class {
|
|
|
2895
3459
|
updated_at: now
|
|
2896
3460
|
}
|
|
2897
3461
|
}));
|
|
3462
|
+
await this.ensureUserPrincipal(principal);
|
|
2898
3463
|
return entity;
|
|
2899
3464
|
} catch (error) {
|
|
2900
3465
|
if (error instanceof ElectricAgentsError && error.code === ErrCodeDuplicateURL) {
|
|
2901
3466
|
const raced = await this.registry.getEntity(principal.url);
|
|
2902
|
-
if (raced)
|
|
3467
|
+
if (raced) {
|
|
3468
|
+
await this.ensureUserPrincipal(principal);
|
|
3469
|
+
return raced;
|
|
3470
|
+
}
|
|
2903
3471
|
}
|
|
2904
3472
|
throw error;
|
|
2905
3473
|
}
|
|
2906
3474
|
}
|
|
3475
|
+
async ensureUserPrincipal(principal) {
|
|
3476
|
+
if (principal.kind === `user`) await this.registry.ensureUserForPrincipal(principal);
|
|
3477
|
+
}
|
|
2907
3478
|
/**
|
|
2908
3479
|
* Spawn a new entity of the given type with durable streams.
|
|
2909
3480
|
*/
|
|
@@ -2933,7 +3504,6 @@ var EntityManager = class {
|
|
|
2933
3504
|
const writeToken = (0, node_crypto.randomUUID)();
|
|
2934
3505
|
const entityURL = typeName === `principal` ? principalUrl(instanceId) : `/${typeName}/${instanceId}`;
|
|
2935
3506
|
const mainPath = `${entityURL}/main`;
|
|
2936
|
-
const errorPath = `${entityURL}/error`;
|
|
2937
3507
|
const subscriptionId = `${typeName}-handler`;
|
|
2938
3508
|
const spawnT0 = performance.now();
|
|
2939
3509
|
const existingByURL = await this.registry.getEntity(entityURL);
|
|
@@ -2944,20 +3514,19 @@ var EntityManager = class {
|
|
|
2944
3514
|
if (!parentEntity) throw new ElectricAgentsError(ErrCodeNotFound, `Parent entity "${req.parent}" not found`, 404);
|
|
2945
3515
|
}
|
|
2946
3516
|
const dispatchPolicy = req.dispatch_policy ? this.validateDispatchPolicy(req.dispatch_policy, { label: `dispatch_policy` }) : parentEntity?.dispatch_policy ? applyTypeDefaultSubscriptionScope(parentEntity.dispatch_policy, entityType.default_dispatch_policy) : entityType.default_dispatch_policy;
|
|
3517
|
+
const sandbox = await resolveSandboxForSpawn(this.registry, dispatchPolicy, req.sandbox, parentEntity);
|
|
2947
3518
|
const now = Date.now();
|
|
2948
3519
|
const entityData = {
|
|
2949
3520
|
type: typeName,
|
|
2950
3521
|
status: `idle`,
|
|
2951
3522
|
url: entityURL,
|
|
2952
|
-
streams: {
|
|
2953
|
-
main: mainPath,
|
|
2954
|
-
error: errorPath
|
|
2955
|
-
},
|
|
3523
|
+
streams: { main: mainPath },
|
|
2956
3524
|
subscription_id: subscriptionId,
|
|
2957
3525
|
dispatch_policy: dispatchPolicy,
|
|
2958
3526
|
write_token: writeToken,
|
|
2959
3527
|
tags: initialTags,
|
|
2960
3528
|
spawn_args: req.args,
|
|
3529
|
+
sandbox,
|
|
2961
3530
|
type_revision: entityType.revision,
|
|
2962
3531
|
inbox_schemas: entityType.inbox_schemas,
|
|
2963
3532
|
state_schemas: entityType.state_schemas,
|
|
@@ -3004,55 +3573,43 @@ var EntityManager = class {
|
|
|
3004
3573
|
const queueEnterT0 = performance.now();
|
|
3005
3574
|
const queueWaiting = this.spawnPersistQueue.length();
|
|
3006
3575
|
const queueRunning = this.spawnPersistQueue.running();
|
|
3007
|
-
const [mainStreamResult,
|
|
3576
|
+
const [mainStreamResult, entityResult] = await this.spawnPersistQueue.push(async () => {
|
|
3008
3577
|
let entityTxid;
|
|
3009
3578
|
try {
|
|
3010
3579
|
entityTxid = await withSpan(`db.createEntity`, () => this.registry.createEntity(entityData));
|
|
3011
3580
|
} catch (err) {
|
|
3012
|
-
return [
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
value: void 0
|
|
3020
|
-
},
|
|
3021
|
-
{
|
|
3022
|
-
status: `rejected`,
|
|
3023
|
-
reason: err
|
|
3024
|
-
}
|
|
3025
|
-
];
|
|
3581
|
+
return [{
|
|
3582
|
+
status: `fulfilled`,
|
|
3583
|
+
value: void 0
|
|
3584
|
+
}, {
|
|
3585
|
+
status: `rejected`,
|
|
3586
|
+
reason: err
|
|
3587
|
+
}];
|
|
3026
3588
|
}
|
|
3027
|
-
const [mainStreamResult$1
|
|
3589
|
+
const [mainStreamResult$1] = await Promise.allSettled([this.streamClient.create(mainPath, {
|
|
3028
3590
|
contentType,
|
|
3029
3591
|
body: initialBody
|
|
3030
|
-
})
|
|
3031
|
-
return [
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
status: `fulfilled`,
|
|
3036
|
-
value: entityTxid
|
|
3037
|
-
}
|
|
3038
|
-
];
|
|
3592
|
+
})]);
|
|
3593
|
+
return [mainStreamResult$1, {
|
|
3594
|
+
status: `fulfilled`,
|
|
3595
|
+
value: entityTxid
|
|
3596
|
+
}];
|
|
3039
3597
|
});
|
|
3040
3598
|
const parallelMs = +(performance.now() - queueEnterT0).toFixed(2);
|
|
3041
|
-
if (mainStreamResult.status === `rejected` ||
|
|
3599
|
+
if (mainStreamResult.status === `rejected` || entityResult.status === `rejected`) {
|
|
3042
3600
|
const entityReason = entityResult.status === `rejected` ? entityResult.reason : null;
|
|
3043
|
-
const streamReason = mainStreamResult.status === `rejected` ? mainStreamResult.reason :
|
|
3601
|
+
const streamReason = mainStreamResult.status === `rejected` ? mainStreamResult.reason : null;
|
|
3044
3602
|
const isDuplicate = entityReason instanceof EntityAlreadyExistsError;
|
|
3045
3603
|
const isStreamConflict = !!streamReason && typeof streamReason === `object` && (`status` in streamReason && streamReason.status === 409 || `code` in streamReason && streamReason.code === `CONFLICT_SEQ`);
|
|
3046
3604
|
const rollbacks = [];
|
|
3047
3605
|
if (!isDuplicate && !isStreamConflict) {
|
|
3048
3606
|
if (mainStreamResult.status === `fulfilled`) rollbacks.push(this.streamClient.delete(mainPath));
|
|
3049
|
-
if (errorStreamResult.status === `fulfilled`) rollbacks.push(this.streamClient.delete(errorPath));
|
|
3050
3607
|
if (entityResult.status === `fulfilled`) rollbacks.push(this.registry.deleteEntity(entityURL));
|
|
3051
3608
|
if (req.wake) rollbacks.push(this.wakeRegistry.unregisterBySubscriberAndSource(req.wake.subscriberUrl, entityURL, this.tenantId));
|
|
3052
3609
|
await Promise.allSettled(rollbacks);
|
|
3053
3610
|
}
|
|
3054
3611
|
if (isDuplicate || isStreamConflict) throw new ElectricAgentsError(ErrCodeDuplicateURL, `Entity already exists at URL "${entityURL}"`, 409);
|
|
3055
|
-
const failure = mainStreamResult.status === `rejected` ? mainStreamResult.reason :
|
|
3612
|
+
const failure = mainStreamResult.status === `rejected` ? mainStreamResult.reason : entityResult.reason;
|
|
3056
3613
|
if (failure instanceof Error) throw failure;
|
|
3057
3614
|
throw new ElectricAgentsError(`SPAWN_FAILED`, `Spawn failed: ${String(failure)}`, 500);
|
|
3058
3615
|
}
|
|
@@ -3087,30 +3644,67 @@ var EntityManager = class {
|
|
|
3087
3644
|
const writeEntityLocks = new Set();
|
|
3088
3645
|
const writeStreamLocks = new Set();
|
|
3089
3646
|
try {
|
|
3090
|
-
|
|
3647
|
+
let sourceTree;
|
|
3648
|
+
if (opts.forkPointer) {
|
|
3649
|
+
const rootEntity = await this.registry.getEntity(rootUrl);
|
|
3650
|
+
if (!rootEntity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3651
|
+
if (isTerminalEntityStatus(rootEntity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Cannot fork terminal entity "${rootEntity.url}"`, 409);
|
|
3652
|
+
sourceTree = await this.listEntitySubtree(rootEntity);
|
|
3653
|
+
} else sourceTree = await this.waitForIdleSubtree(rootUrl, opts, workLocks);
|
|
3091
3654
|
const sourceRoot = sourceTree[0];
|
|
3092
3655
|
if (sourceRoot.parent) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Only top-level entities can be forked`, 400);
|
|
3093
|
-
|
|
3656
|
+
let preFilteredRoot;
|
|
3657
|
+
if (opts.forkPointer) {
|
|
3658
|
+
const sourceEvents = await this.streamClient.readJson(sourceRoot.streams.main);
|
|
3659
|
+
const flat = sourceEvents.flatMap((item) => Array.isArray(item) ? item : [item]);
|
|
3660
|
+
const target = this.resolveForkPointerTarget(flat, opts.forkPointer, sourceRoot.streams.main);
|
|
3661
|
+
const filteredEvents = flat.slice(0, target);
|
|
3662
|
+
const rootManifests = this.reduceStateRows(filteredEvents, `manifest`);
|
|
3663
|
+
const sharedStateIds = new Set();
|
|
3664
|
+
for (const manifest of rootManifests.values()) this.collectSharedStateIds(manifest, sharedStateIds);
|
|
3665
|
+
preFilteredRoot = {
|
|
3666
|
+
manifests: rootManifests,
|
|
3667
|
+
childStatuses: this.reduceStateRows(filteredEvents, `child_status`),
|
|
3668
|
+
replayWatermarks: this.reduceStateRows(filteredEvents, `replay_watermark`),
|
|
3669
|
+
sharedStateIds
|
|
3670
|
+
};
|
|
3671
|
+
}
|
|
3672
|
+
const effectiveSubtree = preFilteredRoot ? this.computeEffectiveSubtree(sourceTree, sourceRoot.url, preFilteredRoot.manifests) : sourceTree;
|
|
3673
|
+
if (opts.forkPointer) {
|
|
3674
|
+
const descendants = effectiveSubtree.filter((entity) => entity.url !== sourceRoot.url);
|
|
3675
|
+
if (descendants.length > 0) await this.waitForGivenEntitiesIdle(descendants, opts, workLocks);
|
|
3676
|
+
}
|
|
3677
|
+
const snapshot = await this.readForkStateSnapshot(
|
|
3678
|
+
// Skip the root when we've already pre-filtered it — avoid both a
|
|
3679
|
+
// wasted HEAD read of main and a re-population that would clobber
|
|
3680
|
+
// the filtered entries.
|
|
3681
|
+
preFilteredRoot ? effectiveSubtree.filter((entity) => entity.url !== sourceRoot.url) : effectiveSubtree
|
|
3682
|
+
);
|
|
3683
|
+
if (preFilteredRoot) {
|
|
3684
|
+
snapshot.manifestsByEntity.set(sourceRoot.url, preFilteredRoot.manifests);
|
|
3685
|
+
snapshot.childStatusesByEntity.set(sourceRoot.url, preFilteredRoot.childStatuses);
|
|
3686
|
+
snapshot.replayWatermarksByEntity.set(sourceRoot.url, preFilteredRoot.replayWatermarks);
|
|
3687
|
+
for (const id of preFilteredRoot.sharedStateIds) snapshot.sharedStateIds.add(id);
|
|
3688
|
+
}
|
|
3094
3689
|
const suffix = (0, node_crypto.randomUUID)().slice(0, 8);
|
|
3095
|
-
const entityUrlMap = await this.buildForkEntityUrlMap(
|
|
3690
|
+
const entityUrlMap = await this.buildForkEntityUrlMap(effectiveSubtree, {
|
|
3096
3691
|
suffix,
|
|
3097
3692
|
rootUrl,
|
|
3098
3693
|
rootInstanceId: opts.rootInstanceId
|
|
3099
3694
|
});
|
|
3100
3695
|
const sharedStateIdMap = await this.buildForkSharedStateIdMap(snapshot.sharedStateIds, suffix);
|
|
3101
3696
|
const stringMap = this.buildForkStringMap(entityUrlMap, sharedStateIdMap);
|
|
3102
|
-
const entityPlans = this.buildForkEntityPlans(
|
|
3103
|
-
this.addForkLocks(this.forkWriteLockedEntities,
|
|
3697
|
+
const entityPlans = this.buildForkEntityPlans(effectiveSubtree, entityUrlMap, stringMap, opts.createdBy);
|
|
3698
|
+
this.addForkLocks(this.forkWriteLockedEntities, effectiveSubtree.map((entity) => entity.url), writeEntityLocks);
|
|
3104
3699
|
this.addForkLocks(this.forkWriteLockedStreams, [...snapshot.sharedStateIds].map((id) => (0, __electric_ax_agents_runtime.getSharedStateStreamPath)(id)), writeStreamLocks);
|
|
3105
3700
|
const createdStreams = [];
|
|
3106
3701
|
const createdEntities = [];
|
|
3107
3702
|
const activeManifestsByEntity = new Map();
|
|
3108
3703
|
try {
|
|
3109
3704
|
for (const plan of entityPlans) {
|
|
3110
|
-
|
|
3705
|
+
const isRoot = plan.source.url === rootUrl;
|
|
3706
|
+
await this.streamClient.fork(plan.fork.streams.main, plan.source.streams.main, isRoot && opts.forkPointer ? { forkPointer: opts.forkPointer } : void 0);
|
|
3111
3707
|
createdStreams.push(plan.fork.streams.main);
|
|
3112
|
-
await this.streamClient.fork(plan.fork.streams.error, plan.source.streams.error);
|
|
3113
|
-
createdStreams.push(plan.fork.streams.error);
|
|
3114
3708
|
}
|
|
3115
3709
|
for (const [sourceId, forkId] of sharedStateIdMap) {
|
|
3116
3710
|
const sourcePath = (0, __electric_ax_agents_runtime.getSharedStateStreamPath)(sourceId);
|
|
@@ -3200,6 +3794,38 @@ var EntityManager = class {
|
|
|
3200
3794
|
}
|
|
3201
3795
|
held.clear();
|
|
3202
3796
|
}
|
|
3797
|
+
/**
|
|
3798
|
+
* Variant of {@link waitForIdleSubtree} that takes an explicit entity
|
|
3799
|
+
* list instead of walking the registry from `rootUrl`. Used by the
|
|
3800
|
+
* pointer-fork path to wait+lock only the kept descendants, since
|
|
3801
|
+
* the root is being forked from history and doesn't need to be idle.
|
|
3802
|
+
*/
|
|
3803
|
+
async waitForGivenEntitiesIdle(entities$1, opts, workLocks) {
|
|
3804
|
+
if (entities$1.length === 0) return;
|
|
3805
|
+
const timeoutMs = opts.waitTimeoutMs ?? DEFAULT_FORK_WAIT_TIMEOUT_MS;
|
|
3806
|
+
const pollMs = opts.waitPollMs ?? DEFAULT_FORK_WAIT_POLL_MS;
|
|
3807
|
+
const refresh = async () => {
|
|
3808
|
+
const refreshed = await Promise.all(entities$1.map((entity) => this.registry.getEntity(entity.url)));
|
|
3809
|
+
return refreshed.filter((entity) => !!entity);
|
|
3810
|
+
};
|
|
3811
|
+
const deadline = Date.now() + timeoutMs;
|
|
3812
|
+
while (true) {
|
|
3813
|
+
const present = await refresh();
|
|
3814
|
+
const stopped = present.find((entity) => isTerminalEntityStatus(entity.status));
|
|
3815
|
+
if (stopped) throw new ElectricAgentsError(ErrCodeNotRunning, `Cannot fork terminal entity "${stopped.url}"`, 409);
|
|
3816
|
+
let active = present.filter((entity) => entity.status !== `idle` && entity.status !== `paused`);
|
|
3817
|
+
if (active.length === 0) {
|
|
3818
|
+
this.addForkLocks(this.forkWorkLockedEntities, present.map((entity) => entity.url), workLocks);
|
|
3819
|
+
const reChecked = await refresh();
|
|
3820
|
+
const reActive = reChecked.filter((entity) => entity.status !== `idle` && entity.status !== `paused`);
|
|
3821
|
+
if (reActive.length === 0) return;
|
|
3822
|
+
this.releaseForkLocks(this.forkWorkLockedEntities, workLocks);
|
|
3823
|
+
active = reActive;
|
|
3824
|
+
}
|
|
3825
|
+
if (Date.now() >= deadline) throw new ElectricAgentsError(ErrCodeForkWaitTimeout, `Timed out waiting for descendants to become idle`, 409, { active: active.map((entity) => entity.url) });
|
|
3826
|
+
await sleep(Math.min(pollMs, Math.max(0, deadline - Date.now())));
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3203
3829
|
async waitForIdleSubtree(rootUrl, opts, workLocks) {
|
|
3204
3830
|
const timeoutMs = opts.waitTimeoutMs ?? DEFAULT_FORK_WAIT_TIMEOUT_MS;
|
|
3205
3831
|
const pollMs = opts.waitPollMs ?? DEFAULT_FORK_WAIT_POLL_MS;
|
|
@@ -3229,6 +3855,73 @@ var EntityManager = class {
|
|
|
3229
3855
|
await sleep(Math.min(pollMs, Math.max(0, deadline - Date.now())));
|
|
3230
3856
|
}
|
|
3231
3857
|
}
|
|
3858
|
+
/**
|
|
3859
|
+
* Translate `forkPointer` into a 1-indexed CUMULATIVE position in the
|
|
3860
|
+
* source's flattened history. Throws a 400 if the pointer doesn't
|
|
3861
|
+
* address a real event.
|
|
3862
|
+
*
|
|
3863
|
+
* Semantics (mirroring the durable-streams server interpretation):
|
|
3864
|
+
* `{ offset: X, subOffset: N }` means "from anchor X, take N flattened
|
|
3865
|
+
* messages forward." Concretely, the target event is the N-th event
|
|
3866
|
+
* after the last event whose `headers.offset` is ≤ X. (When `X` is
|
|
3867
|
+
* `null`, the anchor is the stream start and the target is the N-th
|
|
3868
|
+
* event from the very beginning.) The returned position is the count
|
|
3869
|
+
* of events to KEEP — events 1..position survive the filter.
|
|
3870
|
+
*
|
|
3871
|
+
* A pointer is valid when:
|
|
3872
|
+
* - `pointer.offset` is `null` (stream start) OR matches some
|
|
3873
|
+
* event's `headers.offset` value, AND
|
|
3874
|
+
* - `pointer.subOffset` is in `[1, total events past the anchor]`.
|
|
3875
|
+
*/
|
|
3876
|
+
resolveForkPointerTarget(events, pointer, streamPath) {
|
|
3877
|
+
let positionAtAnchor = 0;
|
|
3878
|
+
let anchorSeen = pointer.offset === null;
|
|
3879
|
+
for (const event of events) {
|
|
3880
|
+
const headers = isRecord(event.headers) ? event.headers : void 0;
|
|
3881
|
+
const eventOffset = typeof headers?.offset === `string` ? headers.offset : void 0;
|
|
3882
|
+
if (eventOffset === void 0) continue;
|
|
3883
|
+
if (pointer.offset === null) continue;
|
|
3884
|
+
if (eventOffset === pointer.offset) anchorSeen = true;
|
|
3885
|
+
if (eventOffset <= pointer.offset) positionAtAnchor++;
|
|
3886
|
+
}
|
|
3887
|
+
if (!anchorSeen) throw new ElectricAgentsError(ErrCodeInvalidRequest, `fork_pointer.offset (${pointer.offset ?? `<stream-start>`}) does not match any event's Stream-Next-Offset on ${streamPath}`, 400);
|
|
3888
|
+
const eventsPastAnchor = events.length - positionAtAnchor;
|
|
3889
|
+
if (pointer.subOffset < 1 || pointer.subOffset > eventsPastAnchor) throw new ElectricAgentsError(ErrCodeInvalidRequest, `fork_pointer.sub_offset ${pointer.subOffset} out of range past anchor on ${streamPath} (valid: 1..${eventsPastAnchor})`, 400);
|
|
3890
|
+
return positionAtAnchor + pointer.subOffset;
|
|
3891
|
+
}
|
|
3892
|
+
/**
|
|
3893
|
+
* Compute the subset of `sourceTree` that survives the manifest filter
|
|
3894
|
+
* applied at the root. After filtering the root's manifest at the fork
|
|
3895
|
+
* pointer, only children whose manifest entries landed at or before the
|
|
3896
|
+
* pointer remain; those kept children carry their CURRENT (HEAD) subtree
|
|
3897
|
+
* along with them. Children dropped from the root's manifest, and any
|
|
3898
|
+
* of their descendants, are excluded.
|
|
3899
|
+
*/
|
|
3900
|
+
computeEffectiveSubtree(sourceTree, rootUrl, filteredRootManifests) {
|
|
3901
|
+
const keptChildUrls = new Set();
|
|
3902
|
+
for (const value of filteredRootManifests.values()) if (value.kind === `child` && typeof value.entity_url === `string`) keptChildUrls.add(value.entity_url);
|
|
3903
|
+
const childrenByParent = new Map();
|
|
3904
|
+
for (const entity of sourceTree) {
|
|
3905
|
+
if (!entity.parent) continue;
|
|
3906
|
+
const list = childrenByParent.get(entity.parent) ?? [];
|
|
3907
|
+
list.push(entity);
|
|
3908
|
+
childrenByParent.set(entity.parent, list);
|
|
3909
|
+
}
|
|
3910
|
+
const rootEntity = sourceTree.find((e) => e.url === rootUrl);
|
|
3911
|
+
if (!rootEntity) return [];
|
|
3912
|
+
const result = [rootEntity];
|
|
3913
|
+
const queue = [];
|
|
3914
|
+
for (const child of childrenByParent.get(rootUrl) ?? []) if (keptChildUrls.has(child.url)) queue.push(child);
|
|
3915
|
+
const seen = new Set([rootUrl]);
|
|
3916
|
+
while (queue.length > 0) {
|
|
3917
|
+
const entity = queue.shift();
|
|
3918
|
+
if (seen.has(entity.url)) continue;
|
|
3919
|
+
seen.add(entity.url);
|
|
3920
|
+
result.push(entity);
|
|
3921
|
+
for (const grandchild of childrenByParent.get(entity.url) ?? []) if (!seen.has(grandchild.url)) queue.push(grandchild);
|
|
3922
|
+
}
|
|
3923
|
+
return result;
|
|
3924
|
+
}
|
|
3232
3925
|
async listEntitySubtree(root) {
|
|
3233
3926
|
const result = [];
|
|
3234
3927
|
const queue = [root];
|
|
@@ -3345,7 +4038,6 @@ var EntityManager = class {
|
|
|
3345
4038
|
for (const [sourceUrl, forkUrl] of entityUrlMap) {
|
|
3346
4039
|
stringMap.set(sourceUrl, forkUrl);
|
|
3347
4040
|
stringMap.set(`${sourceUrl}/main`, `${forkUrl}/main`);
|
|
3348
|
-
stringMap.set(`${sourceUrl}/error`, `${forkUrl}/error`);
|
|
3349
4041
|
}
|
|
3350
4042
|
for (const [sourceId, forkId] of sharedStateIdMap) {
|
|
3351
4043
|
stringMap.set(sourceId, forkId);
|
|
@@ -3353,7 +4045,7 @@ var EntityManager = class {
|
|
|
3353
4045
|
}
|
|
3354
4046
|
return stringMap;
|
|
3355
4047
|
}
|
|
3356
|
-
buildForkEntityPlans(entitiesToFork, entityUrlMap, stringMap) {
|
|
4048
|
+
buildForkEntityPlans(entitiesToFork, entityUrlMap, stringMap, createdBy) {
|
|
3357
4049
|
const now = Date.now();
|
|
3358
4050
|
return entitiesToFork.map((source) => {
|
|
3359
4051
|
const forkUrl = entityUrlMap.get(source.url);
|
|
@@ -3366,14 +4058,12 @@ var EntityManager = class {
|
|
|
3366
4058
|
url: forkUrl,
|
|
3367
4059
|
type,
|
|
3368
4060
|
status: `idle`,
|
|
3369
|
-
streams: {
|
|
3370
|
-
main: `${forkUrl}/main`,
|
|
3371
|
-
error: `${forkUrl}/error`
|
|
3372
|
-
},
|
|
4061
|
+
streams: { main: `${forkUrl}/main` },
|
|
3373
4062
|
subscription_id: `${type}-handler`,
|
|
3374
4063
|
write_token: (0, node_crypto.randomUUID)(),
|
|
3375
4064
|
spawn_args: spawnArgs,
|
|
3376
4065
|
parent,
|
|
4066
|
+
created_by: createdBy ?? source.created_by,
|
|
3377
4067
|
created_at: now,
|
|
3378
4068
|
updated_at: now
|
|
3379
4069
|
};
|
|
@@ -3607,7 +4297,7 @@ var EntityManager = class {
|
|
|
3607
4297
|
}
|
|
3608
4298
|
async materializeForkManifestSideEffects(entityUrl, manifests) {
|
|
3609
4299
|
for (const [manifestKey, manifest] of manifests) {
|
|
3610
|
-
await this.
|
|
4300
|
+
await this.syncManifestLinks(entityUrl, manifestKey, `upsert`, manifest);
|
|
3611
4301
|
const wake = buildManifestWakeRegistration(entityUrl, manifest, manifestKey);
|
|
3612
4302
|
if (wake) await this.wakeRegistry.register({
|
|
3613
4303
|
...wake,
|
|
@@ -3637,6 +4327,7 @@ var EntityManager = class {
|
|
|
3637
4327
|
await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
|
|
3638
4328
|
entityUrl: targetUrl,
|
|
3639
4329
|
from: senderUrl,
|
|
4330
|
+
from_agent: senderUrl,
|
|
3640
4331
|
payload: manifest.payload,
|
|
3641
4332
|
key: `scheduled-${producerId}`,
|
|
3642
4333
|
type: typeof manifest.messageType === `string` ? manifest.messageType : void 0,
|
|
@@ -3676,12 +4367,14 @@ var EntityManager = class {
|
|
|
3676
4367
|
const now = new Date().toISOString();
|
|
3677
4368
|
const key = req.key ?? `msg-in-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
3678
4369
|
const value = {
|
|
3679
|
-
from: req.from,
|
|
4370
|
+
from: req.from_principal ?? req.from,
|
|
3680
4371
|
payload: req.payload,
|
|
3681
4372
|
timestamp: now,
|
|
3682
4373
|
mode: req.mode ?? `immediate`,
|
|
3683
4374
|
status: req.mode === `queued` || req.mode === `paused` ? `pending` : `processed`
|
|
3684
4375
|
};
|
|
4376
|
+
if (req.from_principal) value.from_principal = req.from_principal;
|
|
4377
|
+
if (req.from_agent) value.from_agent = req.from_agent;
|
|
3685
4378
|
if (req.type) value.message_type = req.type;
|
|
3686
4379
|
if (req.position) value.position = req.position;
|
|
3687
4380
|
else if (value.mode === `queued` || value.mode === `paused`) value.position = createInitialQueuePosition(new Date(now));
|
|
@@ -3853,9 +4546,9 @@ var EntityManager = class {
|
|
|
3853
4546
|
if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
|
|
3854
4547
|
return updated;
|
|
3855
4548
|
}
|
|
3856
|
-
async ensureEntitiesMembershipStream(tags) {
|
|
4549
|
+
async ensureEntitiesMembershipStream(tags, principal) {
|
|
3857
4550
|
if (!this.entityBridgeManager) throw new Error(`Entity bridge manager not configured`);
|
|
3858
|
-
return this.entityBridgeManager.register(this.validateTags(tags));
|
|
4551
|
+
return this.entityBridgeManager.register(this.validateTags(tags), principal.url, principal.kind);
|
|
3859
4552
|
}
|
|
3860
4553
|
async writeManifestEntry(entityUrl, key, operation, value, opts) {
|
|
3861
4554
|
const entity = await this.registry.getEntity(entityUrl);
|
|
@@ -3873,11 +4566,11 @@ var EntityManager = class {
|
|
|
3873
4566
|
const encoded = this.encodeChangeEvent(event);
|
|
3874
4567
|
if (opts?.producerId) {
|
|
3875
4568
|
await this.streamClient.appendIdempotent(entity.streams.main, encoded, { producerId: opts.producerId });
|
|
3876
|
-
await this.
|
|
4569
|
+
await this.syncManifestLinks(entityUrl, key, operation, value);
|
|
3877
4570
|
return;
|
|
3878
4571
|
}
|
|
3879
4572
|
await this.streamClient.append(entity.streams.main, encoded);
|
|
3880
|
-
await this.
|
|
4573
|
+
await this.syncManifestLinks(entityUrl, key, operation, value);
|
|
3881
4574
|
}
|
|
3882
4575
|
async upsertCronSchedule(entityUrl, req) {
|
|
3883
4576
|
if (req.payload === void 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: payload`, 400);
|
|
@@ -4026,6 +4719,8 @@ var EntityManager = class {
|
|
|
4026
4719
|
await this.scheduler.enqueueDelayedSend({
|
|
4027
4720
|
entityUrl,
|
|
4028
4721
|
from: req.from,
|
|
4722
|
+
from_principal: req.from_principal,
|
|
4723
|
+
from_agent: req.from_agent,
|
|
4029
4724
|
payload: req.payload,
|
|
4030
4725
|
key: req.key,
|
|
4031
4726
|
type: req.type,
|
|
@@ -4068,14 +4763,23 @@ var EntityManager = class {
|
|
|
4068
4763
|
await this.streamClient.appendIdempotent(subscriber.streams.main, this.encodeChangeEvent(wakeEvent), { producerId: `wake-reg-${result.registrationDbId}-${result.sourceEventKey}` });
|
|
4069
4764
|
});
|
|
4070
4765
|
}
|
|
4071
|
-
async
|
|
4766
|
+
async syncManifestLinks(entityUrl, manifestKey, operation, value) {
|
|
4072
4767
|
const sourceRef = operation === `delete` ? void 0 : this.extractEntitiesSourceRef(value);
|
|
4073
4768
|
await this.registry.replaceEntityManifestSource(entityUrl, manifestKey, sourceRef);
|
|
4769
|
+
const sharedStateId = operation === `delete` ? void 0 : this.extractSharedStateId(value);
|
|
4770
|
+
await this.registry.replaceSharedStateLink(entityUrl, manifestKey, sharedStateId);
|
|
4074
4771
|
}
|
|
4075
4772
|
extractEntitiesSourceRef(manifest) {
|
|
4076
4773
|
if (manifest?.kind === `source` && manifest.sourceType === `entities` && typeof manifest.sourceRef === `string`) return manifest.sourceRef;
|
|
4077
4774
|
return void 0;
|
|
4078
4775
|
}
|
|
4776
|
+
extractSharedStateId(manifest) {
|
|
4777
|
+
if (manifest?.kind === `shared-state` && typeof manifest.id === `string`) return manifest.id;
|
|
4778
|
+
if (manifest?.kind !== `source` || manifest.sourceType !== `db`) return void 0;
|
|
4779
|
+
if (typeof manifest.sourceRef === `string`) return manifest.sourceRef;
|
|
4780
|
+
const config = isRecord(manifest.config) ? manifest.config : void 0;
|
|
4781
|
+
return typeof config?.id === `string` ? config.id : void 0;
|
|
4782
|
+
}
|
|
4079
4783
|
/**
|
|
4080
4784
|
* Read a child entity's stream and extract concatenated text deltas
|
|
4081
4785
|
* for a specific run, plus any error messages for that run.
|
|
@@ -4239,14 +4943,7 @@ var EntityManager = class {
|
|
|
4239
4943
|
await this.streamClient.append(entity.streams.main, signalData);
|
|
4240
4944
|
return;
|
|
4241
4945
|
}
|
|
4242
|
-
const
|
|
4243
|
-
type: `signal`,
|
|
4244
|
-
key: signalEvent.key,
|
|
4245
|
-
value: signalEvent.value,
|
|
4246
|
-
headers: signalEvent.headers
|
|
4247
|
-
};
|
|
4248
|
-
const errorSignalData = this.encodeChangeEvent(errorCloseEvent);
|
|
4249
|
-
for (const [streamPath, data] of [[entity.streams.main, signalData], [entity.streams.error, errorSignalData]]) try {
|
|
4946
|
+
for (const [streamPath, data] of [[entity.streams.main, signalData]]) try {
|
|
4250
4947
|
await this.streamClient.append(streamPath, data, { close: true });
|
|
4251
4948
|
} catch (err) {
|
|
4252
4949
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -4336,6 +5033,7 @@ var EntityManager = class {
|
|
|
4336
5033
|
streams: entity.streams,
|
|
4337
5034
|
tags: entity.tags,
|
|
4338
5035
|
spawnArgs: entity.spawn_args,
|
|
5036
|
+
sandbox: entity.sandbox,
|
|
4339
5037
|
createdBy: entity.created_by
|
|
4340
5038
|
},
|
|
4341
5039
|
principal: principalFromCreatedBy(entity.created_by),
|
|
@@ -5228,6 +5926,8 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
5228
5926
|
try {
|
|
5229
5927
|
await this.manager.send(payload.entityUrl, {
|
|
5230
5928
|
from: payload.from,
|
|
5929
|
+
from_principal: payload.from_principal,
|
|
5930
|
+
from_agent: payload.from_agent,
|
|
5231
5931
|
payload: payload.payload,
|
|
5232
5932
|
key: payload.key ?? `scheduled-task-${taskId}`,
|
|
5233
5933
|
type: payload.type
|
|
@@ -5300,6 +6000,7 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
5300
6000
|
await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
|
|
5301
6001
|
entityUrl: targetUrl,
|
|
5302
6002
|
from: senderUrl,
|
|
6003
|
+
from_agent: senderUrl,
|
|
5303
6004
|
payload: value.payload,
|
|
5304
6005
|
key: `scheduled-${producerId}`,
|
|
5305
6006
|
type: typeof value.messageType === `string` ? value.messageType : void 0,
|
|
@@ -5324,11 +6025,20 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
5324
6025
|
async applyManifestEntitySource(ownerEntityUrl, manifestKey, operation, value) {
|
|
5325
6026
|
const sourceRef = operation === `delete` ? void 0 : this.extractEntitiesSourceRef(value);
|
|
5326
6027
|
await this.manager.registry.replaceEntityManifestSource(ownerEntityUrl, manifestKey, sourceRef);
|
|
6028
|
+
const sharedStateId = operation === `delete` ? void 0 : this.extractSharedStateId(value);
|
|
6029
|
+
await this.manager.registry.replaceSharedStateLink(ownerEntityUrl, manifestKey, sharedStateId);
|
|
5327
6030
|
}
|
|
5328
6031
|
extractEntitiesSourceRef(manifest) {
|
|
5329
6032
|
if (manifest?.kind === `source` && manifest.sourceType === `entities` && typeof manifest.sourceRef === `string`) return manifest.sourceRef;
|
|
5330
6033
|
return void 0;
|
|
5331
6034
|
}
|
|
6035
|
+
extractSharedStateId(manifest) {
|
|
6036
|
+
if (manifest?.kind === `shared-state` && typeof manifest.id === `string`) return manifest.id;
|
|
6037
|
+
if (manifest?.kind !== `source` || manifest.sourceType !== `db`) return void 0;
|
|
6038
|
+
if (typeof manifest.sourceRef === `string`) return manifest.sourceRef;
|
|
6039
|
+
const config = typeof manifest.config === `object` && manifest.config !== null && !Array.isArray(manifest.config) ? manifest.config : void 0;
|
|
6040
|
+
return typeof config?.id === `string` ? config.id : void 0;
|
|
6041
|
+
}
|
|
5332
6042
|
async maybeMarkEntityIdleAfterRunFinished(entityUrl) {
|
|
5333
6043
|
const primaryStream = `${entityUrl}/main`;
|
|
5334
6044
|
const callbacks = await this.db.select().from(consumerCallbacks).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(consumerCallbacks.tenantId, this.serviceId), (0, drizzle_orm.eq)(consumerCallbacks.primaryStream, primaryStream))).limit(1);
|
|
@@ -6001,11 +6711,21 @@ var WakeRegistry = class {
|
|
|
6001
6711
|
}
|
|
6002
6712
|
const kind = operation === `delete` ? `delete` : operation === `update` ? `update` : `insert`;
|
|
6003
6713
|
if (condition.ops && condition.ops.length > 0 && !condition.ops.includes(kind)) return null;
|
|
6004
|
-
|
|
6714
|
+
const change = {
|
|
6005
6715
|
collection: eventType,
|
|
6006
6716
|
kind,
|
|
6007
6717
|
key: event.key || ``
|
|
6008
|
-
}
|
|
6718
|
+
};
|
|
6719
|
+
if (eventType === `inbox`) {
|
|
6720
|
+
const value = event.value;
|
|
6721
|
+
if (typeof value?.from === `string`) change.from = value.from;
|
|
6722
|
+
if (typeof value?.from_principal === `string`) change.from_principal = value.from_principal;
|
|
6723
|
+
if (typeof value?.from_agent === `string`) change.from_agent = value.from_agent;
|
|
6724
|
+
if (`payload` in (value ?? {})) change.payload = value?.payload;
|
|
6725
|
+
if (typeof value?.timestamp === `string`) change.timestamp = value.timestamp;
|
|
6726
|
+
if (typeof value?.message_type === `string`) change.message_type = value.message_type;
|
|
6727
|
+
}
|
|
6728
|
+
return { change };
|
|
6009
6729
|
}
|
|
6010
6730
|
};
|
|
6011
6731
|
|
|
@@ -6412,29 +7132,136 @@ function buildElectricProxyTarget(options) {
|
|
|
6412
7132
|
if (options.electricSecret) target.searchParams.set(`secret`, options.electricSecret);
|
|
6413
7133
|
const table = options.incomingUrl.searchParams.get(`table`);
|
|
6414
7134
|
if (table === `entities`) {
|
|
6415
|
-
target.searchParams.set(`columns`, `"tenant_id","url","type","status","dispatch_policy","tags","spawn_args","parent","type_revision","inbox_schemas","state_schemas","created_at","updated_at"`);
|
|
6416
|
-
|
|
7135
|
+
target.searchParams.set(`columns`, `"tenant_id","url","type","status","dispatch_policy","tags","spawn_args","sandbox","parent","created_by","type_revision","inbox_schemas","state_schemas","created_at","updated_at"`);
|
|
7136
|
+
applyShapeWhere(target, buildReadableEntitiesWhere({
|
|
7137
|
+
tenantId: options.tenantId,
|
|
7138
|
+
principalUrl: options.principalUrl ?? ``,
|
|
7139
|
+
principalKind: options.principalKind ?? ``,
|
|
7140
|
+
permissionBypass: options.permissionBypass
|
|
7141
|
+
}));
|
|
6417
7142
|
} else if (table === `entity_types`) {
|
|
6418
7143
|
target.searchParams.set(`columns`, `"tenant_id","name","description","creation_schema","inbox_schemas","state_schemas","serve_endpoint","default_dispatch_policy","revision","created_at","updated_at"`);
|
|
6419
|
-
|
|
7144
|
+
applyShapeWhere(target, buildSpawnableEntityTypesWhere({
|
|
7145
|
+
tenantId: options.tenantId,
|
|
7146
|
+
principalUrl: options.principalUrl ?? ``,
|
|
7147
|
+
principalKind: options.principalKind ?? ``,
|
|
7148
|
+
permissionBypass: options.permissionBypass
|
|
7149
|
+
}));
|
|
6420
7150
|
} else if (table === `runners`) {
|
|
6421
|
-
target.searchParams.set(`columns`, `"tenant_id","id","owner_principal","label","kind","admin_status","wake_stream","created_at","updated_at"`);
|
|
7151
|
+
target.searchParams.set(`columns`, `"tenant_id","id","owner_principal","label","kind","admin_status","wake_stream","sandbox_profiles","created_at","updated_at"`);
|
|
6422
7152
|
applyTenantShapeWhere(target, options.tenantId, [`owner_principal = ${sqlStringLiteral(options.principalUrl ?? ``)}`]);
|
|
7153
|
+
} else if (table === `users`) {
|
|
7154
|
+
target.searchParams.set(`columns`, `"tenant_id","id","display_name","email","avatar_url","created_at","updated_at"`);
|
|
7155
|
+
applyTenantShapeWhere(target, options.tenantId);
|
|
7156
|
+
} else if (table === `entity_effective_permissions`) {
|
|
7157
|
+
target.searchParams.set(`columns`, `"tenant_id","id","entity_url","source_entity_url","source_grant_id","permission","subject_kind","subject_value","expires_at","created_at"`);
|
|
7158
|
+
applyShapeWhere(target, buildCurrentPrincipalEntityEffectivePermissionsWhere({
|
|
7159
|
+
tenantId: options.tenantId,
|
|
7160
|
+
principalUrl: options.principalUrl ?? ``,
|
|
7161
|
+
principalKind: options.principalKind ?? ``,
|
|
7162
|
+
permissionBypass: options.permissionBypass
|
|
7163
|
+
}));
|
|
6423
7164
|
} else if (table === `runner_runtime_diagnostics`) {
|
|
6424
7165
|
target.searchParams.set(`columns`, `"tenant_id","runner_id","owner_principal","wake_stream_offset","last_seen_at","liveness_lease_expires_at","diagnostics","updated_at"`);
|
|
6425
7166
|
applyTenantShapeWhere(target, options.tenantId, [`owner_principal = ${sqlStringLiteral(options.principalUrl ?? ``)}`]);
|
|
6426
7167
|
} else if (table === `entity_dispatch_state`) {
|
|
6427
7168
|
target.searchParams.set(`columns`, `"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"`);
|
|
6428
|
-
|
|
7169
|
+
applyShapeWhere(target, buildReadableEntityUrlWhere({
|
|
7170
|
+
tenantId: options.tenantId,
|
|
7171
|
+
principalUrl: options.principalUrl ?? ``,
|
|
7172
|
+
principalKind: options.principalKind ?? ``,
|
|
7173
|
+
permissionBypass: options.permissionBypass
|
|
7174
|
+
}));
|
|
6429
7175
|
} else if (table === `wake_notifications`) {
|
|
6430
7176
|
target.searchParams.set(`columns`, `"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"`);
|
|
6431
|
-
|
|
7177
|
+
applyShapeWhere(target, buildReadableEntityUrlWhere({
|
|
7178
|
+
tenantId: options.tenantId,
|
|
7179
|
+
principalUrl: options.principalUrl ?? ``,
|
|
7180
|
+
principalKind: options.principalKind ?? ``,
|
|
7181
|
+
permissionBypass: options.permissionBypass
|
|
7182
|
+
}));
|
|
6432
7183
|
} else if (table === `consumer_claims`) {
|
|
6433
7184
|
target.searchParams.set(`columns`, `"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"`);
|
|
6434
|
-
|
|
7185
|
+
applyShapeWhere(target, buildReadableEntityUrlWhere({
|
|
7186
|
+
tenantId: options.tenantId,
|
|
7187
|
+
principalUrl: options.principalUrl ?? ``,
|
|
7188
|
+
principalKind: options.principalKind ?? ``,
|
|
7189
|
+
permissionBypass: options.permissionBypass
|
|
7190
|
+
}));
|
|
6435
7191
|
}
|
|
6436
7192
|
return target;
|
|
6437
7193
|
}
|
|
7194
|
+
function buildReadableEntitiesWhere(options) {
|
|
7195
|
+
const tenant = sqlStringLiteral(options.tenantId);
|
|
7196
|
+
if (options.permissionBypass) return `tenant_id = ${tenant}`;
|
|
7197
|
+
const principalUrl$1 = sqlStringLiteral(options.principalUrl);
|
|
7198
|
+
const principalKind = sqlStringLiteral(options.principalKind);
|
|
7199
|
+
return [
|
|
7200
|
+
`tenant_id = ${tenant}`,
|
|
7201
|
+
`AND (`,
|
|
7202
|
+
` created_by = ${principalUrl$1}`,
|
|
7203
|
+
` OR url IN (`,
|
|
7204
|
+
` SELECT entity_url`,
|
|
7205
|
+
` FROM entity_effective_permissions`,
|
|
7206
|
+
` WHERE tenant_id = ${tenant}`,
|
|
7207
|
+
` AND permission IN ('read', 'manage')`,
|
|
7208
|
+
` AND (`,
|
|
7209
|
+
` (subject_kind = 'principal' AND subject_value = ${principalUrl$1})`,
|
|
7210
|
+
` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
|
|
7211
|
+
` )`,
|
|
7212
|
+
` )`,
|
|
7213
|
+
`)`
|
|
7214
|
+
].join(`\n`);
|
|
7215
|
+
}
|
|
7216
|
+
function buildReadableEntityUrlWhere(options) {
|
|
7217
|
+
const tenant = sqlStringLiteral(options.tenantId);
|
|
7218
|
+
if (options.permissionBypass) return `tenant_id = ${tenant}`;
|
|
7219
|
+
return [
|
|
7220
|
+
`tenant_id = ${tenant}`,
|
|
7221
|
+
`AND entity_url IN (`,
|
|
7222
|
+
` SELECT url`,
|
|
7223
|
+
` FROM entities`,
|
|
7224
|
+
` WHERE ${indentWhere(buildReadableEntitiesWhere(options), ` `).trimStart()}`,
|
|
7225
|
+
`)`
|
|
7226
|
+
].join(`\n`);
|
|
7227
|
+
}
|
|
7228
|
+
function buildCurrentPrincipalEntityEffectivePermissionsWhere(options) {
|
|
7229
|
+
const tenant = sqlStringLiteral(options.tenantId);
|
|
7230
|
+
if (options.permissionBypass) return `tenant_id = ${tenant}`;
|
|
7231
|
+
const principalUrl$1 = sqlStringLiteral(options.principalUrl);
|
|
7232
|
+
const principalKind = sqlStringLiteral(options.principalKind);
|
|
7233
|
+
return [
|
|
7234
|
+
`tenant_id = ${tenant}`,
|
|
7235
|
+
`AND (`,
|
|
7236
|
+
` (subject_kind = 'principal' AND subject_value = ${principalUrl$1})`,
|
|
7237
|
+
` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
|
|
7238
|
+
`)`,
|
|
7239
|
+
`AND entity_url IN (`,
|
|
7240
|
+
` SELECT url`,
|
|
7241
|
+
` FROM entities`,
|
|
7242
|
+
` WHERE ${buildReadableEntitiesWhere(options)}`,
|
|
7243
|
+
`)`
|
|
7244
|
+
].join(`\n`);
|
|
7245
|
+
}
|
|
7246
|
+
function buildSpawnableEntityTypesWhere(options) {
|
|
7247
|
+
const tenant = sqlStringLiteral(options.tenantId);
|
|
7248
|
+
if (options.permissionBypass) return `tenant_id = ${tenant}`;
|
|
7249
|
+
const principalUrl$1 = sqlStringLiteral(options.principalUrl);
|
|
7250
|
+
const principalKind = sqlStringLiteral(options.principalKind);
|
|
7251
|
+
return [
|
|
7252
|
+
`tenant_id = ${tenant}`,
|
|
7253
|
+
`AND name IN (`,
|
|
7254
|
+
` SELECT entity_type`,
|
|
7255
|
+
` FROM entity_type_permission_grants`,
|
|
7256
|
+
` WHERE tenant_id = ${tenant}`,
|
|
7257
|
+
` AND permission IN ('spawn', 'manage')`,
|
|
7258
|
+
` AND (`,
|
|
7259
|
+
` (subject_kind = 'principal' AND subject_value = ${principalUrl$1})`,
|
|
7260
|
+
` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
|
|
7261
|
+
` )`,
|
|
7262
|
+
`)`
|
|
7263
|
+
].join(`\n`);
|
|
7264
|
+
}
|
|
6438
7265
|
async function forwardFetchRequest(options) {
|
|
6439
7266
|
const routingAdapter = resolveDurableStreamsRoutingAdapter(options.durableStreamsRouting, options.durableStreamsUrl);
|
|
6440
7267
|
const routingInput = {
|
|
@@ -6469,13 +7296,170 @@ function decodeJsonObject(body) {
|
|
|
6469
7296
|
return null;
|
|
6470
7297
|
}
|
|
6471
7298
|
function applyTenantShapeWhere(target, tenantId, extraConditions = []) {
|
|
6472
|
-
|
|
7299
|
+
applyShapeWhere(target, [`tenant_id = ${sqlStringLiteral(tenantId)}`, ...extraConditions].join(` AND `));
|
|
7300
|
+
}
|
|
7301
|
+
function applyShapeWhere(target, enforcedWhere) {
|
|
6473
7302
|
const existingWhere = target.searchParams.get(`where`);
|
|
6474
|
-
target.searchParams.set(`where`, existingWhere ? `${
|
|
7303
|
+
target.searchParams.set(`where`, existingWhere ? `${enforcedWhere} AND (${existingWhere})` : enforcedWhere);
|
|
6475
7304
|
}
|
|
6476
7305
|
function sqlStringLiteral(value) {
|
|
6477
7306
|
return `'${value.replace(/'/g, `''`)}'`;
|
|
6478
7307
|
}
|
|
7308
|
+
function indentWhere(where, prefix) {
|
|
7309
|
+
return where.split(`\n`).map((line) => `${prefix}${line}`).join(`\n`);
|
|
7310
|
+
}
|
|
7311
|
+
|
|
7312
|
+
//#endregion
|
|
7313
|
+
//#region src/permissions.ts
|
|
7314
|
+
const authzDecisionCache = new WeakMap();
|
|
7315
|
+
function principalSubject(principal) {
|
|
7316
|
+
return {
|
|
7317
|
+
principalUrl: principal.url,
|
|
7318
|
+
principalKind: principal.kind
|
|
7319
|
+
};
|
|
7320
|
+
}
|
|
7321
|
+
function isPermissionBypassPrincipal(ctx) {
|
|
7322
|
+
return isBuiltInSystemPrincipalUrl(ctx.principal.url);
|
|
7323
|
+
}
|
|
7324
|
+
async function canAccessEntity(ctx, entity, permission, request) {
|
|
7325
|
+
if (isPermissionBypassPrincipal(ctx)) return true;
|
|
7326
|
+
await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
|
|
7327
|
+
const builtInAllowed = entity.created_by === ctx.principal.url || await ctx.entityManager.registry.hasEntityPermission(entity.url, permission, principalSubject(ctx.principal));
|
|
7328
|
+
return await applyAuthorizationHook(ctx, {
|
|
7329
|
+
verb: permission,
|
|
7330
|
+
resourceKey: `entity:${entity.url}`,
|
|
7331
|
+
resource: {
|
|
7332
|
+
kind: `entity`,
|
|
7333
|
+
entity
|
|
7334
|
+
},
|
|
7335
|
+
builtInAllowed,
|
|
7336
|
+
request
|
|
7337
|
+
});
|
|
7338
|
+
}
|
|
7339
|
+
async function canAccessEntityType(ctx, entityType, permission, request) {
|
|
7340
|
+
if (isPermissionBypassPrincipal(ctx)) return true;
|
|
7341
|
+
await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
|
|
7342
|
+
const builtInAllowed = await ctx.entityManager.registry.hasEntityTypePermission(entityType.name, permission, principalSubject(ctx.principal));
|
|
7343
|
+
return await applyAuthorizationHook(ctx, {
|
|
7344
|
+
verb: permission,
|
|
7345
|
+
resourceKey: `entity_type:${entityType.name}`,
|
|
7346
|
+
resource: {
|
|
7347
|
+
kind: `entity_type`,
|
|
7348
|
+
entityType
|
|
7349
|
+
},
|
|
7350
|
+
builtInAllowed,
|
|
7351
|
+
request
|
|
7352
|
+
});
|
|
7353
|
+
}
|
|
7354
|
+
async function canRegisterEntityType(ctx, input, request) {
|
|
7355
|
+
if (isPermissionBypassPrincipal(ctx)) return true;
|
|
7356
|
+
return await applyAuthorizationHook(ctx, {
|
|
7357
|
+
verb: `manage`,
|
|
7358
|
+
resourceKey: `entity_type_registration:${input.name}`,
|
|
7359
|
+
resource: {
|
|
7360
|
+
kind: `entity_type_registration`,
|
|
7361
|
+
entityTypeName: input.name
|
|
7362
|
+
},
|
|
7363
|
+
builtInAllowed: true,
|
|
7364
|
+
request
|
|
7365
|
+
});
|
|
7366
|
+
}
|
|
7367
|
+
async function canAccessSharedState(ctx, sharedStateId, permission, request, ownerEntityUrl) {
|
|
7368
|
+
if (isPermissionBypassPrincipal(ctx)) return true;
|
|
7369
|
+
await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
|
|
7370
|
+
const storedLinkedEntityUrls = await ctx.entityManager.registry.listSharedStateLinkedEntityUrls(sharedStateId);
|
|
7371
|
+
const bootstrapEntityUrls = storedLinkedEntityUrls.length === 0 && ownerEntityUrl ? [ownerEntityUrl] : [];
|
|
7372
|
+
const linkedEntityUrls = [...new Set([...storedLinkedEntityUrls, ...bootstrapEntityUrls])];
|
|
7373
|
+
for (const entityUrl of linkedEntityUrls) {
|
|
7374
|
+
const entity = await ctx.entityManager.registry.getEntity(entityUrl);
|
|
7375
|
+
if (!entity) continue;
|
|
7376
|
+
if (entity.created_by === ctx.principal.url || await ctx.entityManager.registry.hasEntityPermission(entity.url, permission, principalSubject(ctx.principal))) return await applyAuthorizationHook(ctx, {
|
|
7377
|
+
verb: permission,
|
|
7378
|
+
resourceKey: `shared_state:${sharedStateId}`,
|
|
7379
|
+
resource: {
|
|
7380
|
+
kind: `shared_state`,
|
|
7381
|
+
sharedStateId,
|
|
7382
|
+
linkedEntityUrls
|
|
7383
|
+
},
|
|
7384
|
+
builtInAllowed: true,
|
|
7385
|
+
request
|
|
7386
|
+
});
|
|
7387
|
+
}
|
|
7388
|
+
return await applyAuthorizationHook(ctx, {
|
|
7389
|
+
verb: permission,
|
|
7390
|
+
resourceKey: `shared_state:${sharedStateId}`,
|
|
7391
|
+
resource: {
|
|
7392
|
+
kind: `shared_state`,
|
|
7393
|
+
sharedStateId,
|
|
7394
|
+
linkedEntityUrls
|
|
7395
|
+
},
|
|
7396
|
+
builtInAllowed: false,
|
|
7397
|
+
request
|
|
7398
|
+
});
|
|
7399
|
+
}
|
|
7400
|
+
async function applyAuthorizationHook(ctx, input) {
|
|
7401
|
+
const hook = ctx.authorizeRequest;
|
|
7402
|
+
if (!hook) return input.builtInAllowed;
|
|
7403
|
+
const cacheKey = [
|
|
7404
|
+
ctx.service,
|
|
7405
|
+
ctx.principal.url,
|
|
7406
|
+
input.verb,
|
|
7407
|
+
input.resourceKey
|
|
7408
|
+
].join(`|`);
|
|
7409
|
+
const cached = getCachedDecision(hook, cacheKey);
|
|
7410
|
+
if (cached) return cached.decision === `allow`;
|
|
7411
|
+
let decision;
|
|
7412
|
+
try {
|
|
7413
|
+
decision = await hook({
|
|
7414
|
+
tenant: ctx.service,
|
|
7415
|
+
principal: ctx.principal,
|
|
7416
|
+
verb: input.verb,
|
|
7417
|
+
resource: input.resource,
|
|
7418
|
+
request: input.request ? requestMetadata(input.request) : void 0,
|
|
7419
|
+
builtInAllowed: input.builtInAllowed
|
|
7420
|
+
});
|
|
7421
|
+
} catch (error) {
|
|
7422
|
+
serverLog.warn(`[agent-server] authorization hook failed:`, error);
|
|
7423
|
+
return false;
|
|
7424
|
+
}
|
|
7425
|
+
cacheDecision(hook, cacheKey, decision);
|
|
7426
|
+
return decision.decision === `allow`;
|
|
7427
|
+
}
|
|
7428
|
+
function getCachedDecision(hook, cacheKey) {
|
|
7429
|
+
const cache = authzDecisionCache.get(hook);
|
|
7430
|
+
const entry = cache?.get(cacheKey);
|
|
7431
|
+
if (!entry) return null;
|
|
7432
|
+
if (entry.expiresAt <= Date.now()) {
|
|
7433
|
+
cache?.delete(cacheKey);
|
|
7434
|
+
return null;
|
|
7435
|
+
}
|
|
7436
|
+
return { decision: entry.decision };
|
|
7437
|
+
}
|
|
7438
|
+
function cacheDecision(hook, cacheKey, decision) {
|
|
7439
|
+
if (!decision.expires_at) return;
|
|
7440
|
+
const expiresAt = Date.parse(decision.expires_at);
|
|
7441
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) return;
|
|
7442
|
+
let cache = authzDecisionCache.get(hook);
|
|
7443
|
+
if (!cache) {
|
|
7444
|
+
cache = new Map();
|
|
7445
|
+
authzDecisionCache.set(hook, cache);
|
|
7446
|
+
}
|
|
7447
|
+
cache.set(cacheKey, {
|
|
7448
|
+
decision: decision.decision,
|
|
7449
|
+
expiresAt
|
|
7450
|
+
});
|
|
7451
|
+
}
|
|
7452
|
+
function requestMetadata(request) {
|
|
7453
|
+
const headers = {};
|
|
7454
|
+
request.headers.forEach((value, key) => {
|
|
7455
|
+
headers[key] = value;
|
|
7456
|
+
});
|
|
7457
|
+
return {
|
|
7458
|
+
method: request.method,
|
|
7459
|
+
url: request.url,
|
|
7460
|
+
headers
|
|
7461
|
+
};
|
|
7462
|
+
}
|
|
6479
7463
|
|
|
6480
7464
|
//#endregion
|
|
6481
7465
|
//#region src/webhook-signing.ts
|
|
@@ -6567,6 +7551,7 @@ const subscriptionControlActions = [
|
|
|
6567
7551
|
`ack`,
|
|
6568
7552
|
`release`
|
|
6569
7553
|
];
|
|
7554
|
+
const SHARED_STATE_OWNER_ENTITY_HEADER = `electric-owner-entity`;
|
|
6570
7555
|
const durableStreamsRouter = (0, itty_router.Router)();
|
|
6571
7556
|
durableStreamsRouter.put(`/__ds/subscriptions/:subscriptionId`, putSubscriptionBase);
|
|
6572
7557
|
durableStreamsRouter.get(`/__ds/subscriptions/:subscriptionId`, getSubscriptionBase);
|
|
@@ -6784,6 +7769,8 @@ async function webhookJwks(_request, ctx) {
|
|
|
6784
7769
|
});
|
|
6785
7770
|
}
|
|
6786
7771
|
async function streamAppend(request, ctx) {
|
|
7772
|
+
const auth = await authorizeDurableStreamAccess(request, ctx);
|
|
7773
|
+
if (auth) return auth;
|
|
6787
7774
|
return await electricAgentsStreamAppendRouter.fetch(createStreamAppendRouteRequest(request), ctx.runtime, (req, body) => forwardFetchRequest({
|
|
6788
7775
|
request: {
|
|
6789
7776
|
method: req.method,
|
|
@@ -6800,8 +7787,9 @@ async function streamAppend(request, ctx) {
|
|
|
6800
7787
|
}));
|
|
6801
7788
|
}
|
|
6802
7789
|
async function proxyPassThrough(request, ctx) {
|
|
7790
|
+
const auth = await authorizeDurableStreamAccess(request, ctx);
|
|
7791
|
+
if (auth) return auth;
|
|
6803
7792
|
const streamPath = new URL(request.url).pathname;
|
|
6804
|
-
if (ctx.entityManager?.isAttachmentStreamPath(streamPath)) return new Response(null, { status: 404 });
|
|
6805
7793
|
const upstream = await forwardToDurableStreams(ctx, request);
|
|
6806
7794
|
const method = request.method.toUpperCase();
|
|
6807
7795
|
const endTrackedRead = method === `GET` ? await ctx.entityBridgeManager.beginClientRead(streamPath) : null;
|
|
@@ -6812,6 +7800,51 @@ async function proxyPassThrough(request, ctx) {
|
|
|
6812
7800
|
await endTrackedRead?.();
|
|
6813
7801
|
}
|
|
6814
7802
|
}
|
|
7803
|
+
async function authorizeDurableStreamAccess(request, ctx) {
|
|
7804
|
+
const method = request.method.toUpperCase();
|
|
7805
|
+
const streamPath = new URL(request.url).pathname;
|
|
7806
|
+
if (method === `GET` || method === `HEAD`) {
|
|
7807
|
+
const registry = ctx.entityManager?.registry;
|
|
7808
|
+
const entity = registry?.getEntityByStream ? await registry.getEntityByStream(streamPath) : null;
|
|
7809
|
+
if (entity) {
|
|
7810
|
+
if (await canAccessEntity(ctx, entity, `read`, request)) return void 0;
|
|
7811
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to read ${entity.url}`);
|
|
7812
|
+
}
|
|
7813
|
+
const attachmentEntityUrl = entityUrlFromAttachmentStreamPath(streamPath);
|
|
7814
|
+
if (attachmentEntityUrl) {
|
|
7815
|
+
const attachmentEntity = registry?.getEntity ? await registry.getEntity(attachmentEntityUrl) : null;
|
|
7816
|
+
if (!attachmentEntity) return apiError(404, ErrCodeNotFound, `Entity not found`);
|
|
7817
|
+
if (await canAccessEntity(ctx, attachmentEntity, `read`, request)) return void 0;
|
|
7818
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to read ${attachmentEntity.url}`);
|
|
7819
|
+
}
|
|
7820
|
+
}
|
|
7821
|
+
const sharedStateId = sharedStateIdFromPath(streamPath);
|
|
7822
|
+
if (!sharedStateId) return void 0;
|
|
7823
|
+
if (method === `GET` || method === `HEAD`) {
|
|
7824
|
+
if (await canAccessSharedState(ctx, sharedStateId, `read`, request)) return void 0;
|
|
7825
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to read shared state`);
|
|
7826
|
+
}
|
|
7827
|
+
if (method === `PUT` || method === `POST`) {
|
|
7828
|
+
const ownerEntityUrl = request.headers.get(SHARED_STATE_OWNER_ENTITY_HEADER)?.trim() || void 0;
|
|
7829
|
+
if (await canAccessSharedState(ctx, sharedStateId, `write`, request, ownerEntityUrl)) return void 0;
|
|
7830
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to write shared state`);
|
|
7831
|
+
}
|
|
7832
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to access shared state`);
|
|
7833
|
+
}
|
|
7834
|
+
function entityUrlFromAttachmentStreamPath(path$2) {
|
|
7835
|
+
const match = path$2.match(/^\/([^/]+)\/([^/]+)\/attachments\/[^/]+$/);
|
|
7836
|
+
if (!match) return null;
|
|
7837
|
+
return `/${match[1]}/${match[2]}`;
|
|
7838
|
+
}
|
|
7839
|
+
function sharedStateIdFromPath(path$2) {
|
|
7840
|
+
const match = path$2.match(/^\/_electric\/shared-state\/([^/]+)$/);
|
|
7841
|
+
if (!match) return null;
|
|
7842
|
+
try {
|
|
7843
|
+
return decodeURIComponent(match[1]);
|
|
7844
|
+
} catch {
|
|
7845
|
+
return match[1];
|
|
7846
|
+
}
|
|
7847
|
+
}
|
|
6815
7848
|
|
|
6816
7849
|
//#endregion
|
|
6817
7850
|
//#region src/routing/electric-proxy-router.ts
|
|
@@ -6819,12 +7852,15 @@ const electricProxyRouter = (0, itty_router.Router)({ base: `/_electric/electric
|
|
|
6819
7852
|
electricProxyRouter.get(`/*`, proxyElectric);
|
|
6820
7853
|
async function proxyElectric(request, ctx) {
|
|
6821
7854
|
if (!ctx.electricUrl) return apiError(500, `ELECTRIC_PROXY_FAILED`, `Electric URL not configured`);
|
|
7855
|
+
await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
|
|
6822
7856
|
const target = buildElectricProxyTarget({
|
|
6823
7857
|
incomingUrl: new URL(request.url),
|
|
6824
7858
|
electricUrl: ctx.electricUrl,
|
|
6825
7859
|
electricSecret: ctx.electricSecret,
|
|
6826
7860
|
tenantId: ctx.service,
|
|
6827
|
-
principalUrl: ctx.principal.url
|
|
7861
|
+
principalUrl: ctx.principal.url,
|
|
7862
|
+
principalKind: ctx.principal.kind,
|
|
7863
|
+
permissionBypass: isPermissionBypassPrincipal(ctx)
|
|
6828
7864
|
});
|
|
6829
7865
|
const headers = new Headers(request.headers);
|
|
6830
7866
|
headers.delete(`host`);
|
|
@@ -6844,6 +7880,28 @@ async function proxyElectric(request, ctx) {
|
|
|
6844
7880
|
});
|
|
6845
7881
|
}
|
|
6846
7882
|
|
|
7883
|
+
//#endregion
|
|
7884
|
+
//#region src/sandbox-choice-schema.ts
|
|
7885
|
+
/**
|
|
7886
|
+
* Wire schema for a spawn-time sandbox CHOICE (the request input), as opposed to
|
|
7887
|
+
* the resolved {@link import('./electric-agents-types.js').EntitySandboxSelection}
|
|
7888
|
+
* persisted on the entity. The matching `SandboxChoice` type is hand-maintained
|
|
7889
|
+
* in `electric-agents-types.ts` — mirrors how `dispatchPolicySchema` pairs with
|
|
7890
|
+
* the `DispatchPolicy` type in `dispatch-policy-schema.ts`.
|
|
7891
|
+
*
|
|
7892
|
+
* Validation happens once, at the router boundary (this schema is embedded in
|
|
7893
|
+
* the spawn body schema); the spawn resolver consumes already-validated input,
|
|
7894
|
+
* so there is intentionally no separate `parse` helper here.
|
|
7895
|
+
*/
|
|
7896
|
+
const sandboxChoiceSchema = __sinclair_typebox.Type.Object({
|
|
7897
|
+
profile: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
7898
|
+
key: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
7899
|
+
scope: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Union([__sinclair_typebox.Type.Literal(`entity`), __sinclair_typebox.Type.Literal(`wake`)])),
|
|
7900
|
+
persistent: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Boolean()),
|
|
7901
|
+
owner: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Boolean()),
|
|
7902
|
+
inherit: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Boolean())
|
|
7903
|
+
});
|
|
7904
|
+
|
|
6847
7905
|
//#endregion
|
|
6848
7906
|
//#region src/routing/entities-router.ts
|
|
6849
7907
|
const stringRecordSchema$1 = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.String());
|
|
@@ -6861,12 +7919,35 @@ const wakeConditionSchema = __sinclair_typebox.Type.Union([__sinclair_typebox.Ty
|
|
|
6861
7919
|
__sinclair_typebox.Type.Literal(`delete`)
|
|
6862
7920
|
])))
|
|
6863
7921
|
})]);
|
|
7922
|
+
const permissionSubjectSchema = __sinclair_typebox.Type.Object({
|
|
7923
|
+
subject_kind: __sinclair_typebox.Type.Union([__sinclair_typebox.Type.Literal(`principal`), __sinclair_typebox.Type.Literal(`principal_kind`)]),
|
|
7924
|
+
subject_value: __sinclair_typebox.Type.String()
|
|
7925
|
+
}, { additionalProperties: false });
|
|
7926
|
+
const entityPermissionSchema = __sinclair_typebox.Type.Union([
|
|
7927
|
+
__sinclair_typebox.Type.Literal(`read`),
|
|
7928
|
+
__sinclair_typebox.Type.Literal(`write`),
|
|
7929
|
+
__sinclair_typebox.Type.Literal(`delete`),
|
|
7930
|
+
__sinclair_typebox.Type.Literal(`signal`),
|
|
7931
|
+
__sinclair_typebox.Type.Literal(`fork`),
|
|
7932
|
+
__sinclair_typebox.Type.Literal(`schedule`),
|
|
7933
|
+
__sinclair_typebox.Type.Literal(`spawn`),
|
|
7934
|
+
__sinclair_typebox.Type.Literal(`manage`)
|
|
7935
|
+
]);
|
|
7936
|
+
const entityPermissionGrantInputSchema = __sinclair_typebox.Type.Object({
|
|
7937
|
+
...permissionSubjectSchema.properties,
|
|
7938
|
+
permission: entityPermissionSchema,
|
|
7939
|
+
propagation: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Union([__sinclair_typebox.Type.Literal(`self`), __sinclair_typebox.Type.Literal(`descendants`)])),
|
|
7940
|
+
copy_to_children: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Boolean()),
|
|
7941
|
+
expires_at: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
7942
|
+
}, { additionalProperties: false });
|
|
6864
7943
|
const spawnBodySchema = __sinclair_typebox.Type.Object({
|
|
6865
7944
|
args: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown())),
|
|
6866
7945
|
tags: __sinclair_typebox.Type.Optional(stringRecordSchema$1),
|
|
6867
7946
|
parent: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
6868
7947
|
dispatch_policy: __sinclair_typebox.Type.Optional(dispatchPolicySchema),
|
|
7948
|
+
sandbox: __sinclair_typebox.Type.Optional(sandboxChoiceSchema),
|
|
6869
7949
|
initialMessage: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Unknown()),
|
|
7950
|
+
grants: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(entityPermissionGrantInputSchema)),
|
|
6870
7951
|
wake: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Object({
|
|
6871
7952
|
subscriberUrl: __sinclair_typebox.Type.String(),
|
|
6872
7953
|
condition: wakeConditionSchema,
|
|
@@ -6888,8 +7969,22 @@ const sendBodySchema = __sinclair_typebox.Type.Object({
|
|
|
6888
7969
|
])),
|
|
6889
7970
|
position: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
6890
7971
|
afterMs: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
|
|
6891
|
-
from: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
7972
|
+
from: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
7973
|
+
from_principal: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
7974
|
+
from_agent: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
6892
7975
|
});
|
|
7976
|
+
function agentUrlForPrincipal(principal) {
|
|
7977
|
+
if (principal.kind === `agent`) return `/${principal.id}`;
|
|
7978
|
+
if (principal.key.startsWith(`entity:`)) return `/${principal.key.slice(`entity:`.length)}`;
|
|
7979
|
+
return null;
|
|
7980
|
+
}
|
|
7981
|
+
function agentUrlPath(value) {
|
|
7982
|
+
try {
|
|
7983
|
+
return new URL(value).pathname;
|
|
7984
|
+
} catch {
|
|
7985
|
+
return value;
|
|
7986
|
+
}
|
|
7987
|
+
}
|
|
6893
7988
|
const inboxMessageBodySchema = __sinclair_typebox.Type.Object({
|
|
6894
7989
|
payload: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Unknown()),
|
|
6895
7990
|
position: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
@@ -6907,7 +8002,11 @@ const inboxMessageBodySchema = __sinclair_typebox.Type.Object({
|
|
|
6907
8002
|
});
|
|
6908
8003
|
const forkBodySchema = __sinclair_typebox.Type.Object({
|
|
6909
8004
|
instance_id: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
6910
|
-
waitTimeoutMs: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number())
|
|
8005
|
+
waitTimeoutMs: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
|
|
8006
|
+
fork_pointer: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Object({
|
|
8007
|
+
offset: __sinclair_typebox.Type.Union([__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Null()]),
|
|
8008
|
+
sub_offset: __sinclair_typebox.Type.Number()
|
|
8009
|
+
}))
|
|
6911
8010
|
});
|
|
6912
8011
|
const setTagBodySchema = __sinclair_typebox.Type.Object({ value: __sinclair_typebox.Type.String() });
|
|
6913
8012
|
const entitySignalSchema = __sinclair_typebox.Type.Union([
|
|
@@ -6964,24 +8063,27 @@ const attachmentSubjectTypes = new Set([
|
|
|
6964
8063
|
]);
|
|
6965
8064
|
const entitiesRouter = (0, itty_router.Router)({ base: `/_electric/entities` });
|
|
6966
8065
|
entitiesRouter.get(`/`, listEntities);
|
|
6967
|
-
entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spawnBodySchema), spawnEntity);
|
|
6968
|
-
entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, getEntity);
|
|
6969
|
-
entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity);
|
|
6970
|
-
entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, killEntity);
|
|
6971
|
-
entitiesRouter.post(`/:type/:instanceId/signal`, withExistingEntity, withSchema(signalBodySchema), signalEntity);
|
|
6972
|
-
entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), sendEntity);
|
|
6973
|
-
entitiesRouter.post(`/:type/:instanceId/attachments`, withExistingEntity, createAttachment);
|
|
6974
|
-
entitiesRouter.get(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, readAttachment);
|
|
6975
|
-
entitiesRouter.delete(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, deleteAttachment);
|
|
6976
|
-
entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), updateInboxMessage);
|
|
6977
|
-
entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, deleteInboxMessage);
|
|
6978
|
-
entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), forkEntity);
|
|
6979
|
-
entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), setTag);
|
|
6980
|
-
entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, deleteTag);
|
|
6981
|
-
entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), upsertSchedule);
|
|
6982
|
-
entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, deleteSchedule);
|
|
6983
|
-
entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), upsertEventSourceSubscription);
|
|
6984
|
-
entitiesRouter.delete(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, deleteEventSourceSubscription);
|
|
8066
|
+
entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spawnBodySchema), withSpawnPermission, spawnEntity);
|
|
8067
|
+
entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`read`), getEntity);
|
|
8068
|
+
entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`read`), headEntity);
|
|
8069
|
+
entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`delete`), killEntity);
|
|
8070
|
+
entitiesRouter.post(`/:type/:instanceId/signal`, withExistingEntity, withSchema(signalBodySchema), withEntityPermission(`signal`), signalEntity);
|
|
8071
|
+
entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), withEntityPermission(`write`), sendEntity);
|
|
8072
|
+
entitiesRouter.post(`/:type/:instanceId/attachments`, withExistingEntity, withEntityPermission(`write`), createAttachment);
|
|
8073
|
+
entitiesRouter.get(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, withEntityPermission(`read`), readAttachment);
|
|
8074
|
+
entitiesRouter.delete(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, withEntityPermission(`write`), deleteAttachment);
|
|
8075
|
+
entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), withEntityPermission(`write`), updateInboxMessage);
|
|
8076
|
+
entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withEntityPermission(`write`), deleteInboxMessage);
|
|
8077
|
+
entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), withEntityPermission(`fork`), forkEntity);
|
|
8078
|
+
entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), withEntityPermission(`write`), setTag);
|
|
8079
|
+
entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withEntityPermission(`write`), deleteTag);
|
|
8080
|
+
entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), withEntityPermission(`schedule`), upsertSchedule);
|
|
8081
|
+
entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withEntityPermission(`schedule`), deleteSchedule);
|
|
8082
|
+
entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), withEntityPermission(`write`), upsertEventSourceSubscription);
|
|
8083
|
+
entitiesRouter.delete(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withEntityPermission(`write`), deleteEventSourceSubscription);
|
|
8084
|
+
entitiesRouter.get(`/:type/:instanceId/grants`, withExistingEntity, withEntityPermission(`manage`), listEntityPermissionGrants);
|
|
8085
|
+
entitiesRouter.post(`/:type/:instanceId/grants`, withExistingEntity, withSchema(entityPermissionGrantInputSchema), withEntityPermission(`manage`), createEntityPermissionGrant);
|
|
8086
|
+
entitiesRouter.delete(`/:type/:instanceId/grants/:grantId`, withExistingEntity, withEntityPermission(`manage`), deleteEntityPermissionGrant);
|
|
6985
8087
|
function entityUrlFromSegments(type, instanceId) {
|
|
6986
8088
|
if (!type || !instanceId) return null;
|
|
6987
8089
|
if (type.startsWith(`_`) || type.includes(`*`) || instanceId.includes(`*`)) return null;
|
|
@@ -7080,6 +8182,17 @@ function rejectPrincipalEntityMutation(request, action) {
|
|
|
7080
8182
|
if (entity.type !== `principal`) return void 0;
|
|
7081
8183
|
return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be ${action}`);
|
|
7082
8184
|
}
|
|
8185
|
+
function parseExpiresAt$1(value) {
|
|
8186
|
+
if (value === void 0) return void 0;
|
|
8187
|
+
const expiresAt = new Date(value);
|
|
8188
|
+
if (Number.isNaN(expiresAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid expires_at timestamp`, 400);
|
|
8189
|
+
return expiresAt;
|
|
8190
|
+
}
|
|
8191
|
+
function parseGrantId$1(request) {
|
|
8192
|
+
const grantId = Number.parseInt(String(request.params.grantId), 10);
|
|
8193
|
+
if (!Number.isSafeInteger(grantId) || grantId <= 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid grant id`, 400);
|
|
8194
|
+
return grantId;
|
|
8195
|
+
}
|
|
7083
8196
|
async function withExistingEntity(request, ctx) {
|
|
7084
8197
|
const entityUrl = entityUrlFromSegments(request.params.type, request.params.instanceId);
|
|
7085
8198
|
if (!entityUrl) return void 0;
|
|
@@ -7110,17 +8223,76 @@ async function withSpawnableEntityType(request, ctx) {
|
|
|
7110
8223
|
if (request.params.type === `principal`) return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be spawned directly`);
|
|
7111
8224
|
const entityType = await ctx.entityManager.registry.getEntityType(request.params.type);
|
|
7112
8225
|
if (!entityType) return apiError(404, ErrCodeUnknownEntityType, `Entity type "${request.params.type}" not found`);
|
|
8226
|
+
request.spawnRoute = { entityType };
|
|
7113
8227
|
return void 0;
|
|
7114
8228
|
}
|
|
8229
|
+
function withEntityPermission(permission) {
|
|
8230
|
+
return async (request, ctx) => {
|
|
8231
|
+
const { entity } = requireExistingEntityRoute(request);
|
|
8232
|
+
if (await canAccessEntity(ctx, entity, permission, request)) return void 0;
|
|
8233
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to ${permission} ${entity.url}`);
|
|
8234
|
+
};
|
|
8235
|
+
}
|
|
8236
|
+
async function withSpawnPermission(request, ctx) {
|
|
8237
|
+
const parsed = routeBody(request);
|
|
8238
|
+
const entityType = request.spawnRoute?.entityType;
|
|
8239
|
+
if (!entityType) throw new Error(`spawnable entity type middleware did not run`);
|
|
8240
|
+
if (!await canAccessEntityType(ctx, entityType, `spawn`, request)) return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn ${entityType.name}`);
|
|
8241
|
+
if (!parsed.parent) return void 0;
|
|
8242
|
+
const parent = await ctx.entityManager.registry.getEntity(parsed.parent);
|
|
8243
|
+
if (!parent) return apiError(404, ErrCodeNotFound, `Parent entity not found`);
|
|
8244
|
+
if (await canAccessEntity(ctx, parent, `spawn`, request)) return await validateParentedSpawnGrants(request, ctx, parent, parsed);
|
|
8245
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn children from ${parent.url}`);
|
|
8246
|
+
}
|
|
8247
|
+
async function validateParentedSpawnGrants(request, ctx, parent, parsed) {
|
|
8248
|
+
const needsParentManage = (parsed.grants ?? []).some(requiresParentManageForInitialGrant);
|
|
8249
|
+
if (!needsParentManage) return void 0;
|
|
8250
|
+
if (await canAccessEntity(ctx, parent, `manage`, request)) return void 0;
|
|
8251
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to delegate broad grants from ${parent.url}`);
|
|
8252
|
+
}
|
|
8253
|
+
function requiresParentManageForInitialGrant(grant) {
|
|
8254
|
+
return grant.permission === `manage` || grant.subject_kind === `principal_kind` || grant.propagation === `descendants` || grant.copy_to_children === true;
|
|
8255
|
+
}
|
|
7115
8256
|
async function listEntities({ query }, ctx) {
|
|
7116
8257
|
const { entities: entities$1 } = await ctx.entityManager.registry.listEntities({
|
|
7117
8258
|
type: firstQueryValue$1(query.type),
|
|
7118
8259
|
status: firstQueryValue$1(query.status),
|
|
7119
8260
|
parent: firstQueryValue$1(query.parent),
|
|
7120
|
-
created_by: firstQueryValue$1(query.created_by)
|
|
8261
|
+
created_by: firstQueryValue$1(query.created_by),
|
|
8262
|
+
readableBy: {
|
|
8263
|
+
...principalSubject(ctx.principal),
|
|
8264
|
+
bypass: isPermissionBypassPrincipal(ctx)
|
|
8265
|
+
}
|
|
7121
8266
|
});
|
|
7122
8267
|
return (0, itty_router.json)(entities$1.map((entity) => toPublicEntity(entity)));
|
|
7123
8268
|
}
|
|
8269
|
+
async function listEntityPermissionGrants(request, ctx) {
|
|
8270
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
8271
|
+
const grants = await ctx.entityManager.registry.listEntityPermissionGrants(entityUrl);
|
|
8272
|
+
return (0, itty_router.json)({ grants });
|
|
8273
|
+
}
|
|
8274
|
+
async function createEntityPermissionGrant(request, ctx) {
|
|
8275
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
8276
|
+
const parsed = routeBody(request);
|
|
8277
|
+
const grant = await ctx.entityManager.registry.createEntityPermissionGrant({
|
|
8278
|
+
entityUrl,
|
|
8279
|
+
permission: parsed.permission,
|
|
8280
|
+
subjectKind: parsed.subject_kind,
|
|
8281
|
+
subjectValue: parsed.subject_value,
|
|
8282
|
+
propagation: parsed.propagation,
|
|
8283
|
+
copyToChildren: parsed.copy_to_children,
|
|
8284
|
+
expiresAt: parseExpiresAt$1(parsed.expires_at),
|
|
8285
|
+
createdBy: ctx.principal.url
|
|
8286
|
+
});
|
|
8287
|
+
await ctx.entityBridgeManager.onEntityChanged(entityUrl);
|
|
8288
|
+
return (0, itty_router.json)(grant, { status: 201 });
|
|
8289
|
+
}
|
|
8290
|
+
async function deleteEntityPermissionGrant(request, ctx) {
|
|
8291
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
8292
|
+
const deleted = await ctx.entityManager.registry.deleteEntityPermissionGrant(entityUrl, parseGrantId$1(request));
|
|
8293
|
+
if (deleted) await ctx.entityBridgeManager.onEntityChanged(entityUrl);
|
|
8294
|
+
return deleted ? (0, itty_router.status)(204) : apiError(404, ErrCodeNotFound, `Grant not found`);
|
|
8295
|
+
}
|
|
7124
8296
|
async function upsertSchedule(request, ctx) {
|
|
7125
8297
|
const principalMutationError = rejectPrincipalEntityMutation(request, `scheduled`);
|
|
7126
8298
|
if (principalMutationError) return principalMutationError;
|
|
@@ -7225,7 +8397,12 @@ async function forkEntity(request, ctx) {
|
|
|
7225
8397
|
await assertDispatchPolicyAllowed(ctx, entity.dispatch_policy);
|
|
7226
8398
|
const result = await ctx.entityManager.forkSubtree(entityUrl, {
|
|
7227
8399
|
rootInstanceId: parsed.instance_id,
|
|
7228
|
-
waitTimeoutMs: parsed.waitTimeoutMs
|
|
8400
|
+
waitTimeoutMs: parsed.waitTimeoutMs,
|
|
8401
|
+
createdBy: ctx.principal.url,
|
|
8402
|
+
...parsed.fork_pointer && { forkPointer: {
|
|
8403
|
+
offset: parsed.fork_pointer.offset,
|
|
8404
|
+
subOffset: parsed.fork_pointer.sub_offset
|
|
8405
|
+
} }
|
|
7229
8406
|
});
|
|
7230
8407
|
for (const forkedEntity of result.entities) await linkEntityDispatchSubscription(ctx, forkedEntity);
|
|
7231
8408
|
return (0, itty_router.json)({
|
|
@@ -7237,26 +8414,27 @@ async function sendEntity(request, ctx) {
|
|
|
7237
8414
|
const parsed = routeBody(request);
|
|
7238
8415
|
const principal = ctx.principal;
|
|
7239
8416
|
if (parsed.from !== void 0 && parsed.from !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from must match Electric-Principal`);
|
|
8417
|
+
if (parsed.from_principal !== void 0 && parsed.from_principal !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from_principal must match Electric-Principal`);
|
|
8418
|
+
if (parsed.from_agent !== void 0) {
|
|
8419
|
+
const principalAgentUrl = agentUrlForPrincipal(principal);
|
|
8420
|
+
if (agentUrlPath(parsed.from_agent) !== principalAgentUrl) return apiError(400, ErrCodeInvalidRequest, `Request from_agent must match authenticated agent principal`);
|
|
8421
|
+
}
|
|
7240
8422
|
await ctx.entityManager.ensurePrincipal(principal);
|
|
7241
8423
|
const { entityUrl, entity } = requireExistingEntityRoute(request);
|
|
7242
8424
|
const dispatchEntity = entity.dispatch_policy ? entity : await backfillEntityDispatchPolicy(ctx, entity);
|
|
7243
8425
|
await linkEntityDispatchSubscription(ctx, dispatchEntity);
|
|
7244
|
-
|
|
7245
|
-
from: principal.url,
|
|
7246
|
-
payload: parsed.payload,
|
|
7247
|
-
key: parsed.key,
|
|
7248
|
-
type: parsed.type,
|
|
7249
|
-
mode: parsed.mode,
|
|
7250
|
-
position: parsed.position
|
|
7251
|
-
}, new Date(Date.now() + parsed.afterMs));
|
|
7252
|
-
else await ctx.entityManager.send(entityUrl, {
|
|
8426
|
+
const sendReq = {
|
|
7253
8427
|
from: principal.url,
|
|
8428
|
+
from_principal: principal.url,
|
|
8429
|
+
from_agent: parsed.from_agent,
|
|
7254
8430
|
payload: parsed.payload,
|
|
7255
8431
|
key: parsed.key,
|
|
7256
8432
|
type: parsed.type,
|
|
7257
8433
|
mode: parsed.mode,
|
|
7258
8434
|
position: parsed.position
|
|
7259
|
-
}
|
|
8435
|
+
};
|
|
8436
|
+
if (parsed.afterMs && parsed.afterMs > 0) await ctx.entityManager.enqueueDelayedSend(entityUrl, sendReq, new Date(Date.now() + parsed.afterMs));
|
|
8437
|
+
else await ctx.entityManager.send(entityUrl, sendReq);
|
|
7260
8438
|
return (0, itty_router.status)(204);
|
|
7261
8439
|
}
|
|
7262
8440
|
async function createAttachment(request, ctx) {
|
|
@@ -7323,10 +8501,22 @@ async function spawnEntity(request, ctx) {
|
|
|
7323
8501
|
tags: parsed.tags,
|
|
7324
8502
|
parent: parsed.parent,
|
|
7325
8503
|
dispatch_policy: dispatchPolicy,
|
|
8504
|
+
sandbox: parsed.sandbox,
|
|
7326
8505
|
initialMessage: void 0,
|
|
7327
8506
|
wake: parsed.wake,
|
|
7328
8507
|
created_by: principal.url
|
|
7329
8508
|
});
|
|
8509
|
+
if (parsed.parent) await ctx.entityManager.registry.copyEntityPermissionGrantsForSpawn(parsed.parent, entity.url, principal.url);
|
|
8510
|
+
for (const grant of parsed.grants ?? []) await ctx.entityManager.registry.createEntityPermissionGrant({
|
|
8511
|
+
entityUrl: entity.url,
|
|
8512
|
+
permission: grant.permission,
|
|
8513
|
+
subjectKind: grant.subject_kind,
|
|
8514
|
+
subjectValue: grant.subject_value,
|
|
8515
|
+
propagation: grant.propagation,
|
|
8516
|
+
copyToChildren: grant.copy_to_children,
|
|
8517
|
+
expiresAt: parseExpiresAt$1(grant.expires_at),
|
|
8518
|
+
createdBy: principal.url
|
|
8519
|
+
});
|
|
7330
8520
|
const linkBeforeInitialMessage = parsed.initialMessage !== void 0 && shouldLinkDispatchBeforeInitialMessage(dispatchPolicy);
|
|
7331
8521
|
if (linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
|
|
7332
8522
|
if (parsed.initialMessage !== void 0) await ctx.entityManager.send(entity.url, {
|
|
@@ -7378,6 +8568,12 @@ async function signalEntity(request, ctx) {
|
|
|
7378
8568
|
//#region src/routing/entity-types-router.ts
|
|
7379
8569
|
const jsonObjectSchema = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown());
|
|
7380
8570
|
const schemaMapSchema = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), jsonObjectSchema);
|
|
8571
|
+
const typePermissionGrantInputSchema = __sinclair_typebox.Type.Object({
|
|
8572
|
+
subject_kind: __sinclair_typebox.Type.Union([__sinclair_typebox.Type.Literal(`principal`), __sinclair_typebox.Type.Literal(`principal_kind`)]),
|
|
8573
|
+
subject_value: __sinclair_typebox.Type.String(),
|
|
8574
|
+
permission: __sinclair_typebox.Type.Union([__sinclair_typebox.Type.Literal(`spawn`), __sinclair_typebox.Type.Literal(`manage`)]),
|
|
8575
|
+
expires_at: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
8576
|
+
}, { additionalProperties: false });
|
|
7381
8577
|
const registerEntityTypeBodySchema = __sinclair_typebox.Type.Object({
|
|
7382
8578
|
name: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
7383
8579
|
description: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
@@ -7385,7 +8581,8 @@ const registerEntityTypeBodySchema = __sinclair_typebox.Type.Object({
|
|
|
7385
8581
|
inbox_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
|
|
7386
8582
|
state_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
|
|
7387
8583
|
serve_endpoint: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
7388
|
-
default_dispatch_policy: __sinclair_typebox.Type.Optional(dispatchPolicySchema)
|
|
8584
|
+
default_dispatch_policy: __sinclair_typebox.Type.Optional(dispatchPolicySchema),
|
|
8585
|
+
permission_grants: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(typePermissionGrantInputSchema))
|
|
7389
8586
|
}, { additionalProperties: false });
|
|
7390
8587
|
const amendEntityTypeSchemasBodySchema = __sinclair_typebox.Type.Object({
|
|
7391
8588
|
inbox_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
|
|
@@ -7393,20 +8590,56 @@ const amendEntityTypeSchemasBodySchema = __sinclair_typebox.Type.Object({
|
|
|
7393
8590
|
}, { additionalProperties: false });
|
|
7394
8591
|
const entityTypesRouter = (0, itty_router.Router)({ base: `/_electric/entity-types` });
|
|
7395
8592
|
entityTypesRouter.get(`/`, listEntityTypes);
|
|
7396
|
-
entityTypesRouter.post(`/`, withSchema(registerEntityTypeBodySchema), registerEntityType);
|
|
7397
|
-
entityTypesRouter.patch(`/:name/schemas`, withSchema(amendEntityTypeSchemasBodySchema), amendSchemas);
|
|
7398
|
-
entityTypesRouter.get(`/:name`, getEntityType);
|
|
7399
|
-
entityTypesRouter.delete(`/:name`, deleteEntityType);
|
|
8593
|
+
entityTypesRouter.post(`/`, withSchema(registerEntityTypeBodySchema), withEntityTypeRegistrationPermission, registerEntityType);
|
|
8594
|
+
entityTypesRouter.patch(`/:name/schemas`, withExistingEntityType, withEntityTypeManagePermission, withSchema(amendEntityTypeSchemasBodySchema), amendSchemas);
|
|
8595
|
+
entityTypesRouter.get(`/:name`, withExistingEntityType, withEntityTypeSpawnPermission, getEntityType);
|
|
8596
|
+
entityTypesRouter.delete(`/:name`, withExistingEntityType, withEntityTypeManagePermission, deleteEntityType);
|
|
8597
|
+
entityTypesRouter.get(`/:name/grants`, withExistingEntityType, withEntityTypeManagePermission, listTypePermissionGrants);
|
|
8598
|
+
entityTypesRouter.post(`/:name/grants`, withExistingEntityType, withSchema(typePermissionGrantInputSchema), withEntityTypeManagePermission, createTypePermissionGrant);
|
|
8599
|
+
entityTypesRouter.delete(`/:name/grants/:grantId`, withExistingEntityType, withEntityTypeManagePermission, deleteTypePermissionGrant);
|
|
7400
8600
|
async function registerEntityType(request, ctx) {
|
|
7401
8601
|
const parsed = routeBody(request);
|
|
7402
8602
|
const normalized = normalizeEntityTypeRequest(parsed);
|
|
7403
8603
|
if (normalized.serve_endpoint && !normalized.description && !normalized.creation_schema) return await discoverServeEndpoint(ctx, normalized);
|
|
7404
8604
|
const entityType = await ctx.entityManager.registerEntityType(normalized);
|
|
8605
|
+
await applyRegistrationPermissionGrants(ctx, entityType.name, normalized);
|
|
7405
8606
|
return (0, itty_router.json)(toPublicEntityType(entityType), { status: 201 });
|
|
7406
8607
|
}
|
|
7407
8608
|
async function listEntityTypes(_request, ctx) {
|
|
7408
8609
|
const entityTypes$1 = await ctx.entityManager.registry.listEntityTypes();
|
|
7409
|
-
|
|
8610
|
+
const visible = [];
|
|
8611
|
+
for (const entityType of entityTypes$1) if (await canAccessEntityType(ctx, entityType, `spawn`)) visible.push(entityType);
|
|
8612
|
+
return (0, itty_router.json)(visible.map((entityType) => toPublicEntityType(entityType)));
|
|
8613
|
+
}
|
|
8614
|
+
async function withExistingEntityType(request, ctx) {
|
|
8615
|
+
const entityType = await ctx.entityManager.registry.getEntityType(request.params.name);
|
|
8616
|
+
if (!entityType) return apiError(404, ErrCodeNotFound, `Entity type not found`);
|
|
8617
|
+
request.entityTypeRoute = { entityType };
|
|
8618
|
+
return void 0;
|
|
8619
|
+
}
|
|
8620
|
+
async function withEntityTypeManagePermission(request, ctx) {
|
|
8621
|
+
const entityType = request.entityTypeRoute?.entityType;
|
|
8622
|
+
if (!entityType) throw new Error(`entity type middleware did not run`);
|
|
8623
|
+
if (await canAccessEntityType(ctx, entityType, `manage`, request)) return void 0;
|
|
8624
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to manage ${entityType.name}`);
|
|
8625
|
+
}
|
|
8626
|
+
async function withEntityTypeSpawnPermission(request, ctx) {
|
|
8627
|
+
const entityType = request.entityTypeRoute?.entityType;
|
|
8628
|
+
if (!entityType) throw new Error(`entity type middleware did not run`);
|
|
8629
|
+
if (await canAccessEntityType(ctx, entityType, `spawn`, request)) return void 0;
|
|
8630
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn ${entityType.name}`);
|
|
8631
|
+
}
|
|
8632
|
+
async function withEntityTypeRegistrationPermission(request, ctx) {
|
|
8633
|
+
const parsed = normalizeEntityTypeRequest(routeBody(request));
|
|
8634
|
+
if (!parsed.name) return void 0;
|
|
8635
|
+
const existing = await ctx.entityManager.registry.getEntityType(parsed.name);
|
|
8636
|
+
if (existing) {
|
|
8637
|
+
request.entityTypeRoute = { entityType: existing };
|
|
8638
|
+
if (await canAccessEntityType(ctx, existing, `manage`, request)) return void 0;
|
|
8639
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to manage ${existing.name}`);
|
|
8640
|
+
}
|
|
8641
|
+
if (await canRegisterEntityType(ctx, parsed, request)) return void 0;
|
|
8642
|
+
return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to register entity types`);
|
|
7410
8643
|
}
|
|
7411
8644
|
async function discoverServeEndpoint(ctx, parsed) {
|
|
7412
8645
|
try {
|
|
@@ -7415,17 +8648,17 @@ async function discoverServeEndpoint(ctx, parsed) {
|
|
|
7415
8648
|
const manifest = await response.json();
|
|
7416
8649
|
if (manifest.name !== parsed.name) return apiError(400, ErrCodeServeEndpointNameMismatch, `Serve endpoint returned name "${manifest.name}" but expected "${parsed.name}"`);
|
|
7417
8650
|
manifest.serve_endpoint = parsed.serve_endpoint;
|
|
8651
|
+
manifest.permission_grants = parsed.permission_grants;
|
|
7418
8652
|
const entityType = await ctx.entityManager.registerEntityType(normalizeEntityTypeRequest(manifest));
|
|
8653
|
+
await applyRegistrationPermissionGrants(ctx, entityType.name, manifest);
|
|
7419
8654
|
return (0, itty_router.json)(toPublicEntityType(entityType), { status: 201 });
|
|
7420
8655
|
} catch (err) {
|
|
7421
8656
|
if (err instanceof ElectricAgentsError) throw err;
|
|
7422
8657
|
return apiError(502, ErrCodeServeEndpointUnreachable, `Failed to reach serve endpoint: ${err instanceof Error ? err.message : String(err)}`);
|
|
7423
8658
|
}
|
|
7424
8659
|
}
|
|
7425
|
-
async function getEntityType(request
|
|
7426
|
-
|
|
7427
|
-
if (!entityType) return apiError(404, ErrCodeNotFound, `Entity type not found`);
|
|
7428
|
-
return (0, itty_router.json)(toPublicEntityType(entityType));
|
|
8660
|
+
async function getEntityType(request) {
|
|
8661
|
+
return (0, itty_router.json)(toPublicEntityType(request.entityTypeRoute.entityType));
|
|
7429
8662
|
}
|
|
7430
8663
|
async function amendSchemas(request, ctx) {
|
|
7431
8664
|
const parsed = routeBody(request);
|
|
@@ -7439,6 +8672,47 @@ async function deleteEntityType(request, ctx) {
|
|
|
7439
8672
|
await ctx.entityManager.deleteEntityType(request.params.name);
|
|
7440
8673
|
return (0, itty_router.status)(204);
|
|
7441
8674
|
}
|
|
8675
|
+
async function listTypePermissionGrants(request, ctx) {
|
|
8676
|
+
const grants = await ctx.entityManager.registry.listEntityTypePermissionGrants(request.entityTypeRoute.entityType.name);
|
|
8677
|
+
return (0, itty_router.json)({ grants });
|
|
8678
|
+
}
|
|
8679
|
+
async function createTypePermissionGrant(request, ctx) {
|
|
8680
|
+
const parsed = routeBody(request);
|
|
8681
|
+
const grant = await ctx.entityManager.registry.createEntityTypePermissionGrant({
|
|
8682
|
+
entityType: request.entityTypeRoute.entityType.name,
|
|
8683
|
+
permission: parsed.permission,
|
|
8684
|
+
subjectKind: parsed.subject_kind,
|
|
8685
|
+
subjectValue: parsed.subject_value,
|
|
8686
|
+
expiresAt: parseExpiresAt(parsed.expires_at),
|
|
8687
|
+
createdBy: ctx.principal.url
|
|
8688
|
+
});
|
|
8689
|
+
return (0, itty_router.json)(grant, { status: 201 });
|
|
8690
|
+
}
|
|
8691
|
+
async function deleteTypePermissionGrant(request, ctx) {
|
|
8692
|
+
const deleted = await ctx.entityManager.registry.deleteEntityTypePermissionGrant(request.entityTypeRoute.entityType.name, parseGrantId(request));
|
|
8693
|
+
return deleted ? (0, itty_router.status)(204) : apiError(404, ErrCodeNotFound, `Grant not found`);
|
|
8694
|
+
}
|
|
8695
|
+
async function applyRegistrationPermissionGrants(ctx, entityType, request) {
|
|
8696
|
+
for (const grant of request.permission_grants ?? []) await ctx.entityManager.registry.ensureEntityTypePermissionGrant({
|
|
8697
|
+
entityType,
|
|
8698
|
+
permission: grant.permission,
|
|
8699
|
+
subjectKind: grant.subject_kind,
|
|
8700
|
+
subjectValue: grant.subject_value,
|
|
8701
|
+
expiresAt: parseExpiresAt(grant.expires_at),
|
|
8702
|
+
createdBy: ctx.principal.url
|
|
8703
|
+
});
|
|
8704
|
+
}
|
|
8705
|
+
function parseGrantId(request) {
|
|
8706
|
+
const grantId = Number.parseInt(String(request.params.grantId), 10);
|
|
8707
|
+
if (!Number.isSafeInteger(grantId) || grantId <= 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid grant id`, 400);
|
|
8708
|
+
return grantId;
|
|
8709
|
+
}
|
|
8710
|
+
function parseExpiresAt(value) {
|
|
8711
|
+
if (value === void 0) return void 0;
|
|
8712
|
+
const expiresAt = new Date(value);
|
|
8713
|
+
if (Number.isNaN(expiresAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid expires_at timestamp`, 400);
|
|
8714
|
+
return expiresAt;
|
|
8715
|
+
}
|
|
7442
8716
|
function normalizeEntityTypeRequest(parsed) {
|
|
7443
8717
|
const serveEndpoint = rewriteLoopbackWebhookUrl(parsed.serve_endpoint);
|
|
7444
8718
|
return {
|
|
@@ -7451,7 +8725,8 @@ function normalizeEntityTypeRequest(parsed) {
|
|
|
7451
8725
|
default_dispatch_policy: parsed.default_dispatch_policy ?? (serveEndpoint ? { targets: [{
|
|
7452
8726
|
type: `webhook`,
|
|
7453
8727
|
url: serveEndpoint
|
|
7454
|
-
}] } : void 0)
|
|
8728
|
+
}] } : void 0),
|
|
8729
|
+
permission_grants: parsed.permission_grants
|
|
7455
8730
|
};
|
|
7456
8731
|
}
|
|
7457
8732
|
function toPublicEntityType(entityType) {
|
|
@@ -7510,6 +8785,7 @@ function applyCors(response) {
|
|
|
7510
8785
|
`content-type`,
|
|
7511
8786
|
`authorization`,
|
|
7512
8787
|
`electric-claim-token`,
|
|
8788
|
+
`electric-owner-entity`,
|
|
7513
8789
|
ELECTRIC_PRINCIPAL_HEADER,
|
|
7514
8790
|
`ngrok-skip-browser-warning`
|
|
7515
8791
|
].join(`, `));
|
|
@@ -7560,7 +8836,7 @@ observationsRouter.post(`/entities/ensure-stream`, withSchema(ensureEntitiesMemb
|
|
|
7560
8836
|
observationsRouter.post(`/cron/ensure-stream`, withSchema(ensureCronStreamBodySchema), ensureCronStream);
|
|
7561
8837
|
async function ensureEntitiesMembershipStream(request, ctx) {
|
|
7562
8838
|
const parsed = routeBody(request);
|
|
7563
|
-
const result = await ctx.entityManager.ensureEntitiesMembershipStream(parsed.tags ?? {});
|
|
8839
|
+
const result = await ctx.entityManager.ensureEntitiesMembershipStream(parsed.tags ?? {}, ctx.principal);
|
|
7564
8840
|
return (0, itty_router.json)(result);
|
|
7565
8841
|
}
|
|
7566
8842
|
async function ensureCronStream(request, ctx) {
|
|
@@ -7577,6 +8853,12 @@ function withLeadingSlash(path$2) {
|
|
|
7577
8853
|
|
|
7578
8854
|
//#endregion
|
|
7579
8855
|
//#region src/routing/runners-router.ts
|
|
8856
|
+
const sandboxProfileBodySchema = __sinclair_typebox.Type.Object({
|
|
8857
|
+
name: __sinclair_typebox.Type.String(),
|
|
8858
|
+
label: __sinclair_typebox.Type.String(),
|
|
8859
|
+
description: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
8860
|
+
remote: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Boolean())
|
|
8861
|
+
});
|
|
7580
8862
|
const registerRunnerBodySchema = __sinclair_typebox.Type.Object({
|
|
7581
8863
|
id: __sinclair_typebox.Type.String(),
|
|
7582
8864
|
owner_principal: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
@@ -7589,7 +8871,8 @@ const registerRunnerBodySchema = __sinclair_typebox.Type.Object({
|
|
|
7589
8871
|
__sinclair_typebox.Type.Literal(`server`)
|
|
7590
8872
|
])),
|
|
7591
8873
|
admin_status: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Union([__sinclair_typebox.Type.Literal(`enabled`), __sinclair_typebox.Type.Literal(`disabled`)])),
|
|
7592
|
-
wake_stream: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
8874
|
+
wake_stream: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
8875
|
+
sandbox_profiles: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(sandboxProfileBodySchema))
|
|
7593
8876
|
});
|
|
7594
8877
|
const heartbeatBodySchema = __sinclair_typebox.Type.Object({
|
|
7595
8878
|
lease_ms: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
|
|
@@ -7687,7 +8970,8 @@ async function registerRunner(request, ctx) {
|
|
|
7687
8970
|
label: parsed.label,
|
|
7688
8971
|
kind: parsed.kind,
|
|
7689
8972
|
adminStatus: parsed.admin_status,
|
|
7690
|
-
wakeStream: parsed.wake_stream
|
|
8973
|
+
wakeStream: parsed.wake_stream,
|
|
8974
|
+
sandboxProfiles: parsed.sandbox_profiles
|
|
7691
8975
|
});
|
|
7692
8976
|
await ctx.streamClient.ensure(runner.wake_stream, { contentType: `application/json` });
|
|
7693
8977
|
return (0, itty_router.json)(runner, { status: 201 });
|
|
@@ -7917,6 +9201,7 @@ async function notificationFromClaim(ctx, input) {
|
|
|
7917
9201
|
streams: entity.streams,
|
|
7918
9202
|
tags: entity.tags,
|
|
7919
9203
|
spawnArgs: entity.spawn_args,
|
|
9204
|
+
sandbox: entity.sandbox,
|
|
7920
9205
|
createdBy: entity.created_by
|
|
7921
9206
|
},
|
|
7922
9207
|
principal: principalFromCreatedBy(entity.created_by)
|