@electric-ax/agents-server 0.4.15 → 0.4.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,
@@ -73,6 +78,7 @@ const entityTypes = (0, drizzle_orm_pg_core.pgTable)(`entity_types`, {
73
78
  creationSchema: (0, drizzle_orm_pg_core.jsonb)(`creation_schema`),
74
79
  inboxSchemas: (0, drizzle_orm_pg_core.jsonb)(`inbox_schemas`),
75
80
  stateSchemas: (0, drizzle_orm_pg_core.jsonb)(`state_schemas`),
81
+ slashCommands: (0, drizzle_orm_pg_core.jsonb)(`slash_commands`),
76
82
  serveEndpoint: (0, drizzle_orm_pg_core.text)(`serve_endpoint`),
77
83
  defaultDispatchPolicy: (0, drizzle_orm_pg_core.jsonb)(`default_dispatch_policy`),
78
84
  revision: (0, drizzle_orm_pg_core.integer)(`revision`).notNull().default(1),
@@ -107,6 +113,94 @@ const entities = (0, drizzle_orm_pg_core.pgTable)(`entities`, {
107
113
  (0, drizzle_orm_pg_core.index)(`entities_tags_index_gin`).using(`gin`, table.tagsIndex),
108
114
  (0, drizzle_orm_pg_core.check)(`chk_entities_status`, drizzle_orm.sql`${table.status} IN ('spawning', 'running', 'idle', 'paused', 'stopping', 'stopped', 'killed')`)
109
115
  ]);
116
+ const entityTypePermissionGrants = (0, drizzle_orm_pg_core.pgTable)(`entity_type_permission_grants`, {
117
+ id: (0, drizzle_orm_pg_core.bigserial)(`id`, { mode: `number` }).primaryKey(),
118
+ tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
119
+ entityType: (0, drizzle_orm_pg_core.text)(`entity_type`).notNull(),
120
+ permission: (0, drizzle_orm_pg_core.text)(`permission`).notNull(),
121
+ subjectKind: (0, drizzle_orm_pg_core.text)(`subject_kind`).notNull(),
122
+ subjectValue: (0, drizzle_orm_pg_core.text)(`subject_value`).notNull(),
123
+ createdBy: (0, drizzle_orm_pg_core.text)(`created_by`),
124
+ expiresAt: (0, drizzle_orm_pg_core.timestamp)(`expires_at`, { withTimezone: true }),
125
+ createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow(),
126
+ updatedAt: (0, drizzle_orm_pg_core.timestamp)(`updated_at`, { withTimezone: true }).notNull().defaultNow()
127
+ }, (table) => [
128
+ (0, drizzle_orm_pg_core.index)(`idx_type_permission_grants_lookup`).on(table.tenantId, table.entityType, table.permission, table.subjectKind, table.subjectValue),
129
+ (0, drizzle_orm_pg_core.index)(`idx_type_permission_grants_expiry`).on(table.tenantId, table.expiresAt),
130
+ (0, drizzle_orm_pg_core.check)(`chk_type_permission_grants_permission`, drizzle_orm.sql`${table.permission} IN ('spawn', 'manage')`),
131
+ (0, drizzle_orm_pg_core.check)(`chk_type_permission_grants_subject_kind`, drizzle_orm.sql`${table.subjectKind} IN ('principal', 'principal_kind')`)
132
+ ]);
133
+ const entityLineage = (0, drizzle_orm_pg_core.pgTable)(`entity_lineage`, {
134
+ tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
135
+ ancestorUrl: (0, drizzle_orm_pg_core.text)(`ancestor_url`).notNull(),
136
+ descendantUrl: (0, drizzle_orm_pg_core.text)(`descendant_url`).notNull(),
137
+ depth: (0, drizzle_orm_pg_core.integer)(`depth`).notNull(),
138
+ createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow()
139
+ }, (table) => [
140
+ (0, drizzle_orm_pg_core.primaryKey)({ columns: [
141
+ table.tenantId,
142
+ table.ancestorUrl,
143
+ table.descendantUrl
144
+ ] }),
145
+ (0, drizzle_orm_pg_core.index)(`idx_entity_lineage_descendant`).on(table.tenantId, table.descendantUrl),
146
+ (0, drizzle_orm_pg_core.check)(`chk_entity_lineage_depth`, drizzle_orm.sql`${table.depth} >= 0`)
147
+ ]);
148
+ const entityPermissionGrants = (0, drizzle_orm_pg_core.pgTable)(`entity_permission_grants`, {
149
+ id: (0, drizzle_orm_pg_core.bigserial)(`id`, { mode: `number` }).primaryKey(),
150
+ tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
151
+ entityUrl: (0, drizzle_orm_pg_core.text)(`entity_url`).notNull(),
152
+ permission: (0, drizzle_orm_pg_core.text)(`permission`).notNull(),
153
+ subjectKind: (0, drizzle_orm_pg_core.text)(`subject_kind`).notNull(),
154
+ subjectValue: (0, drizzle_orm_pg_core.text)(`subject_value`).notNull(),
155
+ propagation: (0, drizzle_orm_pg_core.text)(`propagation`).notNull().default(`self`),
156
+ copyToChildren: (0, drizzle_orm_pg_core.boolean)(`copy_to_children`).notNull().default(false),
157
+ createdBy: (0, drizzle_orm_pg_core.text)(`created_by`),
158
+ expiresAt: (0, drizzle_orm_pg_core.timestamp)(`expires_at`, { withTimezone: true }),
159
+ createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow(),
160
+ updatedAt: (0, drizzle_orm_pg_core.timestamp)(`updated_at`, { withTimezone: true }).notNull().defaultNow()
161
+ }, (table) => [
162
+ (0, drizzle_orm_pg_core.index)(`idx_entity_permission_grants_entity`).on(table.tenantId, table.entityUrl),
163
+ (0, drizzle_orm_pg_core.index)(`idx_entity_permission_grants_subject`).on(table.tenantId, table.permission, table.subjectKind, table.subjectValue),
164
+ (0, drizzle_orm_pg_core.index)(`idx_entity_permission_grants_expiry`).on(table.tenantId, table.expiresAt),
165
+ (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')`),
166
+ (0, drizzle_orm_pg_core.check)(`chk_entity_permission_grants_subject_kind`, drizzle_orm.sql`${table.subjectKind} IN ('principal', 'principal_kind')`),
167
+ (0, drizzle_orm_pg_core.check)(`chk_entity_permission_grants_propagation`, drizzle_orm.sql`${table.propagation} IN ('self', 'descendants')`)
168
+ ]);
169
+ const entityEffectivePermissions = (0, drizzle_orm_pg_core.pgTable)(`entity_effective_permissions`, {
170
+ id: (0, drizzle_orm_pg_core.bigserial)(`id`, { mode: `number` }).primaryKey(),
171
+ tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
172
+ entityUrl: (0, drizzle_orm_pg_core.text)(`entity_url`).notNull(),
173
+ sourceEntityUrl: (0, drizzle_orm_pg_core.text)(`source_entity_url`).notNull(),
174
+ sourceGrantId: (0, drizzle_orm_pg_core.bigint)(`source_grant_id`, { mode: `number` }).notNull(),
175
+ permission: (0, drizzle_orm_pg_core.text)(`permission`).notNull(),
176
+ subjectKind: (0, drizzle_orm_pg_core.text)(`subject_kind`).notNull(),
177
+ subjectValue: (0, drizzle_orm_pg_core.text)(`subject_value`).notNull(),
178
+ expiresAt: (0, drizzle_orm_pg_core.timestamp)(`expires_at`, { withTimezone: true }),
179
+ createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow()
180
+ }, (table) => [
181
+ (0, drizzle_orm_pg_core.unique)(`uq_entity_effective_permission`).on(table.tenantId, table.entityUrl, table.sourceGrantId),
182
+ (0, drizzle_orm_pg_core.index)(`idx_entity_effective_permissions_lookup`).on(table.tenantId, table.permission, table.subjectKind, table.subjectValue, table.entityUrl),
183
+ (0, drizzle_orm_pg_core.index)(`idx_entity_effective_permissions_entity`).on(table.tenantId, table.entityUrl),
184
+ (0, drizzle_orm_pg_core.index)(`idx_entity_effective_permissions_expiry`).on(table.tenantId, table.expiresAt),
185
+ (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')`),
186
+ (0, drizzle_orm_pg_core.check)(`chk_entity_effective_permissions_subject_kind`, drizzle_orm.sql`${table.subjectKind} IN ('principal', 'principal_kind')`)
187
+ ]);
188
+ const sharedStateLinks = (0, drizzle_orm_pg_core.pgTable)(`shared_state_links`, {
189
+ tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
190
+ sharedStateId: (0, drizzle_orm_pg_core.text)(`shared_state_id`).notNull(),
191
+ ownerEntityUrl: (0, drizzle_orm_pg_core.text)(`owner_entity_url`).notNull(),
192
+ manifestKey: (0, drizzle_orm_pg_core.text)(`manifest_key`).notNull(),
193
+ createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow(),
194
+ updatedAt: (0, drizzle_orm_pg_core.timestamp)(`updated_at`, { withTimezone: true }).notNull().defaultNow()
195
+ }, (table) => [
196
+ (0, drizzle_orm_pg_core.primaryKey)({ columns: [
197
+ table.tenantId,
198
+ table.ownerEntityUrl,
199
+ table.manifestKey
200
+ ] }),
201
+ (0, drizzle_orm_pg_core.index)(`idx_shared_state_links_shared_state`).on(table.tenantId, table.sharedStateId),
202
+ (0, drizzle_orm_pg_core.index)(`idx_shared_state_links_owner`).on(table.tenantId, table.ownerEntityUrl)
203
+ ]);
110
204
  const users = (0, drizzle_orm_pg_core.pgTable)(`users`, {
111
205
  tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
112
206
  id: (0, drizzle_orm_pg_core.text)(`id`).notNull(),
@@ -293,12 +387,18 @@ const entityBridges = (0, drizzle_orm_pg_core.pgTable)(`entity_bridges`, {
293
387
  sourceRef: (0, drizzle_orm_pg_core.text)(`source_ref`).notNull(),
294
388
  tags: (0, drizzle_orm_pg_core.jsonb)(`tags`).notNull(),
295
389
  streamUrl: (0, drizzle_orm_pg_core.text)(`stream_url`).notNull(),
390
+ principalUrl: (0, drizzle_orm_pg_core.text)(`principal_url`),
391
+ principalKind: (0, drizzle_orm_pg_core.text)(`principal_kind`),
296
392
  shapeHandle: (0, drizzle_orm_pg_core.text)(`shape_handle`),
297
393
  shapeOffset: (0, drizzle_orm_pg_core.text)(`shape_offset`),
298
394
  lastObserverActivityAt: (0, drizzle_orm_pg_core.timestamp)(`last_observer_activity_at`, { withTimezone: true }).notNull().defaultNow(),
299
395
  createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow(),
300
396
  updatedAt: (0, drizzle_orm_pg_core.timestamp)(`updated_at`, { withTimezone: true }).notNull().defaultNow()
301
- }, (table) => [(0, drizzle_orm_pg_core.primaryKey)({ columns: [table.tenantId, table.sourceRef] }), (0, drizzle_orm_pg_core.unique)(`uq_entity_bridges_stream_url`).on(table.tenantId, table.streamUrl)]);
397
+ }, (table) => [
398
+ (0, drizzle_orm_pg_core.primaryKey)({ columns: [table.tenantId, table.sourceRef] }),
399
+ (0, drizzle_orm_pg_core.unique)(`uq_entity_bridges_stream_url`).on(table.tenantId, table.streamUrl),
400
+ (0, drizzle_orm_pg_core.index)(`idx_entity_bridges_principal`).on(table.tenantId, table.principalKind, table.principalUrl)
401
+ ]);
302
402
  const entityManifestSources = (0, drizzle_orm_pg_core.pgTable)(`entity_manifest_sources`, {
303
403
  tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
304
404
  ownerEntityUrl: (0, drizzle_orm_pg_core.text)(`owner_entity_url`).notNull(),
@@ -486,16 +586,26 @@ function isDuplicateUrlError(err) {
486
586
  return e.code === `23505`;
487
587
  }
488
588
  const DEFAULT_RUNNER_LEASE_MS = 3e4;
589
+ const PERMISSION_PRUNE_INTERVAL_MS = 3e4;
489
590
  function runnerWakeStream(runnerId) {
490
591
  return `/runners/${runnerId}/wake`;
491
592
  }
492
593
  var PostgresRegistry = class {
594
+ lastPermissionPruneStartedAt = 0;
595
+ permissionPrunePromise = null;
493
596
  constructor(db, tenantId = DEFAULT_TENANT_ID) {
494
597
  this.db = db;
495
598
  this.tenantId = tenantId;
496
599
  }
497
600
  async initialize() {}
498
601
  close() {}
602
+ async ensureUserForPrincipal(principal) {
603
+ if (principal.kind !== `user`) return;
604
+ await this.db.insert(users).values({
605
+ tenantId: this.tenantId,
606
+ id: principal.id
607
+ }).onConflictDoNothing();
608
+ }
499
609
  async createRunner(input) {
500
610
  const now = new Date();
501
611
  const wakeStream = input.wakeStream ?? runnerWakeStream(input.id);
@@ -740,6 +850,7 @@ var PostgresRegistry = class {
740
850
  creationSchema: et.creation_schema ?? null,
741
851
  inboxSchemas: et.inbox_schemas ?? null,
742
852
  stateSchemas: et.state_schemas ?? null,
853
+ slashCommands: et.slash_commands ?? null,
743
854
  serveEndpoint: et.serve_endpoint ?? null,
744
855
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
745
856
  revision: et.revision,
@@ -752,6 +863,7 @@ var PostgresRegistry = class {
752
863
  creationSchema: et.creation_schema ?? null,
753
864
  inboxSchemas: et.inbox_schemas ?? null,
754
865
  stateSchemas: et.state_schemas ?? null,
866
+ slashCommands: et.slash_commands ?? null,
755
867
  serveEndpoint: et.serve_endpoint ?? null,
756
868
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
757
869
  revision: et.revision,
@@ -769,6 +881,7 @@ var PostgresRegistry = class {
769
881
  creationSchema: et.creation_schema ?? null,
770
882
  inboxSchemas: et.inbox_schemas ?? null,
771
883
  stateSchemas: et.state_schemas ?? null,
884
+ slashCommands: et.slash_commands ?? null,
772
885
  serveEndpoint: et.serve_endpoint ?? null,
773
886
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
774
887
  revision: et.revision,
@@ -795,6 +908,7 @@ var PostgresRegistry = class {
795
908
  creationSchema: et.creation_schema ?? null,
796
909
  inboxSchemas: et.inbox_schemas ?? null,
797
910
  stateSchemas: et.state_schemas ?? null,
911
+ slashCommands: et.slash_commands ?? null,
798
912
  serveEndpoint: et.serve_endpoint ?? null,
799
913
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
800
914
  revision: et.revision,
@@ -830,6 +944,59 @@ var PostgresRegistry = class {
830
944
  pendingSourceStreams: [],
831
945
  updatedAt: new Date()
832
946
  }).onConflictDoNothing();
947
+ await tx.insert(entityLineage).values({
948
+ tenantId: this.tenantId,
949
+ ancestorUrl: entity.url,
950
+ descendantUrl: entity.url,
951
+ depth: 0
952
+ }).onConflictDoNothing();
953
+ if (entity.parent) await tx.execute(drizzle_orm.sql`
954
+ INSERT INTO ${entityLineage} (
955
+ tenant_id,
956
+ ancestor_url,
957
+ descendant_url,
958
+ depth
959
+ )
960
+ SELECT
961
+ ${this.tenantId},
962
+ ancestor_url,
963
+ ${entity.url},
964
+ depth + 1
965
+ FROM ${entityLineage}
966
+ WHERE tenant_id = ${this.tenantId}
967
+ AND descendant_url = ${entity.parent}
968
+ ON CONFLICT DO NOTHING
969
+ `);
970
+ await tx.execute(drizzle_orm.sql`
971
+ INSERT INTO ${entityEffectivePermissions} (
972
+ tenant_id,
973
+ entity_url,
974
+ source_entity_url,
975
+ source_grant_id,
976
+ permission,
977
+ subject_kind,
978
+ subject_value,
979
+ expires_at
980
+ )
981
+ SELECT
982
+ ${this.tenantId},
983
+ ${entity.url},
984
+ grants.entity_url,
985
+ grants.id,
986
+ grants.permission,
987
+ grants.subject_kind,
988
+ grants.subject_value,
989
+ grants.expires_at
990
+ FROM ${entityPermissionGrants} grants
991
+ JOIN ${entityLineage} lineage
992
+ ON lineage.tenant_id = grants.tenant_id
993
+ AND lineage.ancestor_url = grants.entity_url
994
+ AND lineage.descendant_url = ${entity.url}
995
+ WHERE grants.tenant_id = ${this.tenantId}
996
+ AND grants.propagation = 'descendants'
997
+ AND (grants.expires_at IS NULL OR grants.expires_at > now())
998
+ ON CONFLICT DO NOTHING
999
+ `);
833
1000
  return parseInt(result[0].txid);
834
1001
  });
835
1002
  } catch (err) {
@@ -851,10 +1018,8 @@ var PostgresRegistry = class {
851
1018
  }
852
1019
  async getEntityByStream(streamPath) {
853
1020
  const mainSuffix = `/main`;
854
- const errorSuffix = `/error`;
855
1021
  let entityUrl = null;
856
1022
  if (streamPath.endsWith(mainSuffix)) entityUrl = streamPath.slice(0, -mainSuffix.length);
857
- else if (streamPath.endsWith(errorSuffix)) entityUrl = streamPath.slice(0, -errorSuffix.length);
858
1023
  if (!entityUrl) return null;
859
1024
  return this.getEntity(entityUrl);
860
1025
  }
@@ -864,6 +1029,23 @@ var PostgresRegistry = class {
864
1029
  if (filter?.status) conditions.push((0, drizzle_orm.eq)(entities.status, filter.status));
865
1030
  if (filter?.parent) conditions.push((0, drizzle_orm.eq)(entities.parent, filter.parent));
866
1031
  if (filter?.created_by) conditions.push((0, drizzle_orm.eq)(entities.createdBy, filter.created_by));
1032
+ if (filter?.readableBy && !filter.readableBy.bypass) conditions.push(drizzle_orm.sql`(
1033
+ ${entities.createdBy} = ${filter.readableBy.principalUrl}
1034
+ OR ${entities.url} IN (
1035
+ SELECT ${entityEffectivePermissions.entityUrl}
1036
+ FROM ${entityEffectivePermissions}
1037
+ WHERE ${entityEffectivePermissions.tenantId} = ${this.tenantId}
1038
+ AND ${entityEffectivePermissions.permission} IN ('read', 'manage')
1039
+ AND (${entityEffectivePermissions.expiresAt} IS NULL OR ${entityEffectivePermissions.expiresAt} > now())
1040
+ AND (
1041
+ (${entityEffectivePermissions.subjectKind} = 'principal'
1042
+ AND ${entityEffectivePermissions.subjectValue} = ${filter.readableBy.principalUrl})
1043
+ OR
1044
+ (${entityEffectivePermissions.subjectKind} = 'principal_kind'
1045
+ AND ${entityEffectivePermissions.subjectValue} = ${filter.readableBy.principalKind})
1046
+ )
1047
+ )
1048
+ )`);
867
1049
  const whereClause = (0, drizzle_orm.and)(...conditions);
868
1050
  const countResult = await this.db.select({ count: drizzle_orm.sql`count(*)` }).from(entities).where(whereClause);
869
1051
  const total = Number(countResult[0].count);
@@ -876,6 +1058,189 @@ var PostgresRegistry = class {
876
1058
  total
877
1059
  };
878
1060
  }
1061
+ async createEntityTypePermissionGrant(input) {
1062
+ const [row] = await this.db.insert(entityTypePermissionGrants).values({
1063
+ tenantId: this.tenantId,
1064
+ entityType: input.entityType,
1065
+ permission: input.permission,
1066
+ subjectKind: input.subjectKind,
1067
+ subjectValue: input.subjectValue,
1068
+ createdBy: input.createdBy ?? null,
1069
+ expiresAt: input.expiresAt ?? null
1070
+ }).returning();
1071
+ return this.rowToEntityTypePermissionGrant(row);
1072
+ }
1073
+ async ensureEntityTypePermissionGrant(input) {
1074
+ 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);
1075
+ if (existing) return this.rowToEntityTypePermissionGrant(existing);
1076
+ return await this.createEntityTypePermissionGrant(input);
1077
+ }
1078
+ async listEntityTypePermissionGrants(entityType) {
1079
+ 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);
1080
+ return rows.map((row) => this.rowToEntityTypePermissionGrant(row));
1081
+ }
1082
+ async deleteEntityTypePermissionGrant(entityType, grantId) {
1083
+ 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 });
1084
+ return rows.length > 0;
1085
+ }
1086
+ async hasEntityTypePermission(entityType, permission, subject) {
1087
+ const permissions = [permission, `manage`];
1088
+ 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`(
1089
+ (${entityTypePermissionGrants.subjectKind} = 'principal'
1090
+ AND ${entityTypePermissionGrants.subjectValue} = ${subject.principalUrl})
1091
+ OR
1092
+ (${entityTypePermissionGrants.subjectKind} = 'principal_kind'
1093
+ AND ${entityTypePermissionGrants.subjectValue} = ${subject.principalKind})
1094
+ )`)).limit(1);
1095
+ return rows.length > 0;
1096
+ }
1097
+ async createEntityPermissionGrant(input) {
1098
+ return await this.db.transaction(async (tx) => {
1099
+ const [row] = await tx.insert(entityPermissionGrants).values({
1100
+ tenantId: this.tenantId,
1101
+ entityUrl: input.entityUrl,
1102
+ permission: input.permission,
1103
+ subjectKind: input.subjectKind,
1104
+ subjectValue: input.subjectValue,
1105
+ propagation: input.propagation ?? `self`,
1106
+ copyToChildren: input.copyToChildren ?? false,
1107
+ createdBy: input.createdBy ?? null,
1108
+ expiresAt: input.expiresAt ?? null
1109
+ }).returning();
1110
+ await this.materializeEntityPermissionGrant(tx, row);
1111
+ return this.rowToEntityPermissionGrant(row);
1112
+ });
1113
+ }
1114
+ async listEntityPermissionGrants(entityUrl) {
1115
+ 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);
1116
+ return rows.map((row) => this.rowToEntityPermissionGrant(row));
1117
+ }
1118
+ async deleteEntityPermissionGrant(entityUrl, grantId) {
1119
+ return await this.db.transaction(async (tx) => {
1120
+ await tx.delete(entityEffectivePermissions).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityEffectivePermissions.tenantId, this.tenantId), (0, drizzle_orm.eq)(entityEffectivePermissions.sourceGrantId, grantId)));
1121
+ 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 });
1122
+ return rows.length > 0;
1123
+ });
1124
+ }
1125
+ async copyEntityPermissionGrantsForSpawn(parentEntityUrl, childEntityUrl, createdBy) {
1126
+ 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())`));
1127
+ const copied = [];
1128
+ for (const grant of parentGrants) copied.push(await this.createEntityPermissionGrant({
1129
+ entityUrl: childEntityUrl,
1130
+ permission: grant.permission,
1131
+ subjectKind: grant.subjectKind,
1132
+ subjectValue: grant.subjectValue,
1133
+ propagation: `self`,
1134
+ copyToChildren: grant.copyToChildren,
1135
+ createdBy,
1136
+ expiresAt: grant.expiresAt ?? void 0
1137
+ }));
1138
+ return copied;
1139
+ }
1140
+ async hasEntityPermission(entityUrl, permission, subject) {
1141
+ const permissions = [permission, `manage`];
1142
+ 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`(
1143
+ (${entityEffectivePermissions.subjectKind} = 'principal'
1144
+ AND ${entityEffectivePermissions.subjectValue} = ${subject.principalUrl})
1145
+ OR
1146
+ (${entityEffectivePermissions.subjectKind} = 'principal_kind'
1147
+ AND ${entityEffectivePermissions.subjectValue} = ${subject.principalKind})
1148
+ )`)).limit(1);
1149
+ return rows.length > 0;
1150
+ }
1151
+ async replaceSharedStateLink(ownerEntityUrl, manifestKey, sharedStateId) {
1152
+ 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)));
1153
+ if (!sharedStateId) return;
1154
+ await this.db.insert(sharedStateLinks).values({
1155
+ tenantId: this.tenantId,
1156
+ ownerEntityUrl,
1157
+ manifestKey,
1158
+ sharedStateId
1159
+ }).onConflictDoUpdate({
1160
+ target: [
1161
+ sharedStateLinks.tenantId,
1162
+ sharedStateLinks.ownerEntityUrl,
1163
+ sharedStateLinks.manifestKey
1164
+ ],
1165
+ set: {
1166
+ sharedStateId,
1167
+ updatedAt: new Date()
1168
+ }
1169
+ });
1170
+ }
1171
+ async listSharedStateLinkedEntityUrls(sharedStateId) {
1172
+ 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)));
1173
+ return rows.map((row) => row.ownerEntityUrl);
1174
+ }
1175
+ async pruneExpiredPermissionGrants(now = new Date(), options = {}) {
1176
+ if (this.permissionPrunePromise) return await this.permissionPrunePromise;
1177
+ const startedAt = Date.now();
1178
+ if (!options.force && startedAt - this.lastPermissionPruneStartedAt < PERMISSION_PRUNE_INTERVAL_MS) return;
1179
+ this.lastPermissionPruneStartedAt = startedAt;
1180
+ const promise = this.pruneExpiredPermissionGrantsNow(now);
1181
+ this.permissionPrunePromise = promise;
1182
+ try {
1183
+ await promise;
1184
+ } catch (error) {
1185
+ this.lastPermissionPruneStartedAt = 0;
1186
+ throw error;
1187
+ } finally {
1188
+ if (this.permissionPrunePromise === promise) this.permissionPrunePromise = null;
1189
+ }
1190
+ }
1191
+ async pruneExpiredPermissionGrantsNow(now) {
1192
+ await this.db.transaction(async (tx) => {
1193
+ 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)));
1194
+ const ids = expiredEntityGrantIds.map((row) => row.id);
1195
+ if (ids.length > 0) {
1196
+ await tx.delete(entityEffectivePermissions).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityEffectivePermissions.tenantId, this.tenantId), (0, drizzle_orm.inArray)(entityEffectivePermissions.sourceGrantId, ids)));
1197
+ await tx.delete(entityPermissionGrants).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(entityPermissionGrants.tenantId, this.tenantId), (0, drizzle_orm.inArray)(entityPermissionGrants.id, ids)));
1198
+ }
1199
+ 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)));
1200
+ 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)));
1201
+ });
1202
+ }
1203
+ async materializeEntityPermissionGrant(tx, grant) {
1204
+ 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)));
1205
+ if (grant.propagation === `descendants`) {
1206
+ await tx.execute(drizzle_orm.sql`
1207
+ INSERT INTO ${entityEffectivePermissions} (
1208
+ tenant_id,
1209
+ entity_url,
1210
+ source_entity_url,
1211
+ source_grant_id,
1212
+ permission,
1213
+ subject_kind,
1214
+ subject_value,
1215
+ expires_at
1216
+ )
1217
+ SELECT
1218
+ ${this.tenantId},
1219
+ descendant_url,
1220
+ ${grant.entityUrl},
1221
+ ${grant.id},
1222
+ ${grant.permission},
1223
+ ${grant.subjectKind},
1224
+ ${grant.subjectValue},
1225
+ ${grant.expiresAt}
1226
+ FROM ${entityLineage}
1227
+ WHERE tenant_id = ${this.tenantId}
1228
+ AND ancestor_url = ${grant.entityUrl}
1229
+ ON CONFLICT DO NOTHING
1230
+ `);
1231
+ return;
1232
+ }
1233
+ await tx.insert(entityEffectivePermissions).values({
1234
+ tenantId: this.tenantId,
1235
+ entityUrl: grant.entityUrl,
1236
+ sourceEntityUrl: grant.entityUrl,
1237
+ sourceGrantId: grant.id,
1238
+ permission: grant.permission,
1239
+ subjectKind: grant.subjectKind,
1240
+ subjectValue: grant.subjectValue,
1241
+ expiresAt: grant.expiresAt
1242
+ }).onConflictDoNothing();
1243
+ }
879
1244
  async updateStatus(entityUrl, status$4) {
880
1245
  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`));
881
1246
  await this.db.update(entities).set({
@@ -977,7 +1342,9 @@ var PostgresRegistry = class {
977
1342
  tenantId: this.tenantId,
978
1343
  sourceRef: row.sourceRef,
979
1344
  tags: (0, __electric_ax_agents_runtime.normalizeTags)(row.tags),
980
- streamUrl: row.streamUrl
1345
+ streamUrl: row.streamUrl,
1346
+ principalUrl: row.principalUrl,
1347
+ principalKind: row.principalKind
981
1348
  }).onConflictDoNothing();
982
1349
  const existing = await this.getEntityBridge(row.sourceRef);
983
1350
  if (!existing) throw new Error(`Failed to load entity bridge ${row.sourceRef}`);
@@ -1132,6 +1499,7 @@ var PostgresRegistry = class {
1132
1499
  creation_schema: row.creationSchema,
1133
1500
  inbox_schemas: row.inboxSchemas,
1134
1501
  state_schemas: row.stateSchemas,
1502
+ slash_commands: row.slashCommands ?? void 0,
1135
1503
  serve_endpoint: row.serveEndpoint ?? void 0,
1136
1504
  default_dispatch_policy: row.defaultDispatchPolicy ?? void 0,
1137
1505
  revision: row.revision,
@@ -1139,15 +1507,40 @@ var PostgresRegistry = class {
1139
1507
  updated_at: row.updatedAt
1140
1508
  };
1141
1509
  }
1510
+ rowToEntityTypePermissionGrant(row) {
1511
+ return {
1512
+ id: row.id,
1513
+ entity_type: row.entityType,
1514
+ permission: row.permission,
1515
+ subject_kind: row.subjectKind,
1516
+ subject_value: row.subjectValue,
1517
+ created_by: row.createdBy ?? void 0,
1518
+ expires_at: row.expiresAt?.toISOString(),
1519
+ created_at: row.createdAt.toISOString(),
1520
+ updated_at: row.updatedAt.toISOString()
1521
+ };
1522
+ }
1523
+ rowToEntityPermissionGrant(row) {
1524
+ return {
1525
+ id: row.id,
1526
+ entity_url: row.entityUrl,
1527
+ permission: row.permission,
1528
+ subject_kind: row.subjectKind,
1529
+ subject_value: row.subjectValue,
1530
+ propagation: row.propagation,
1531
+ copy_to_children: row.copyToChildren,
1532
+ created_by: row.createdBy ?? void 0,
1533
+ expires_at: row.expiresAt?.toISOString(),
1534
+ created_at: row.createdAt.toISOString(),
1535
+ updated_at: row.updatedAt.toISOString()
1536
+ };
1537
+ }
1142
1538
  rowToEntity(row) {
1143
1539
  return {
1144
1540
  url: row.url,
1145
1541
  type: row.type,
1146
1542
  status: assertEntityStatus(row.status),
1147
- streams: {
1148
- main: `${row.url}/main`,
1149
- error: `${row.url}/error`
1150
- },
1543
+ streams: { main: `${row.url}/main` },
1151
1544
  subscription_id: row.subscriptionId,
1152
1545
  dispatch_policy: row.dispatchPolicy ?? void 0,
1153
1546
  write_token: row.writeToken,
@@ -1169,6 +1562,8 @@ var PostgresRegistry = class {
1169
1562
  sourceRef: row.sourceRef,
1170
1563
  tags: row.tags ?? {},
1171
1564
  streamUrl: row.streamUrl,
1565
+ principalUrl: row.principalUrl ?? void 0,
1566
+ principalKind: row.principalKind ?? void 0,
1172
1567
  shapeHandle: row.shapeHandle ?? void 0,
1173
1568
  shapeOffset: row.shapeOffset ?? void 0,
1174
1569
  lastObserverActivityAt: row.lastObserverActivityAt,
@@ -1323,6 +1718,93 @@ const serverLog = {
1323
1718
  }
1324
1719
  };
1325
1720
 
1721
+ //#endregion
1722
+ //#region src/principal.ts
1723
+ const ELECTRIC_PRINCIPAL_HEADER = `electric-principal`;
1724
+ const PRINCIPAL_KINDS = new Set([
1725
+ `user`,
1726
+ `agent`,
1727
+ `service`,
1728
+ `system`
1729
+ ]);
1730
+ function parsePrincipalKey(input) {
1731
+ const colon = input.indexOf(`:`);
1732
+ if (colon <= 0) throw new Error(`Invalid principal identifier`);
1733
+ const kind = input.slice(0, colon);
1734
+ const id = input.slice(colon + 1);
1735
+ if (!PRINCIPAL_KINDS.has(kind)) throw new Error(`Invalid principal kind`);
1736
+ if (!id || id.includes(`/`)) throw new Error(`Invalid principal id`);
1737
+ const key = `${kind}:${id}`;
1738
+ return {
1739
+ kind,
1740
+ id,
1741
+ key,
1742
+ url: `/principal/${encodeURIComponent(key)}`
1743
+ };
1744
+ }
1745
+ function principalUrl(key) {
1746
+ return parsePrincipalKey(key).url;
1747
+ }
1748
+ function parsePrincipalUrl(url) {
1749
+ if (!url.startsWith(`/principal/`)) return null;
1750
+ const segment = url.slice(`/principal/`.length);
1751
+ if (!segment || segment.includes(`/`)) return null;
1752
+ try {
1753
+ return parsePrincipalKey(decodeURIComponent(segment));
1754
+ } catch {
1755
+ return null;
1756
+ }
1757
+ }
1758
+ const BUILT_IN_SYSTEM_PRINCIPAL_IDS = new Set([
1759
+ `framework`,
1760
+ `auth-sync`,
1761
+ `dev-local`
1762
+ ]);
1763
+ function isBuiltInSystemPrincipalUrl(url) {
1764
+ if (!url?.startsWith(`/principal/`)) return false;
1765
+ try {
1766
+ const principal = parsePrincipalUrl(url);
1767
+ if (!principal) return false;
1768
+ return principal.kind === `system` && BUILT_IN_SYSTEM_PRINCIPAL_IDS.has(principal.id);
1769
+ } catch {
1770
+ return false;
1771
+ }
1772
+ }
1773
+ function principalFromCreatedBy(createdBy) {
1774
+ if (!createdBy) return void 0;
1775
+ const principal = parsePrincipalUrl(createdBy);
1776
+ if (!principal) return {
1777
+ url: createdBy,
1778
+ key: null
1779
+ };
1780
+ return {
1781
+ url: principal.url,
1782
+ key: principal.key,
1783
+ kind: principal.kind,
1784
+ id: principal.id
1785
+ };
1786
+ }
1787
+ const principalIdentityStateSchema = __sinclair_typebox.Type.Object({
1788
+ kind: __sinclair_typebox.Type.Union([
1789
+ __sinclair_typebox.Type.Literal(`user`),
1790
+ __sinclair_typebox.Type.Literal(`agent`),
1791
+ __sinclair_typebox.Type.Literal(`service`),
1792
+ __sinclair_typebox.Type.Literal(`system`)
1793
+ ]),
1794
+ id: __sinclair_typebox.Type.String(),
1795
+ key: __sinclair_typebox.Type.String(),
1796
+ url: __sinclair_typebox.Type.String(),
1797
+ updated_at: __sinclair_typebox.Type.String(),
1798
+ display_name: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
1799
+ email: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
1800
+ avatar_url: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
1801
+ auth_provider: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
1802
+ auth_subject: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
1803
+ claims: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown())),
1804
+ created_at: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
1805
+ }, { additionalProperties: false });
1806
+ const principalUpdateIdentityMessageSchema = __sinclair_typebox.Type.Object({ identity: principalIdentityStateSchema }, { additionalProperties: false });
1807
+
1326
1808
  //#endregion
1327
1809
  //#region src/entity-projector.ts
1328
1810
  const ENTITY_SHAPE_COLUMNS = [
@@ -1331,6 +1813,7 @@ const ENTITY_SHAPE_COLUMNS = [
1331
1813
  `type`,
1332
1814
  `status`,
1333
1815
  `tags`,
1816
+ `created_by`,
1334
1817
  `spawn_args`,
1335
1818
  `sandbox`,
1336
1819
  `parent`,
@@ -1350,6 +1833,12 @@ function sourceRefFromStreamPath(streamPath) {
1350
1833
  const match = streamPath.match(/^\/_entities\/([^/]+)$/);
1351
1834
  return match?.[1] ?? null;
1352
1835
  }
1836
+ function principalScopedSourceRef(tagSourceRef, principalUrl$1, principalKind) {
1837
+ return `${tagSourceRef}-${(0, __electric_ax_agents_runtime.hashString)(JSON.stringify({
1838
+ principalKind,
1839
+ principalUrl: principalUrl$1
1840
+ }))}`;
1841
+ }
1353
1842
  function sameMember(left, right) {
1354
1843
  return JSON.stringify(left) === JSON.stringify(right);
1355
1844
  }
@@ -1380,15 +1869,22 @@ var ProjectedEntityBridge = class {
1380
1869
  sourceRef;
1381
1870
  tags;
1382
1871
  streamUrl;
1872
+ principalUrl;
1873
+ principalKind;
1874
+ permissionBypass;
1383
1875
  currentMembers = new Map();
1384
1876
  producer = null;
1385
1877
  stopped = false;
1386
- constructor(row, streamClient) {
1878
+ constructor(row, registry, streamClient) {
1879
+ this.registry = registry;
1387
1880
  this.streamClient = streamClient;
1388
1881
  this.tenantId = row.tenantId;
1389
1882
  this.sourceRef = row.sourceRef;
1390
1883
  this.tags = (0, __electric_ax_agents_runtime.normalizeTags)(row.tags);
1391
1884
  this.streamUrl = row.streamUrl;
1885
+ this.principalUrl = row.principalUrl;
1886
+ this.principalKind = row.principalKind;
1887
+ this.permissionBypass = isBuiltInSystemPrincipalUrl(row.principalUrl);
1392
1888
  }
1393
1889
  async start(initialEntities) {
1394
1890
  await this.ensureStream();
@@ -1402,7 +1898,7 @@ var ProjectedEntityBridge = class {
1402
1898
  }
1403
1899
  });
1404
1900
  await this.loadCurrentMembers();
1405
- this.reconcile(initialEntities);
1901
+ await this.reconcile(initialEntities);
1406
1902
  }
1407
1903
  async stop() {
1408
1904
  this.stopped = true;
@@ -1414,12 +1910,13 @@ var ProjectedEntityBridge = class {
1414
1910
  this.producer = null;
1415
1911
  }
1416
1912
  }
1417
- reconcile(entities$1) {
1913
+ async reconcile(entities$1) {
1418
1914
  if (this.stopped) return;
1419
1915
  const staleMembers = new Map(this.currentMembers);
1420
1916
  for (const entity of entities$1) {
1421
1917
  if (entity.tenant_id !== this.tenantId) continue;
1422
1918
  if (!entityMatchesTags(entity, this.tags)) continue;
1919
+ if (!await this.canReadEntity(entity)) continue;
1423
1920
  staleMembers.delete(entity.url);
1424
1921
  this.upsertEntity(entity);
1425
1922
  }
@@ -1428,10 +1925,10 @@ var ProjectedEntityBridge = class {
1428
1925
  this.currentMembers.delete(url);
1429
1926
  }
1430
1927
  }
1431
- applyEntity(entity) {
1928
+ async applyEntity(entity) {
1432
1929
  if (this.stopped) return;
1433
1930
  if (entity.tenant_id !== this.tenantId) return;
1434
- if (!entityMatchesTags(entity, this.tags)) {
1931
+ if (!entityMatchesTags(entity, this.tags) || !await this.canReadEntity(entity)) {
1435
1932
  const existing = this.currentMembers.get(entity.url);
1436
1933
  if (!existing) return;
1437
1934
  this.append(`delete`, existing);
@@ -1460,6 +1957,15 @@ var ProjectedEntityBridge = class {
1460
1957
  this.currentMembers.set(entity.url, next);
1461
1958
  }
1462
1959
  }
1960
+ async canReadEntity(entity) {
1961
+ if (this.permissionBypass) return true;
1962
+ if (!this.principalUrl || !this.principalKind) return false;
1963
+ if (entity.created_by === this.principalUrl) return true;
1964
+ return await this.registry.hasEntityPermission(entity.url, `read`, {
1965
+ principalUrl: this.principalUrl,
1966
+ principalKind: this.principalKind
1967
+ });
1968
+ }
1463
1969
  async ensureStream() {
1464
1970
  if (!await this.streamClient.exists(this.streamUrl)) await this.streamClient.create(this.streamUrl, { contentType: `application/json` });
1465
1971
  }
@@ -1564,17 +2070,19 @@ var EntityProjector = class {
1564
2070
  this.activeReaders.clear();
1565
2071
  await Promise.all(projections.map((projection) => projection.stop()));
1566
2072
  }
1567
- async register(tenantId, registry, tagsInput) {
2073
+ async register(tenantId, registry, tagsInput, principalUrl$1, principalKind) {
1568
2074
  if (!this.electricUrl) throw new Error(`[entity-projector] Electric URL is required for entities()`);
1569
2075
  await this.start();
1570
2076
  this.registries.set(tenantId, registry);
1571
2077
  const tags = (0, __electric_ax_agents_runtime.normalizeTags)((0, __electric_ax_agents_runtime.assertTags)(tagsInput));
1572
- const sourceRef = (0, __electric_ax_agents_runtime.sourceRefForTags)(tags);
2078
+ const sourceRef = principalScopedSourceRef((0, __electric_ax_agents_runtime.sourceRefForTags)(tags), principalUrl$1, principalKind);
1573
2079
  const streamUrl = (0, __electric_ax_agents_runtime.getEntitiesStreamPath)(sourceRef);
1574
2080
  const row = await registry.upsertEntityBridge({
1575
2081
  sourceRef,
1576
2082
  tags,
1577
- streamUrl
2083
+ streamUrl,
2084
+ principalUrl: principalUrl$1,
2085
+ principalKind
1578
2086
  });
1579
2087
  await registry.touchEntityBridge(sourceRef);
1580
2088
  await this.ensureProjection(row);
@@ -1603,7 +2111,11 @@ var EntityProjector = class {
1603
2111
  await this.touchSourceRef(tenantId, registry, sourceRef, `read-close`);
1604
2112
  };
1605
2113
  }
1606
- async onEntityChanged(_tenantId, _entityUrl) {}
2114
+ async onEntityChanged(tenantId, entityUrl) {
2115
+ const entity = this.entities.get(entityKey(tenantId, entityUrl));
2116
+ if (!entity) return;
2117
+ for (const projection of this.projectionsForTenant(tenantId)) await projection.applyEntity(entity);
2118
+ }
1607
2119
  async loadTenantBridges(tenantId, registry = this.registryForTenant(tenantId)) {
1608
2120
  if (!this.started || !this.electricUrl) return;
1609
2121
  await this.loadPersistedBridgesForTenant(tenantId, registry);
@@ -1664,16 +2176,16 @@ var EntityProjector = class {
1664
2176
  }
1665
2177
  if (message.headers.control === `up-to-date`) {
1666
2178
  this.upToDate = true;
1667
- this.reconcileAll();
2179
+ await this.reconcileAll();
1668
2180
  this.readyResolve?.();
1669
2181
  }
1670
2182
  continue;
1671
2183
  }
1672
2184
  if (!(0, __electric_sql_client.isChangeMessage)(message)) continue;
1673
- this.applyChangeMessage(message);
2185
+ await this.applyChangeMessage(message);
1674
2186
  }
1675
2187
  }
1676
- applyChangeMessage(message) {
2188
+ async applyChangeMessage(message) {
1677
2189
  const entity = message.value;
1678
2190
  const key = entityKey(entity.tenant_id, entity.url);
1679
2191
  if (message.headers.operation === `delete`) {
@@ -1682,7 +2194,7 @@ var EntityProjector = class {
1682
2194
  return;
1683
2195
  }
1684
2196
  this.entities.set(key, entity);
1685
- if (this.upToDate) for (const projection of this.projectionsForTenant(entity.tenant_id)) projection.applyEntity(entity);
2197
+ if (this.upToDate) for (const projection of this.projectionsForTenant(entity.tenant_id)) await projection.applyEntity(entity);
1686
2198
  }
1687
2199
  async loadPersistedBridges() {
1688
2200
  const registry = new PostgresRegistry(this.db);
@@ -1745,7 +2257,7 @@ var EntityProjector = class {
1745
2257
  }
1746
2258
  throw error;
1747
2259
  }
1748
- const projection = new ProjectedEntityBridge(row, streamClient);
2260
+ const projection = new ProjectedEntityBridge(row, this.registryForTenant(row.tenantId), streamClient);
1749
2261
  await projection.start(this.entitiesForTenant(row.tenantId));
1750
2262
  this.projections.set(key, projection);
1751
2263
  })().finally(() => {
@@ -1760,8 +2272,8 @@ var EntityProjector = class {
1760
2272
  projectionsForTenant(tenantId) {
1761
2273
  return [...this.projections.values()].filter((projection) => projection.tenantId === tenantId);
1762
2274
  }
1763
- reconcileAll() {
1764
- for (const projection of this.projections.values()) projection.reconcile(this.entitiesForTenant(projection.tenantId));
2275
+ async reconcileAll() {
2276
+ for (const projection of this.projections.values()) await projection.reconcile(this.entitiesForTenant(projection.tenantId));
1765
2277
  }
1766
2278
  async touchSourceRef(tenantId, registry, sourceRef, reason) {
1767
2279
  try {
@@ -1803,8 +2315,8 @@ var EntityProjectorTenantFacade = class {
1803
2315
  await this.projector.start();
1804
2316
  }
1805
2317
  async stop() {}
1806
- async register(tagsInput) {
1807
- return await this.projector.register(this.tenantId, this.registry, tagsInput);
2318
+ async register(tagsInput, principalUrl$1, principalKind) {
2319
+ return await this.projector.register(this.tenantId, this.registry, tagsInput, principalUrl$1, principalKind);
1808
2320
  }
1809
2321
  async onEntityChanged(entityUrl) {
1810
2322
  await this.projector.onEntityChanged(this.tenantId, entityUrl);
@@ -2686,93 +3198,6 @@ function assertSharedSandboxColocated(key, chosenIsRemote, dispatchPolicy) {
2686
3198
  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);
2687
3199
  }
2688
3200
 
2689
- //#endregion
2690
- //#region src/principal.ts
2691
- const ELECTRIC_PRINCIPAL_HEADER = `electric-principal`;
2692
- const PRINCIPAL_KINDS = new Set([
2693
- `user`,
2694
- `agent`,
2695
- `service`,
2696
- `system`
2697
- ]);
2698
- function parsePrincipalKey(input) {
2699
- const colon = input.indexOf(`:`);
2700
- if (colon <= 0) throw new Error(`Invalid principal identifier`);
2701
- const kind = input.slice(0, colon);
2702
- const id = input.slice(colon + 1);
2703
- if (!PRINCIPAL_KINDS.has(kind)) throw new Error(`Invalid principal kind`);
2704
- if (!id || id.includes(`/`)) throw new Error(`Invalid principal id`);
2705
- const key = `${kind}:${id}`;
2706
- return {
2707
- kind,
2708
- id,
2709
- key,
2710
- url: `/principal/${encodeURIComponent(key)}`
2711
- };
2712
- }
2713
- function principalUrl(key) {
2714
- return parsePrincipalKey(key).url;
2715
- }
2716
- function parsePrincipalUrl(url) {
2717
- if (!url.startsWith(`/principal/`)) return null;
2718
- const segment = url.slice(`/principal/`.length);
2719
- if (!segment || segment.includes(`/`)) return null;
2720
- try {
2721
- return parsePrincipalKey(decodeURIComponent(segment));
2722
- } catch {
2723
- return null;
2724
- }
2725
- }
2726
- const BUILT_IN_SYSTEM_PRINCIPAL_IDS = new Set([
2727
- `framework`,
2728
- `auth-sync`,
2729
- `dev-local`
2730
- ]);
2731
- function isBuiltInSystemPrincipalUrl(url) {
2732
- if (!url?.startsWith(`/principal/`)) return false;
2733
- try {
2734
- const principal = parsePrincipalUrl(url);
2735
- if (!principal) return false;
2736
- return principal.kind === `system` && BUILT_IN_SYSTEM_PRINCIPAL_IDS.has(principal.id);
2737
- } catch {
2738
- return false;
2739
- }
2740
- }
2741
- function principalFromCreatedBy(createdBy) {
2742
- if (!createdBy) return void 0;
2743
- const principal = parsePrincipalUrl(createdBy);
2744
- if (!principal) return {
2745
- url: createdBy,
2746
- key: null
2747
- };
2748
- return {
2749
- url: principal.url,
2750
- key: principal.key,
2751
- kind: principal.kind,
2752
- id: principal.id
2753
- };
2754
- }
2755
- const principalIdentityStateSchema = __sinclair_typebox.Type.Object({
2756
- kind: __sinclair_typebox.Type.Union([
2757
- __sinclair_typebox.Type.Literal(`user`),
2758
- __sinclair_typebox.Type.Literal(`agent`),
2759
- __sinclair_typebox.Type.Literal(`service`),
2760
- __sinclair_typebox.Type.Literal(`system`)
2761
- ]),
2762
- id: __sinclair_typebox.Type.String(),
2763
- key: __sinclair_typebox.Type.String(),
2764
- url: __sinclair_typebox.Type.String(),
2765
- updated_at: __sinclair_typebox.Type.String(),
2766
- display_name: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
2767
- email: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
2768
- avatar_url: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
2769
- auth_provider: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
2770
- auth_subject: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
2771
- claims: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown())),
2772
- created_at: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
2773
- }, { additionalProperties: false });
2774
- const principalUpdateIdentityMessageSchema = __sinclair_typebox.Type.Object({ identity: principalIdentityStateSchema }, { additionalProperties: false });
2775
-
2776
3201
  //#endregion
2777
3202
  //#region src/manifest-side-effects.ts
2778
3203
  function isRecord$1(value) {
@@ -2968,6 +3393,7 @@ var EntityManager = class {
2968
3393
  this.validateSchema(req.creation_schema);
2969
3394
  this.validateSchemaMap(req.inbox_schemas);
2970
3395
  this.validateSchemaMap(req.state_schemas);
3396
+ this.validateSlashCommands(req.slash_commands);
2971
3397
  const defaultDispatchPolicy = req.default_dispatch_policy ? this.validateDispatchPolicy(req.default_dispatch_policy, { label: `default_dispatch_policy` }) : void 0;
2972
3398
  const existing = await this.registry.getEntityType(req.name);
2973
3399
  const now = new Date().toISOString();
@@ -2977,6 +3403,7 @@ var EntityManager = class {
2977
3403
  creation_schema: req.creation_schema,
2978
3404
  inbox_schemas: req.inbox_schemas,
2979
3405
  state_schemas: req.state_schemas,
3406
+ slash_commands: req.slash_commands,
2980
3407
  serve_endpoint: req.serve_endpoint,
2981
3408
  default_dispatch_policy: defaultDispatchPolicy,
2982
3409
  revision: existing ? existing.revision + 1 : 1,
@@ -3008,7 +3435,10 @@ var EntityManager = class {
3008
3435
  }
3009
3436
  async ensurePrincipal(principal) {
3010
3437
  const existing = await this.registry.getEntity(principal.url);
3011
- if (existing) return existing;
3438
+ if (existing) {
3439
+ await this.ensureUserPrincipal(principal);
3440
+ return existing;
3441
+ }
3012
3442
  await this.ensurePrincipalEntityType();
3013
3443
  try {
3014
3444
  const entity = await this.spawn(`principal`, {
@@ -3037,15 +3467,22 @@ var EntityManager = class {
3037
3467
  updated_at: now
3038
3468
  }
3039
3469
  }));
3470
+ await this.ensureUserPrincipal(principal);
3040
3471
  return entity;
3041
3472
  } catch (error) {
3042
3473
  if (error instanceof ElectricAgentsError && error.code === ErrCodeDuplicateURL) {
3043
3474
  const raced = await this.registry.getEntity(principal.url);
3044
- if (raced) return raced;
3475
+ if (raced) {
3476
+ await this.ensureUserPrincipal(principal);
3477
+ return raced;
3478
+ }
3045
3479
  }
3046
3480
  throw error;
3047
3481
  }
3048
3482
  }
3483
+ async ensureUserPrincipal(principal) {
3484
+ if (principal.kind === `user`) await this.registry.ensureUserForPrincipal(principal);
3485
+ }
3049
3486
  /**
3050
3487
  * Spawn a new entity of the given type with durable streams.
3051
3488
  */
@@ -3075,7 +3512,6 @@ var EntityManager = class {
3075
3512
  const writeToken = (0, node_crypto.randomUUID)();
3076
3513
  const entityURL = typeName === `principal` ? principalUrl(instanceId) : `/${typeName}/${instanceId}`;
3077
3514
  const mainPath = `${entityURL}/main`;
3078
- const errorPath = `${entityURL}/error`;
3079
3515
  const subscriptionId = `${typeName}-handler`;
3080
3516
  const spawnT0 = performance.now();
3081
3517
  const existingByURL = await this.registry.getEntity(entityURL);
@@ -3092,10 +3528,7 @@ var EntityManager = class {
3092
3528
  type: typeName,
3093
3529
  status: `idle`,
3094
3530
  url: entityURL,
3095
- streams: {
3096
- main: mainPath,
3097
- error: errorPath
3098
- },
3531
+ streams: { main: mainPath },
3099
3532
  subscription_id: subscriptionId,
3100
3533
  dispatch_policy: dispatchPolicy,
3101
3534
  write_token: writeToken,
@@ -3132,6 +3565,18 @@ var EntityManager = class {
3132
3565
  }
3133
3566
  });
3134
3567
  const initialEvents = [createdEvent];
3568
+ const slashCommandTimestamp = new Date().toISOString();
3569
+ for (const command of entityType.slash_commands ?? []) {
3570
+ const slashCommandEvent = __electric_ax_agents_runtime.entityStateSchema.slashCommands.insert({
3571
+ key: command.name,
3572
+ value: {
3573
+ ...command,
3574
+ source: `static`,
3575
+ updated_at: slashCommandTimestamp
3576
+ }
3577
+ });
3578
+ initialEvents.push(slashCommandEvent);
3579
+ }
3135
3580
  if (req.initialMessage !== void 0) {
3136
3581
  const msgNow = new Date().toISOString();
3137
3582
  const inboxEvent = __electric_ax_agents_runtime.entityStateSchema.inbox.insert({
@@ -3139,6 +3584,7 @@ var EntityManager = class {
3139
3584
  value: {
3140
3585
  from: req.created_by ?? req.parent ?? `spawn`,
3141
3586
  payload: req.initialMessage,
3587
+ message_type: req.initialMessageType,
3142
3588
  timestamp: msgNow
3143
3589
  }
3144
3590
  });
@@ -3148,55 +3594,43 @@ var EntityManager = class {
3148
3594
  const queueEnterT0 = performance.now();
3149
3595
  const queueWaiting = this.spawnPersistQueue.length();
3150
3596
  const queueRunning = this.spawnPersistQueue.running();
3151
- const [mainStreamResult, errorStreamResult, entityResult] = await this.spawnPersistQueue.push(async () => {
3597
+ const [mainStreamResult, entityResult] = await this.spawnPersistQueue.push(async () => {
3152
3598
  let entityTxid;
3153
3599
  try {
3154
3600
  entityTxid = await withSpan(`db.createEntity`, () => this.registry.createEntity(entityData));
3155
3601
  } catch (err) {
3156
- return [
3157
- {
3158
- status: `fulfilled`,
3159
- value: void 0
3160
- },
3161
- {
3162
- status: `fulfilled`,
3163
- value: void 0
3164
- },
3165
- {
3166
- status: `rejected`,
3167
- reason: err
3168
- }
3169
- ];
3602
+ return [{
3603
+ status: `fulfilled`,
3604
+ value: void 0
3605
+ }, {
3606
+ status: `rejected`,
3607
+ reason: err
3608
+ }];
3170
3609
  }
3171
- const [mainStreamResult$1, errorStreamResult$1] = await Promise.allSettled([this.streamClient.create(mainPath, {
3610
+ const [mainStreamResult$1] = await Promise.allSettled([this.streamClient.create(mainPath, {
3172
3611
  contentType,
3173
3612
  body: initialBody
3174
- }), this.streamClient.create(errorPath, { contentType })]);
3175
- return [
3176
- mainStreamResult$1,
3177
- errorStreamResult$1,
3178
- {
3179
- status: `fulfilled`,
3180
- value: entityTxid
3181
- }
3182
- ];
3613
+ })]);
3614
+ return [mainStreamResult$1, {
3615
+ status: `fulfilled`,
3616
+ value: entityTxid
3617
+ }];
3183
3618
  });
3184
3619
  const parallelMs = +(performance.now() - queueEnterT0).toFixed(2);
3185
- if (mainStreamResult.status === `rejected` || errorStreamResult.status === `rejected` || entityResult.status === `rejected`) {
3620
+ if (mainStreamResult.status === `rejected` || entityResult.status === `rejected`) {
3186
3621
  const entityReason = entityResult.status === `rejected` ? entityResult.reason : null;
3187
- const streamReason = mainStreamResult.status === `rejected` ? mainStreamResult.reason : errorStreamResult.status === `rejected` ? errorStreamResult.reason : null;
3622
+ const streamReason = mainStreamResult.status === `rejected` ? mainStreamResult.reason : null;
3188
3623
  const isDuplicate = entityReason instanceof EntityAlreadyExistsError;
3189
3624
  const isStreamConflict = !!streamReason && typeof streamReason === `object` && (`status` in streamReason && streamReason.status === 409 || `code` in streamReason && streamReason.code === `CONFLICT_SEQ`);
3190
3625
  const rollbacks = [];
3191
3626
  if (!isDuplicate && !isStreamConflict) {
3192
3627
  if (mainStreamResult.status === `fulfilled`) rollbacks.push(this.streamClient.delete(mainPath));
3193
- if (errorStreamResult.status === `fulfilled`) rollbacks.push(this.streamClient.delete(errorPath));
3194
3628
  if (entityResult.status === `fulfilled`) rollbacks.push(this.registry.deleteEntity(entityURL));
3195
3629
  if (req.wake) rollbacks.push(this.wakeRegistry.unregisterBySubscriberAndSource(req.wake.subscriberUrl, entityURL, this.tenantId));
3196
3630
  await Promise.allSettled(rollbacks);
3197
3631
  }
3198
3632
  if (isDuplicate || isStreamConflict) throw new ElectricAgentsError(ErrCodeDuplicateURL, `Entity already exists at URL "${entityURL}"`, 409);
3199
- const failure = mainStreamResult.status === `rejected` ? mainStreamResult.reason : errorStreamResult.status === `rejected` ? errorStreamResult.reason : entityResult.reason;
3633
+ const failure = mainStreamResult.status === `rejected` ? mainStreamResult.reason : entityResult.reason;
3200
3634
  if (failure instanceof Error) throw failure;
3201
3635
  throw new ElectricAgentsError(`SPAWN_FAILED`, `Spawn failed: ${String(failure)}`, 500);
3202
3636
  }
@@ -3281,7 +3715,7 @@ var EntityManager = class {
3281
3715
  });
3282
3716
  const sharedStateIdMap = await this.buildForkSharedStateIdMap(snapshot.sharedStateIds, suffix);
3283
3717
  const stringMap = this.buildForkStringMap(entityUrlMap, sharedStateIdMap);
3284
- const entityPlans = this.buildForkEntityPlans(effectiveSubtree, entityUrlMap, stringMap);
3718
+ const entityPlans = this.buildForkEntityPlans(effectiveSubtree, entityUrlMap, stringMap, opts.createdBy);
3285
3719
  this.addForkLocks(this.forkWriteLockedEntities, effectiveSubtree.map((entity) => entity.url), writeEntityLocks);
3286
3720
  this.addForkLocks(this.forkWriteLockedStreams, [...snapshot.sharedStateIds].map((id) => (0, __electric_ax_agents_runtime.getSharedStateStreamPath)(id)), writeStreamLocks);
3287
3721
  const createdStreams = [];
@@ -3292,8 +3726,6 @@ var EntityManager = class {
3292
3726
  const isRoot = plan.source.url === rootUrl;
3293
3727
  await this.streamClient.fork(plan.fork.streams.main, plan.source.streams.main, isRoot && opts.forkPointer ? { forkPointer: opts.forkPointer } : void 0);
3294
3728
  createdStreams.push(plan.fork.streams.main);
3295
- await this.streamClient.fork(plan.fork.streams.error, plan.source.streams.error);
3296
- createdStreams.push(plan.fork.streams.error);
3297
3729
  }
3298
3730
  for (const [sourceId, forkId] of sharedStateIdMap) {
3299
3731
  const sourcePath = (0, __electric_ax_agents_runtime.getSharedStateStreamPath)(sourceId);
@@ -3627,7 +4059,6 @@ var EntityManager = class {
3627
4059
  for (const [sourceUrl, forkUrl] of entityUrlMap) {
3628
4060
  stringMap.set(sourceUrl, forkUrl);
3629
4061
  stringMap.set(`${sourceUrl}/main`, `${forkUrl}/main`);
3630
- stringMap.set(`${sourceUrl}/error`, `${forkUrl}/error`);
3631
4062
  }
3632
4063
  for (const [sourceId, forkId] of sharedStateIdMap) {
3633
4064
  stringMap.set(sourceId, forkId);
@@ -3635,7 +4066,7 @@ var EntityManager = class {
3635
4066
  }
3636
4067
  return stringMap;
3637
4068
  }
3638
- buildForkEntityPlans(entitiesToFork, entityUrlMap, stringMap) {
4069
+ buildForkEntityPlans(entitiesToFork, entityUrlMap, stringMap, createdBy) {
3639
4070
  const now = Date.now();
3640
4071
  return entitiesToFork.map((source) => {
3641
4072
  const forkUrl = entityUrlMap.get(source.url);
@@ -3648,14 +4079,12 @@ var EntityManager = class {
3648
4079
  url: forkUrl,
3649
4080
  type,
3650
4081
  status: `idle`,
3651
- streams: {
3652
- main: `${forkUrl}/main`,
3653
- error: `${forkUrl}/error`
3654
- },
4082
+ streams: { main: `${forkUrl}/main` },
3655
4083
  subscription_id: `${type}-handler`,
3656
4084
  write_token: (0, node_crypto.randomUUID)(),
3657
4085
  spawn_args: spawnArgs,
3658
4086
  parent,
4087
+ created_by: createdBy ?? source.created_by,
3659
4088
  created_at: now,
3660
4089
  updated_at: now
3661
4090
  };
@@ -3889,7 +4318,7 @@ var EntityManager = class {
3889
4318
  }
3890
4319
  async materializeForkManifestSideEffects(entityUrl, manifests) {
3891
4320
  for (const [manifestKey, manifest] of manifests) {
3892
- await this.syncEntitiesManifestSource(entityUrl, manifestKey, `upsert`, manifest);
4321
+ await this.syncManifestLinks(entityUrl, manifestKey, `upsert`, manifest);
3893
4322
  const wake = buildManifestWakeRegistration(entityUrl, manifest, manifestKey);
3894
4323
  if (wake) await this.wakeRegistry.register({
3895
4324
  ...wake,
@@ -3919,6 +4348,7 @@ var EntityManager = class {
3919
4348
  await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
3920
4349
  entityUrl: targetUrl,
3921
4350
  from: senderUrl,
4351
+ from_agent: senderUrl,
3922
4352
  payload: manifest.payload,
3923
4353
  key: `scheduled-${producerId}`,
3924
4354
  type: typeof manifest.messageType === `string` ? manifest.messageType : void 0,
@@ -3958,12 +4388,14 @@ var EntityManager = class {
3958
4388
  const now = new Date().toISOString();
3959
4389
  const key = req.key ?? `msg-in-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
3960
4390
  const value = {
3961
- from: req.from,
4391
+ from: req.from_principal ?? req.from,
3962
4392
  payload: req.payload,
3963
4393
  timestamp: now,
3964
4394
  mode: req.mode ?? `immediate`,
3965
4395
  status: req.mode === `queued` || req.mode === `paused` ? `pending` : `processed`
3966
4396
  };
4397
+ if (req.from_principal) value.from_principal = req.from_principal;
4398
+ if (req.from_agent) value.from_agent = req.from_agent;
3967
4399
  if (req.type) value.message_type = req.type;
3968
4400
  if (req.position) value.position = req.position;
3969
4401
  else if (value.mode === `queued` || value.mode === `paused`) value.position = createInitialQueuePosition(new Date(now));
@@ -4135,9 +4567,9 @@ var EntityManager = class {
4135
4567
  if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
4136
4568
  return updated;
4137
4569
  }
4138
- async ensureEntitiesMembershipStream(tags) {
4570
+ async ensureEntitiesMembershipStream(tags, principal) {
4139
4571
  if (!this.entityBridgeManager) throw new Error(`Entity bridge manager not configured`);
4140
- return this.entityBridgeManager.register(this.validateTags(tags));
4572
+ return this.entityBridgeManager.register(this.validateTags(tags), principal.url, principal.kind);
4141
4573
  }
4142
4574
  async writeManifestEntry(entityUrl, key, operation, value, opts) {
4143
4575
  const entity = await this.registry.getEntity(entityUrl);
@@ -4155,11 +4587,11 @@ var EntityManager = class {
4155
4587
  const encoded = this.encodeChangeEvent(event);
4156
4588
  if (opts?.producerId) {
4157
4589
  await this.streamClient.appendIdempotent(entity.streams.main, encoded, { producerId: opts.producerId });
4158
- await this.syncEntitiesManifestSource(entityUrl, key, operation, value);
4590
+ await this.syncManifestLinks(entityUrl, key, operation, value);
4159
4591
  return;
4160
4592
  }
4161
4593
  await this.streamClient.append(entity.streams.main, encoded);
4162
- await this.syncEntitiesManifestSource(entityUrl, key, operation, value);
4594
+ await this.syncManifestLinks(entityUrl, key, operation, value);
4163
4595
  }
4164
4596
  async upsertCronSchedule(entityUrl, req) {
4165
4597
  if (req.payload === void 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: payload`, 400);
@@ -4308,6 +4740,8 @@ var EntityManager = class {
4308
4740
  await this.scheduler.enqueueDelayedSend({
4309
4741
  entityUrl,
4310
4742
  from: req.from,
4743
+ from_principal: req.from_principal,
4744
+ from_agent: req.from_agent,
4311
4745
  payload: req.payload,
4312
4746
  key: req.key,
4313
4747
  type: req.type,
@@ -4350,14 +4784,23 @@ var EntityManager = class {
4350
4784
  await this.streamClient.appendIdempotent(subscriber.streams.main, this.encodeChangeEvent(wakeEvent), { producerId: `wake-reg-${result.registrationDbId}-${result.sourceEventKey}` });
4351
4785
  });
4352
4786
  }
4353
- async syncEntitiesManifestSource(entityUrl, manifestKey, operation, value) {
4787
+ async syncManifestLinks(entityUrl, manifestKey, operation, value) {
4354
4788
  const sourceRef = operation === `delete` ? void 0 : this.extractEntitiesSourceRef(value);
4355
4789
  await this.registry.replaceEntityManifestSource(entityUrl, manifestKey, sourceRef);
4790
+ const sharedStateId = operation === `delete` ? void 0 : this.extractSharedStateId(value);
4791
+ await this.registry.replaceSharedStateLink(entityUrl, manifestKey, sharedStateId);
4356
4792
  }
4357
4793
  extractEntitiesSourceRef(manifest) {
4358
4794
  if (manifest?.kind === `source` && manifest.sourceType === `entities` && typeof manifest.sourceRef === `string`) return manifest.sourceRef;
4359
4795
  return void 0;
4360
4796
  }
4797
+ extractSharedStateId(manifest) {
4798
+ if (manifest?.kind === `shared-state` && typeof manifest.id === `string`) return manifest.id;
4799
+ if (manifest?.kind !== `source` || manifest.sourceType !== `db`) return void 0;
4800
+ if (typeof manifest.sourceRef === `string`) return manifest.sourceRef;
4801
+ const config = isRecord(manifest.config) ? manifest.config : void 0;
4802
+ return typeof config?.id === `string` ? config.id : void 0;
4803
+ }
4361
4804
  /**
4362
4805
  * Read a child entity's stream and extract concatenated text deltas
4363
4806
  * for a specific run, plus any error messages for that run.
@@ -4521,14 +4964,7 @@ var EntityManager = class {
4521
4964
  await this.streamClient.append(entity.streams.main, signalData);
4522
4965
  return;
4523
4966
  }
4524
- const errorCloseEvent = {
4525
- type: `signal`,
4526
- key: signalEvent.key,
4527
- value: signalEvent.value,
4528
- headers: signalEvent.headers
4529
- };
4530
- const errorSignalData = this.encodeChangeEvent(errorCloseEvent);
4531
- for (const [streamPath, data] of [[entity.streams.main, signalData], [entity.streams.error, errorSignalData]]) try {
4967
+ for (const [streamPath, data] of [[entity.streams.main, signalData]]) try {
4532
4968
  await this.streamClient.append(streamPath, data, { close: true });
4533
4969
  } catch (err) {
4534
4970
  const message = err instanceof Error ? err.message : String(err);
@@ -4594,7 +5030,9 @@ var EntityManager = class {
4594
5030
  creation_schema: existing.creation_schema,
4595
5031
  inbox_schemas: mergedInbox,
4596
5032
  state_schemas: mergedState,
5033
+ slash_commands: existing.slash_commands,
4597
5034
  serve_endpoint: existing.serve_endpoint,
5035
+ default_dispatch_policy: existing.default_dispatch_policy,
4598
5036
  revision: nextRevision,
4599
5037
  created_at: existing.created_at,
4600
5038
  updated_at: now
@@ -4648,11 +5086,19 @@ var EntityManager = class {
4648
5086
  throw new ElectricAgentsError(ErrCodeInvalidRequest, error instanceof Error ? error.message : `Invalid tags`, 400);
4649
5087
  }
4650
5088
  }
5089
+ validateSlashCommands(input) {
5090
+ const validationError = (0, __electric_ax_agents_runtime.validateSlashCommandDefinitions)(input);
5091
+ if (!validationError) return;
5092
+ throw new ElectricAgentsError(ErrCodeSchemaValidationFailed, validationError.message, 422, validationError.details);
5093
+ }
4651
5094
  async validateSendRequest(entityUrl, req) {
4652
5095
  const entity = await this.registry.getEntity(entityUrl);
4653
5096
  if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
4654
5097
  if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
4655
- if (req.type && entity.type) {
5098
+ if (req.type === __electric_ax_agents_runtime.COMPOSER_INPUT_MESSAGE_TYPE) {
5099
+ const valErr = (0, __electric_ax_agents_runtime.validateComposerInputPayload)(req.payload);
5100
+ if (valErr) throw new ElectricAgentsError(ErrCodeSchemaValidationFailed, valErr.message, 422, valErr.details);
5101
+ } else if (req.type && entity.type) {
4656
5102
  const { inboxSchemas } = await this.getEffectiveSchemas(entity);
4657
5103
  if (inboxSchemas) {
4658
5104
  const schema = inboxSchemas[req.type];
@@ -5511,6 +5957,8 @@ var ElectricAgentsTenantRuntime = class {
5511
5957
  try {
5512
5958
  await this.manager.send(payload.entityUrl, {
5513
5959
  from: payload.from,
5960
+ from_principal: payload.from_principal,
5961
+ from_agent: payload.from_agent,
5514
5962
  payload: payload.payload,
5515
5963
  key: payload.key ?? `scheduled-task-${taskId}`,
5516
5964
  type: payload.type
@@ -5583,6 +6031,7 @@ var ElectricAgentsTenantRuntime = class {
5583
6031
  await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
5584
6032
  entityUrl: targetUrl,
5585
6033
  from: senderUrl,
6034
+ from_agent: senderUrl,
5586
6035
  payload: value.payload,
5587
6036
  key: `scheduled-${producerId}`,
5588
6037
  type: typeof value.messageType === `string` ? value.messageType : void 0,
@@ -5607,11 +6056,20 @@ var ElectricAgentsTenantRuntime = class {
5607
6056
  async applyManifestEntitySource(ownerEntityUrl, manifestKey, operation, value) {
5608
6057
  const sourceRef = operation === `delete` ? void 0 : this.extractEntitiesSourceRef(value);
5609
6058
  await this.manager.registry.replaceEntityManifestSource(ownerEntityUrl, manifestKey, sourceRef);
6059
+ const sharedStateId = operation === `delete` ? void 0 : this.extractSharedStateId(value);
6060
+ await this.manager.registry.replaceSharedStateLink(ownerEntityUrl, manifestKey, sharedStateId);
5610
6061
  }
5611
6062
  extractEntitiesSourceRef(manifest) {
5612
6063
  if (manifest?.kind === `source` && manifest.sourceType === `entities` && typeof manifest.sourceRef === `string`) return manifest.sourceRef;
5613
6064
  return void 0;
5614
6065
  }
6066
+ extractSharedStateId(manifest) {
6067
+ if (manifest?.kind === `shared-state` && typeof manifest.id === `string`) return manifest.id;
6068
+ if (manifest?.kind !== `source` || manifest.sourceType !== `db`) return void 0;
6069
+ if (typeof manifest.sourceRef === `string`) return manifest.sourceRef;
6070
+ const config = typeof manifest.config === `object` && manifest.config !== null && !Array.isArray(manifest.config) ? manifest.config : void 0;
6071
+ return typeof config?.id === `string` ? config.id : void 0;
6072
+ }
5615
6073
  async maybeMarkEntityIdleAfterRunFinished(entityUrl) {
5616
6074
  const primaryStream = `${entityUrl}/main`;
5617
6075
  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);
@@ -6292,6 +6750,8 @@ var WakeRegistry = class {
6292
6750
  if (eventType === `inbox`) {
6293
6751
  const value = event.value;
6294
6752
  if (typeof value?.from === `string`) change.from = value.from;
6753
+ if (typeof value?.from_principal === `string`) change.from_principal = value.from_principal;
6754
+ if (typeof value?.from_agent === `string`) change.from_agent = value.from_agent;
6295
6755
  if (`payload` in (value ?? {})) change.payload = value?.payload;
6296
6756
  if (typeof value?.timestamp === `string`) change.timestamp = value.timestamp;
6297
6757
  if (typeof value?.message_type === `string`) change.message_type = value.message_type;
@@ -6703,29 +7163,136 @@ function buildElectricProxyTarget(options) {
6703
7163
  if (options.electricSecret) target.searchParams.set(`secret`, options.electricSecret);
6704
7164
  const table = options.incomingUrl.searchParams.get(`table`);
6705
7165
  if (table === `entities`) {
6706
- target.searchParams.set(`columns`, `"tenant_id","url","type","status","dispatch_policy","tags","spawn_args","sandbox","parent","type_revision","inbox_schemas","state_schemas","created_at","updated_at"`);
6707
- applyTenantShapeWhere(target, options.tenantId);
7166
+ 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"`);
7167
+ applyShapeWhere(target, buildReadableEntitiesWhere({
7168
+ tenantId: options.tenantId,
7169
+ principalUrl: options.principalUrl ?? ``,
7170
+ principalKind: options.principalKind ?? ``,
7171
+ permissionBypass: options.permissionBypass
7172
+ }));
6708
7173
  } else if (table === `entity_types`) {
6709
- target.searchParams.set(`columns`, `"tenant_id","name","description","creation_schema","inbox_schemas","state_schemas","serve_endpoint","default_dispatch_policy","revision","created_at","updated_at"`);
6710
- applyTenantShapeWhere(target, options.tenantId);
7174
+ target.searchParams.set(`columns`, `"tenant_id","name","description","creation_schema","inbox_schemas","state_schemas","slash_commands","serve_endpoint","default_dispatch_policy","revision","created_at","updated_at"`);
7175
+ applyShapeWhere(target, buildSpawnableEntityTypesWhere({
7176
+ tenantId: options.tenantId,
7177
+ principalUrl: options.principalUrl ?? ``,
7178
+ principalKind: options.principalKind ?? ``,
7179
+ permissionBypass: options.permissionBypass
7180
+ }));
6711
7181
  } else if (table === `runners`) {
6712
7182
  target.searchParams.set(`columns`, `"tenant_id","id","owner_principal","label","kind","admin_status","wake_stream","sandbox_profiles","created_at","updated_at"`);
6713
7183
  applyTenantShapeWhere(target, options.tenantId, [`owner_principal = ${sqlStringLiteral(options.principalUrl ?? ``)}`]);
7184
+ } else if (table === `users`) {
7185
+ target.searchParams.set(`columns`, `"tenant_id","id","display_name","email","avatar_url","created_at","updated_at"`);
7186
+ applyTenantShapeWhere(target, options.tenantId);
7187
+ } else if (table === `entity_effective_permissions`) {
7188
+ target.searchParams.set(`columns`, `"tenant_id","id","entity_url","source_entity_url","source_grant_id","permission","subject_kind","subject_value","expires_at","created_at"`);
7189
+ applyShapeWhere(target, buildCurrentPrincipalEntityEffectivePermissionsWhere({
7190
+ tenantId: options.tenantId,
7191
+ principalUrl: options.principalUrl ?? ``,
7192
+ principalKind: options.principalKind ?? ``,
7193
+ permissionBypass: options.permissionBypass
7194
+ }));
6714
7195
  } else if (table === `runner_runtime_diagnostics`) {
6715
7196
  target.searchParams.set(`columns`, `"tenant_id","runner_id","owner_principal","wake_stream_offset","last_seen_at","liveness_lease_expires_at","diagnostics","updated_at"`);
6716
7197
  applyTenantShapeWhere(target, options.tenantId, [`owner_principal = ${sqlStringLiteral(options.principalUrl ?? ``)}`]);
6717
7198
  } else if (table === `entity_dispatch_state`) {
6718
7199
  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"`);
6719
- applyTenantShapeWhere(target, options.tenantId);
7200
+ applyShapeWhere(target, buildReadableEntityUrlWhere({
7201
+ tenantId: options.tenantId,
7202
+ principalUrl: options.principalUrl ?? ``,
7203
+ principalKind: options.principalKind ?? ``,
7204
+ permissionBypass: options.permissionBypass
7205
+ }));
6720
7206
  } else if (table === `wake_notifications`) {
6721
7207
  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"`);
6722
- applyTenantShapeWhere(target, options.tenantId);
7208
+ applyShapeWhere(target, buildReadableEntityUrlWhere({
7209
+ tenantId: options.tenantId,
7210
+ principalUrl: options.principalUrl ?? ``,
7211
+ principalKind: options.principalKind ?? ``,
7212
+ permissionBypass: options.permissionBypass
7213
+ }));
6723
7214
  } else if (table === `consumer_claims`) {
6724
7215
  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"`);
6725
- applyTenantShapeWhere(target, options.tenantId);
7216
+ applyShapeWhere(target, buildReadableEntityUrlWhere({
7217
+ tenantId: options.tenantId,
7218
+ principalUrl: options.principalUrl ?? ``,
7219
+ principalKind: options.principalKind ?? ``,
7220
+ permissionBypass: options.permissionBypass
7221
+ }));
6726
7222
  }
6727
7223
  return target;
6728
7224
  }
7225
+ function buildReadableEntitiesWhere(options) {
7226
+ const tenant = sqlStringLiteral(options.tenantId);
7227
+ if (options.permissionBypass) return `tenant_id = ${tenant}`;
7228
+ const principalUrl$1 = sqlStringLiteral(options.principalUrl);
7229
+ const principalKind = sqlStringLiteral(options.principalKind);
7230
+ return [
7231
+ `tenant_id = ${tenant}`,
7232
+ `AND (`,
7233
+ ` created_by = ${principalUrl$1}`,
7234
+ ` OR url IN (`,
7235
+ ` SELECT entity_url`,
7236
+ ` FROM entity_effective_permissions`,
7237
+ ` WHERE tenant_id = ${tenant}`,
7238
+ ` AND permission IN ('read', 'manage')`,
7239
+ ` AND (`,
7240
+ ` (subject_kind = 'principal' AND subject_value = ${principalUrl$1})`,
7241
+ ` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
7242
+ ` )`,
7243
+ ` )`,
7244
+ `)`
7245
+ ].join(`\n`);
7246
+ }
7247
+ function buildReadableEntityUrlWhere(options) {
7248
+ const tenant = sqlStringLiteral(options.tenantId);
7249
+ if (options.permissionBypass) return `tenant_id = ${tenant}`;
7250
+ return [
7251
+ `tenant_id = ${tenant}`,
7252
+ `AND entity_url IN (`,
7253
+ ` SELECT url`,
7254
+ ` FROM entities`,
7255
+ ` WHERE ${indentWhere(buildReadableEntitiesWhere(options), ` `).trimStart()}`,
7256
+ `)`
7257
+ ].join(`\n`);
7258
+ }
7259
+ function buildCurrentPrincipalEntityEffectivePermissionsWhere(options) {
7260
+ const tenant = sqlStringLiteral(options.tenantId);
7261
+ if (options.permissionBypass) return `tenant_id = ${tenant}`;
7262
+ const principalUrl$1 = sqlStringLiteral(options.principalUrl);
7263
+ const principalKind = sqlStringLiteral(options.principalKind);
7264
+ return [
7265
+ `tenant_id = ${tenant}`,
7266
+ `AND (`,
7267
+ ` (subject_kind = 'principal' AND subject_value = ${principalUrl$1})`,
7268
+ ` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
7269
+ `)`,
7270
+ `AND entity_url IN (`,
7271
+ ` SELECT url`,
7272
+ ` FROM entities`,
7273
+ ` WHERE ${buildReadableEntitiesWhere(options)}`,
7274
+ `)`
7275
+ ].join(`\n`);
7276
+ }
7277
+ function buildSpawnableEntityTypesWhere(options) {
7278
+ const tenant = sqlStringLiteral(options.tenantId);
7279
+ if (options.permissionBypass) return `tenant_id = ${tenant}`;
7280
+ const principalUrl$1 = sqlStringLiteral(options.principalUrl);
7281
+ const principalKind = sqlStringLiteral(options.principalKind);
7282
+ return [
7283
+ `tenant_id = ${tenant}`,
7284
+ `AND name IN (`,
7285
+ ` SELECT entity_type`,
7286
+ ` FROM entity_type_permission_grants`,
7287
+ ` WHERE tenant_id = ${tenant}`,
7288
+ ` AND permission IN ('spawn', 'manage')`,
7289
+ ` AND (`,
7290
+ ` (subject_kind = 'principal' AND subject_value = ${principalUrl$1})`,
7291
+ ` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
7292
+ ` )`,
7293
+ `)`
7294
+ ].join(`\n`);
7295
+ }
6729
7296
  async function forwardFetchRequest(options) {
6730
7297
  const routingAdapter = resolveDurableStreamsRoutingAdapter(options.durableStreamsRouting, options.durableStreamsUrl);
6731
7298
  const routingInput = {
@@ -6760,13 +7327,170 @@ function decodeJsonObject(body) {
6760
7327
  return null;
6761
7328
  }
6762
7329
  function applyTenantShapeWhere(target, tenantId, extraConditions = []) {
6763
- const tenantWhere = [`tenant_id = ${sqlStringLiteral(tenantId)}`, ...extraConditions].join(` AND `);
7330
+ applyShapeWhere(target, [`tenant_id = ${sqlStringLiteral(tenantId)}`, ...extraConditions].join(` AND `));
7331
+ }
7332
+ function applyShapeWhere(target, enforcedWhere) {
6764
7333
  const existingWhere = target.searchParams.get(`where`);
6765
- target.searchParams.set(`where`, existingWhere ? `${tenantWhere} AND (${existingWhere})` : tenantWhere);
7334
+ target.searchParams.set(`where`, existingWhere ? `${enforcedWhere} AND (${existingWhere})` : enforcedWhere);
6766
7335
  }
6767
7336
  function sqlStringLiteral(value) {
6768
7337
  return `'${value.replace(/'/g, `''`)}'`;
6769
7338
  }
7339
+ function indentWhere(where, prefix) {
7340
+ return where.split(`\n`).map((line) => `${prefix}${line}`).join(`\n`);
7341
+ }
7342
+
7343
+ //#endregion
7344
+ //#region src/permissions.ts
7345
+ const authzDecisionCache = new WeakMap();
7346
+ function principalSubject(principal) {
7347
+ return {
7348
+ principalUrl: principal.url,
7349
+ principalKind: principal.kind
7350
+ };
7351
+ }
7352
+ function isPermissionBypassPrincipal(ctx) {
7353
+ return isBuiltInSystemPrincipalUrl(ctx.principal.url);
7354
+ }
7355
+ async function canAccessEntity(ctx, entity, permission, request) {
7356
+ if (isPermissionBypassPrincipal(ctx)) return true;
7357
+ await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
7358
+ const builtInAllowed = entity.created_by === ctx.principal.url || await ctx.entityManager.registry.hasEntityPermission(entity.url, permission, principalSubject(ctx.principal));
7359
+ return await applyAuthorizationHook(ctx, {
7360
+ verb: permission,
7361
+ resourceKey: `entity:${entity.url}`,
7362
+ resource: {
7363
+ kind: `entity`,
7364
+ entity
7365
+ },
7366
+ builtInAllowed,
7367
+ request
7368
+ });
7369
+ }
7370
+ async function canAccessEntityType(ctx, entityType, permission, request) {
7371
+ if (isPermissionBypassPrincipal(ctx)) return true;
7372
+ await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
7373
+ const builtInAllowed = await ctx.entityManager.registry.hasEntityTypePermission(entityType.name, permission, principalSubject(ctx.principal));
7374
+ return await applyAuthorizationHook(ctx, {
7375
+ verb: permission,
7376
+ resourceKey: `entity_type:${entityType.name}`,
7377
+ resource: {
7378
+ kind: `entity_type`,
7379
+ entityType
7380
+ },
7381
+ builtInAllowed,
7382
+ request
7383
+ });
7384
+ }
7385
+ async function canRegisterEntityType(ctx, input, request) {
7386
+ if (isPermissionBypassPrincipal(ctx)) return true;
7387
+ return await applyAuthorizationHook(ctx, {
7388
+ verb: `manage`,
7389
+ resourceKey: `entity_type_registration:${input.name}`,
7390
+ resource: {
7391
+ kind: `entity_type_registration`,
7392
+ entityTypeName: input.name
7393
+ },
7394
+ builtInAllowed: true,
7395
+ request
7396
+ });
7397
+ }
7398
+ async function canAccessSharedState(ctx, sharedStateId, permission, request, ownerEntityUrl) {
7399
+ if (isPermissionBypassPrincipal(ctx)) return true;
7400
+ await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
7401
+ const storedLinkedEntityUrls = await ctx.entityManager.registry.listSharedStateLinkedEntityUrls(sharedStateId);
7402
+ const bootstrapEntityUrls = storedLinkedEntityUrls.length === 0 && ownerEntityUrl ? [ownerEntityUrl] : [];
7403
+ const linkedEntityUrls = [...new Set([...storedLinkedEntityUrls, ...bootstrapEntityUrls])];
7404
+ for (const entityUrl of linkedEntityUrls) {
7405
+ const entity = await ctx.entityManager.registry.getEntity(entityUrl);
7406
+ if (!entity) continue;
7407
+ if (entity.created_by === ctx.principal.url || await ctx.entityManager.registry.hasEntityPermission(entity.url, permission, principalSubject(ctx.principal))) return await applyAuthorizationHook(ctx, {
7408
+ verb: permission,
7409
+ resourceKey: `shared_state:${sharedStateId}`,
7410
+ resource: {
7411
+ kind: `shared_state`,
7412
+ sharedStateId,
7413
+ linkedEntityUrls
7414
+ },
7415
+ builtInAllowed: true,
7416
+ request
7417
+ });
7418
+ }
7419
+ return await applyAuthorizationHook(ctx, {
7420
+ verb: permission,
7421
+ resourceKey: `shared_state:${sharedStateId}`,
7422
+ resource: {
7423
+ kind: `shared_state`,
7424
+ sharedStateId,
7425
+ linkedEntityUrls
7426
+ },
7427
+ builtInAllowed: false,
7428
+ request
7429
+ });
7430
+ }
7431
+ async function applyAuthorizationHook(ctx, input) {
7432
+ const hook = ctx.authorizeRequest;
7433
+ if (!hook) return input.builtInAllowed;
7434
+ const cacheKey = [
7435
+ ctx.service,
7436
+ ctx.principal.url,
7437
+ input.verb,
7438
+ input.resourceKey
7439
+ ].join(`|`);
7440
+ const cached = getCachedDecision(hook, cacheKey);
7441
+ if (cached) return cached.decision === `allow`;
7442
+ let decision;
7443
+ try {
7444
+ decision = await hook({
7445
+ tenant: ctx.service,
7446
+ principal: ctx.principal,
7447
+ verb: input.verb,
7448
+ resource: input.resource,
7449
+ request: input.request ? requestMetadata(input.request) : void 0,
7450
+ builtInAllowed: input.builtInAllowed
7451
+ });
7452
+ } catch (error) {
7453
+ serverLog.warn(`[agent-server] authorization hook failed:`, error);
7454
+ return false;
7455
+ }
7456
+ cacheDecision(hook, cacheKey, decision);
7457
+ return decision.decision === `allow`;
7458
+ }
7459
+ function getCachedDecision(hook, cacheKey) {
7460
+ const cache = authzDecisionCache.get(hook);
7461
+ const entry = cache?.get(cacheKey);
7462
+ if (!entry) return null;
7463
+ if (entry.expiresAt <= Date.now()) {
7464
+ cache?.delete(cacheKey);
7465
+ return null;
7466
+ }
7467
+ return { decision: entry.decision };
7468
+ }
7469
+ function cacheDecision(hook, cacheKey, decision) {
7470
+ if (!decision.expires_at) return;
7471
+ const expiresAt = Date.parse(decision.expires_at);
7472
+ if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) return;
7473
+ let cache = authzDecisionCache.get(hook);
7474
+ if (!cache) {
7475
+ cache = new Map();
7476
+ authzDecisionCache.set(hook, cache);
7477
+ }
7478
+ cache.set(cacheKey, {
7479
+ decision: decision.decision,
7480
+ expiresAt
7481
+ });
7482
+ }
7483
+ function requestMetadata(request) {
7484
+ const headers = {};
7485
+ request.headers.forEach((value, key) => {
7486
+ headers[key] = value;
7487
+ });
7488
+ return {
7489
+ method: request.method,
7490
+ url: request.url,
7491
+ headers
7492
+ };
7493
+ }
6770
7494
 
6771
7495
  //#endregion
6772
7496
  //#region src/webhook-signing.ts
@@ -6858,6 +7582,7 @@ const subscriptionControlActions = [
6858
7582
  `ack`,
6859
7583
  `release`
6860
7584
  ];
7585
+ const SHARED_STATE_OWNER_ENTITY_HEADER = `electric-owner-entity`;
6861
7586
  const durableStreamsRouter = (0, itty_router.Router)();
6862
7587
  durableStreamsRouter.put(`/__ds/subscriptions/:subscriptionId`, putSubscriptionBase);
6863
7588
  durableStreamsRouter.get(`/__ds/subscriptions/:subscriptionId`, getSubscriptionBase);
@@ -7075,6 +7800,8 @@ async function webhookJwks(_request, ctx) {
7075
7800
  });
7076
7801
  }
7077
7802
  async function streamAppend(request, ctx) {
7803
+ const auth = await authorizeDurableStreamAccess(request, ctx);
7804
+ if (auth) return auth;
7078
7805
  return await electricAgentsStreamAppendRouter.fetch(createStreamAppendRouteRequest(request), ctx.runtime, (req, body) => forwardFetchRequest({
7079
7806
  request: {
7080
7807
  method: req.method,
@@ -7091,8 +7818,9 @@ async function streamAppend(request, ctx) {
7091
7818
  }));
7092
7819
  }
7093
7820
  async function proxyPassThrough(request, ctx) {
7821
+ const auth = await authorizeDurableStreamAccess(request, ctx);
7822
+ if (auth) return auth;
7094
7823
  const streamPath = new URL(request.url).pathname;
7095
- if (ctx.entityManager?.isAttachmentStreamPath(streamPath)) return new Response(null, { status: 404 });
7096
7824
  const upstream = await forwardToDurableStreams(ctx, request);
7097
7825
  const method = request.method.toUpperCase();
7098
7826
  const endTrackedRead = method === `GET` ? await ctx.entityBridgeManager.beginClientRead(streamPath) : null;
@@ -7103,6 +7831,51 @@ async function proxyPassThrough(request, ctx) {
7103
7831
  await endTrackedRead?.();
7104
7832
  }
7105
7833
  }
7834
+ async function authorizeDurableStreamAccess(request, ctx) {
7835
+ const method = request.method.toUpperCase();
7836
+ const streamPath = new URL(request.url).pathname;
7837
+ if (method === `GET` || method === `HEAD`) {
7838
+ const registry = ctx.entityManager?.registry;
7839
+ const entity = registry?.getEntityByStream ? await registry.getEntityByStream(streamPath) : null;
7840
+ if (entity) {
7841
+ if (await canAccessEntity(ctx, entity, `read`, request)) return void 0;
7842
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to read ${entity.url}`);
7843
+ }
7844
+ const attachmentEntityUrl = entityUrlFromAttachmentStreamPath(streamPath);
7845
+ if (attachmentEntityUrl) {
7846
+ const attachmentEntity = registry?.getEntity ? await registry.getEntity(attachmentEntityUrl) : null;
7847
+ if (!attachmentEntity) return apiError(404, ErrCodeNotFound, `Entity not found`);
7848
+ if (await canAccessEntity(ctx, attachmentEntity, `read`, request)) return void 0;
7849
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to read ${attachmentEntity.url}`);
7850
+ }
7851
+ }
7852
+ const sharedStateId = sharedStateIdFromPath(streamPath);
7853
+ if (!sharedStateId) return void 0;
7854
+ if (method === `GET` || method === `HEAD`) {
7855
+ if (await canAccessSharedState(ctx, sharedStateId, `read`, request)) return void 0;
7856
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to read shared state`);
7857
+ }
7858
+ if (method === `PUT` || method === `POST`) {
7859
+ const ownerEntityUrl = request.headers.get(SHARED_STATE_OWNER_ENTITY_HEADER)?.trim() || void 0;
7860
+ if (await canAccessSharedState(ctx, sharedStateId, `write`, request, ownerEntityUrl)) return void 0;
7861
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to write shared state`);
7862
+ }
7863
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to access shared state`);
7864
+ }
7865
+ function entityUrlFromAttachmentStreamPath(path$2) {
7866
+ const match = path$2.match(/^\/([^/]+)\/([^/]+)\/attachments\/[^/]+$/);
7867
+ if (!match) return null;
7868
+ return `/${match[1]}/${match[2]}`;
7869
+ }
7870
+ function sharedStateIdFromPath(path$2) {
7871
+ const match = path$2.match(/^\/_electric\/shared-state\/([^/]+)$/);
7872
+ if (!match) return null;
7873
+ try {
7874
+ return decodeURIComponent(match[1]);
7875
+ } catch {
7876
+ return match[1];
7877
+ }
7878
+ }
7106
7879
 
7107
7880
  //#endregion
7108
7881
  //#region src/routing/electric-proxy-router.ts
@@ -7110,12 +7883,15 @@ const electricProxyRouter = (0, itty_router.Router)({ base: `/_electric/electric
7110
7883
  electricProxyRouter.get(`/*`, proxyElectric);
7111
7884
  async function proxyElectric(request, ctx) {
7112
7885
  if (!ctx.electricUrl) return apiError(500, `ELECTRIC_PROXY_FAILED`, `Electric URL not configured`);
7886
+ await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
7113
7887
  const target = buildElectricProxyTarget({
7114
7888
  incomingUrl: new URL(request.url),
7115
7889
  electricUrl: ctx.electricUrl,
7116
7890
  electricSecret: ctx.electricSecret,
7117
7891
  tenantId: ctx.service,
7118
- principalUrl: ctx.principal.url
7892
+ principalUrl: ctx.principal.url,
7893
+ principalKind: ctx.principal.kind,
7894
+ permissionBypass: isPermissionBypassPrincipal(ctx)
7119
7895
  });
7120
7896
  const headers = new Headers(request.headers);
7121
7897
  headers.delete(`host`);
@@ -7174,6 +7950,27 @@ const wakeConditionSchema = __sinclair_typebox.Type.Union([__sinclair_typebox.Ty
7174
7950
  __sinclair_typebox.Type.Literal(`delete`)
7175
7951
  ])))
7176
7952
  })]);
7953
+ const permissionSubjectSchema = __sinclair_typebox.Type.Object({
7954
+ subject_kind: __sinclair_typebox.Type.Union([__sinclair_typebox.Type.Literal(`principal`), __sinclair_typebox.Type.Literal(`principal_kind`)]),
7955
+ subject_value: __sinclair_typebox.Type.String()
7956
+ }, { additionalProperties: false });
7957
+ const entityPermissionSchema = __sinclair_typebox.Type.Union([
7958
+ __sinclair_typebox.Type.Literal(`read`),
7959
+ __sinclair_typebox.Type.Literal(`write`),
7960
+ __sinclair_typebox.Type.Literal(`delete`),
7961
+ __sinclair_typebox.Type.Literal(`signal`),
7962
+ __sinclair_typebox.Type.Literal(`fork`),
7963
+ __sinclair_typebox.Type.Literal(`schedule`),
7964
+ __sinclair_typebox.Type.Literal(`spawn`),
7965
+ __sinclair_typebox.Type.Literal(`manage`)
7966
+ ]);
7967
+ const entityPermissionGrantInputSchema = __sinclair_typebox.Type.Object({
7968
+ ...permissionSubjectSchema.properties,
7969
+ permission: entityPermissionSchema,
7970
+ propagation: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Union([__sinclair_typebox.Type.Literal(`self`), __sinclair_typebox.Type.Literal(`descendants`)])),
7971
+ copy_to_children: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Boolean()),
7972
+ expires_at: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
7973
+ }, { additionalProperties: false });
7177
7974
  const spawnBodySchema = __sinclair_typebox.Type.Object({
7178
7975
  args: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown())),
7179
7976
  tags: __sinclair_typebox.Type.Optional(stringRecordSchema$1),
@@ -7181,6 +7978,8 @@ const spawnBodySchema = __sinclair_typebox.Type.Object({
7181
7978
  dispatch_policy: __sinclair_typebox.Type.Optional(dispatchPolicySchema),
7182
7979
  sandbox: __sinclair_typebox.Type.Optional(sandboxChoiceSchema),
7183
7980
  initialMessage: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Unknown()),
7981
+ grants: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(entityPermissionGrantInputSchema)),
7982
+ initialMessageType: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
7184
7983
  wake: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Object({
7185
7984
  subscriberUrl: __sinclair_typebox.Type.String(),
7186
7985
  condition: wakeConditionSchema,
@@ -7202,8 +8001,22 @@ const sendBodySchema = __sinclair_typebox.Type.Object({
7202
8001
  ])),
7203
8002
  position: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
7204
8003
  afterMs: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
7205
- from: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
8004
+ from: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
8005
+ from_principal: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
8006
+ from_agent: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
7206
8007
  });
8008
+ function agentUrlForPrincipal(principal) {
8009
+ if (principal.kind === `agent`) return `/${principal.id}`;
8010
+ if (principal.key.startsWith(`entity:`)) return `/${principal.key.slice(`entity:`.length)}`;
8011
+ return null;
8012
+ }
8013
+ function agentUrlPath(value) {
8014
+ try {
8015
+ return new URL(value).pathname;
8016
+ } catch {
8017
+ return value;
8018
+ }
8019
+ }
7207
8020
  const inboxMessageBodySchema = __sinclair_typebox.Type.Object({
7208
8021
  payload: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Unknown()),
7209
8022
  position: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
@@ -7282,24 +8095,27 @@ const attachmentSubjectTypes = new Set([
7282
8095
  ]);
7283
8096
  const entitiesRouter = (0, itty_router.Router)({ base: `/_electric/entities` });
7284
8097
  entitiesRouter.get(`/`, listEntities);
7285
- entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spawnBodySchema), spawnEntity);
7286
- entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, getEntity);
7287
- entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity);
7288
- entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, killEntity);
7289
- entitiesRouter.post(`/:type/:instanceId/signal`, withExistingEntity, withSchema(signalBodySchema), signalEntity);
7290
- entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), sendEntity);
7291
- entitiesRouter.post(`/:type/:instanceId/attachments`, withExistingEntity, createAttachment);
7292
- entitiesRouter.get(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, readAttachment);
7293
- entitiesRouter.delete(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, deleteAttachment);
7294
- entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), updateInboxMessage);
7295
- entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, deleteInboxMessage);
7296
- entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), forkEntity);
7297
- entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), setTag);
7298
- entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, deleteTag);
7299
- entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), upsertSchedule);
7300
- entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, deleteSchedule);
7301
- entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), upsertEventSourceSubscription);
7302
- entitiesRouter.delete(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, deleteEventSourceSubscription);
8098
+ entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spawnBodySchema), withSpawnPermission, spawnEntity);
8099
+ entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`read`), getEntity);
8100
+ entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`read`), headEntity);
8101
+ entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`delete`), killEntity);
8102
+ entitiesRouter.post(`/:type/:instanceId/signal`, withExistingEntity, withSchema(signalBodySchema), withEntityPermission(`signal`), signalEntity);
8103
+ entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), withEntityPermission(`write`), sendEntity);
8104
+ entitiesRouter.post(`/:type/:instanceId/attachments`, withExistingEntity, withEntityPermission(`write`), createAttachment);
8105
+ entitiesRouter.get(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, withEntityPermission(`read`), readAttachment);
8106
+ entitiesRouter.delete(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, withEntityPermission(`write`), deleteAttachment);
8107
+ entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), withEntityPermission(`write`), updateInboxMessage);
8108
+ entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withEntityPermission(`write`), deleteInboxMessage);
8109
+ entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), withEntityPermission(`fork`), forkEntity);
8110
+ entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), withEntityPermission(`write`), setTag);
8111
+ entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withEntityPermission(`write`), deleteTag);
8112
+ entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), withEntityPermission(`schedule`), upsertSchedule);
8113
+ entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withEntityPermission(`schedule`), deleteSchedule);
8114
+ entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), withEntityPermission(`write`), upsertEventSourceSubscription);
8115
+ entitiesRouter.delete(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withEntityPermission(`write`), deleteEventSourceSubscription);
8116
+ entitiesRouter.get(`/:type/:instanceId/grants`, withExistingEntity, withEntityPermission(`manage`), listEntityPermissionGrants);
8117
+ entitiesRouter.post(`/:type/:instanceId/grants`, withExistingEntity, withSchema(entityPermissionGrantInputSchema), withEntityPermission(`manage`), createEntityPermissionGrant);
8118
+ entitiesRouter.delete(`/:type/:instanceId/grants/:grantId`, withExistingEntity, withEntityPermission(`manage`), deleteEntityPermissionGrant);
7303
8119
  function entityUrlFromSegments(type, instanceId) {
7304
8120
  if (!type || !instanceId) return null;
7305
8121
  if (type.startsWith(`_`) || type.includes(`*`) || instanceId.includes(`*`)) return null;
@@ -7398,6 +8214,17 @@ function rejectPrincipalEntityMutation(request, action) {
7398
8214
  if (entity.type !== `principal`) return void 0;
7399
8215
  return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be ${action}`);
7400
8216
  }
8217
+ function parseExpiresAt$1(value) {
8218
+ if (value === void 0) return void 0;
8219
+ const expiresAt = new Date(value);
8220
+ if (Number.isNaN(expiresAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid expires_at timestamp`, 400);
8221
+ return expiresAt;
8222
+ }
8223
+ function parseGrantId$1(request) {
8224
+ const grantId = Number.parseInt(String(request.params.grantId), 10);
8225
+ if (!Number.isSafeInteger(grantId) || grantId <= 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid grant id`, 400);
8226
+ return grantId;
8227
+ }
7401
8228
  async function withExistingEntity(request, ctx) {
7402
8229
  const entityUrl = entityUrlFromSegments(request.params.type, request.params.instanceId);
7403
8230
  if (!entityUrl) return void 0;
@@ -7428,17 +8255,76 @@ async function withSpawnableEntityType(request, ctx) {
7428
8255
  if (request.params.type === `principal`) return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be spawned directly`);
7429
8256
  const entityType = await ctx.entityManager.registry.getEntityType(request.params.type);
7430
8257
  if (!entityType) return apiError(404, ErrCodeUnknownEntityType, `Entity type "${request.params.type}" not found`);
8258
+ request.spawnRoute = { entityType };
7431
8259
  return void 0;
7432
8260
  }
8261
+ function withEntityPermission(permission) {
8262
+ return async (request, ctx) => {
8263
+ const { entity } = requireExistingEntityRoute(request);
8264
+ if (await canAccessEntity(ctx, entity, permission, request)) return void 0;
8265
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to ${permission} ${entity.url}`);
8266
+ };
8267
+ }
8268
+ async function withSpawnPermission(request, ctx) {
8269
+ const parsed = routeBody(request);
8270
+ const entityType = request.spawnRoute?.entityType;
8271
+ if (!entityType) throw new Error(`spawnable entity type middleware did not run`);
8272
+ if (!await canAccessEntityType(ctx, entityType, `spawn`, request)) return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn ${entityType.name}`);
8273
+ if (!parsed.parent) return void 0;
8274
+ const parent = await ctx.entityManager.registry.getEntity(parsed.parent);
8275
+ if (!parent) return apiError(404, ErrCodeNotFound, `Parent entity not found`);
8276
+ if (await canAccessEntity(ctx, parent, `spawn`, request)) return await validateParentedSpawnGrants(request, ctx, parent, parsed);
8277
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn children from ${parent.url}`);
8278
+ }
8279
+ async function validateParentedSpawnGrants(request, ctx, parent, parsed) {
8280
+ const needsParentManage = (parsed.grants ?? []).some(requiresParentManageForInitialGrant);
8281
+ if (!needsParentManage) return void 0;
8282
+ if (await canAccessEntity(ctx, parent, `manage`, request)) return void 0;
8283
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to delegate broad grants from ${parent.url}`);
8284
+ }
8285
+ function requiresParentManageForInitialGrant(grant) {
8286
+ return grant.permission === `manage` || grant.subject_kind === `principal_kind` || grant.propagation === `descendants` || grant.copy_to_children === true;
8287
+ }
7433
8288
  async function listEntities({ query }, ctx) {
7434
8289
  const { entities: entities$1 } = await ctx.entityManager.registry.listEntities({
7435
8290
  type: firstQueryValue$1(query.type),
7436
8291
  status: firstQueryValue$1(query.status),
7437
8292
  parent: firstQueryValue$1(query.parent),
7438
- created_by: firstQueryValue$1(query.created_by)
8293
+ created_by: firstQueryValue$1(query.created_by),
8294
+ readableBy: {
8295
+ ...principalSubject(ctx.principal),
8296
+ bypass: isPermissionBypassPrincipal(ctx)
8297
+ }
7439
8298
  });
7440
8299
  return (0, itty_router.json)(entities$1.map((entity) => toPublicEntity(entity)));
7441
8300
  }
8301
+ async function listEntityPermissionGrants(request, ctx) {
8302
+ const { entityUrl } = requireExistingEntityRoute(request);
8303
+ const grants = await ctx.entityManager.registry.listEntityPermissionGrants(entityUrl);
8304
+ return (0, itty_router.json)({ grants });
8305
+ }
8306
+ async function createEntityPermissionGrant(request, ctx) {
8307
+ const { entityUrl } = requireExistingEntityRoute(request);
8308
+ const parsed = routeBody(request);
8309
+ const grant = await ctx.entityManager.registry.createEntityPermissionGrant({
8310
+ entityUrl,
8311
+ permission: parsed.permission,
8312
+ subjectKind: parsed.subject_kind,
8313
+ subjectValue: parsed.subject_value,
8314
+ propagation: parsed.propagation,
8315
+ copyToChildren: parsed.copy_to_children,
8316
+ expiresAt: parseExpiresAt$1(parsed.expires_at),
8317
+ createdBy: ctx.principal.url
8318
+ });
8319
+ await ctx.entityBridgeManager.onEntityChanged(entityUrl);
8320
+ return (0, itty_router.json)(grant, { status: 201 });
8321
+ }
8322
+ async function deleteEntityPermissionGrant(request, ctx) {
8323
+ const { entityUrl } = requireExistingEntityRoute(request);
8324
+ const deleted = await ctx.entityManager.registry.deleteEntityPermissionGrant(entityUrl, parseGrantId$1(request));
8325
+ if (deleted) await ctx.entityBridgeManager.onEntityChanged(entityUrl);
8326
+ return deleted ? (0, itty_router.status)(204) : apiError(404, ErrCodeNotFound, `Grant not found`);
8327
+ }
7442
8328
  async function upsertSchedule(request, ctx) {
7443
8329
  const principalMutationError = rejectPrincipalEntityMutation(request, `scheduled`);
7444
8330
  if (principalMutationError) return principalMutationError;
@@ -7544,6 +8430,7 @@ async function forkEntity(request, ctx) {
7544
8430
  const result = await ctx.entityManager.forkSubtree(entityUrl, {
7545
8431
  rootInstanceId: parsed.instance_id,
7546
8432
  waitTimeoutMs: parsed.waitTimeoutMs,
8433
+ createdBy: ctx.principal.url,
7547
8434
  ...parsed.fork_pointer && { forkPointer: {
7548
8435
  offset: parsed.fork_pointer.offset,
7549
8436
  subOffset: parsed.fork_pointer.sub_offset
@@ -7559,26 +8446,27 @@ async function sendEntity(request, ctx) {
7559
8446
  const parsed = routeBody(request);
7560
8447
  const principal = ctx.principal;
7561
8448
  if (parsed.from !== void 0 && parsed.from !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from must match Electric-Principal`);
8449
+ if (parsed.from_principal !== void 0 && parsed.from_principal !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from_principal must match Electric-Principal`);
8450
+ if (parsed.from_agent !== void 0) {
8451
+ const principalAgentUrl = agentUrlForPrincipal(principal);
8452
+ if (agentUrlPath(parsed.from_agent) !== principalAgentUrl) return apiError(400, ErrCodeInvalidRequest, `Request from_agent must match authenticated agent principal`);
8453
+ }
7562
8454
  await ctx.entityManager.ensurePrincipal(principal);
7563
8455
  const { entityUrl, entity } = requireExistingEntityRoute(request);
7564
8456
  const dispatchEntity = entity.dispatch_policy ? entity : await backfillEntityDispatchPolicy(ctx, entity);
7565
8457
  await linkEntityDispatchSubscription(ctx, dispatchEntity);
7566
- if (parsed.afterMs && parsed.afterMs > 0) await ctx.entityManager.enqueueDelayedSend(entityUrl, {
7567
- from: principal.url,
7568
- payload: parsed.payload,
7569
- key: parsed.key,
7570
- type: parsed.type,
7571
- mode: parsed.mode,
7572
- position: parsed.position
7573
- }, new Date(Date.now() + parsed.afterMs));
7574
- else await ctx.entityManager.send(entityUrl, {
8458
+ const sendReq = {
7575
8459
  from: principal.url,
8460
+ from_principal: principal.url,
8461
+ from_agent: parsed.from_agent,
7576
8462
  payload: parsed.payload,
7577
8463
  key: parsed.key,
7578
8464
  type: parsed.type,
7579
8465
  mode: parsed.mode,
7580
8466
  position: parsed.position
7581
- });
8467
+ };
8468
+ if (parsed.afterMs && parsed.afterMs > 0) await ctx.entityManager.enqueueDelayedSend(entityUrl, sendReq, new Date(Date.now() + parsed.afterMs));
8469
+ else await ctx.entityManager.send(entityUrl, sendReq);
7582
8470
  return (0, itty_router.status)(204);
7583
8471
  }
7584
8472
  async function createAttachment(request, ctx) {
@@ -7647,14 +8535,27 @@ async function spawnEntity(request, ctx) {
7647
8535
  dispatch_policy: dispatchPolicy,
7648
8536
  sandbox: parsed.sandbox,
7649
8537
  initialMessage: void 0,
8538
+ initialMessageType: void 0,
7650
8539
  wake: parsed.wake,
7651
8540
  created_by: principal.url
7652
8541
  });
8542
+ if (parsed.parent) await ctx.entityManager.registry.copyEntityPermissionGrantsForSpawn(parsed.parent, entity.url, principal.url);
8543
+ for (const grant of parsed.grants ?? []) await ctx.entityManager.registry.createEntityPermissionGrant({
8544
+ entityUrl: entity.url,
8545
+ permission: grant.permission,
8546
+ subjectKind: grant.subject_kind,
8547
+ subjectValue: grant.subject_value,
8548
+ propagation: grant.propagation,
8549
+ copyToChildren: grant.copy_to_children,
8550
+ expiresAt: parseExpiresAt$1(grant.expires_at),
8551
+ createdBy: principal.url
8552
+ });
7653
8553
  const linkBeforeInitialMessage = parsed.initialMessage !== void 0 && shouldLinkDispatchBeforeInitialMessage(dispatchPolicy);
7654
8554
  if (linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
7655
8555
  if (parsed.initialMessage !== void 0) await ctx.entityManager.send(entity.url, {
7656
8556
  from: principal.url,
7657
- payload: parsed.initialMessage
8557
+ payload: parsed.initialMessage,
8558
+ type: parsed.initialMessageType
7658
8559
  });
7659
8560
  if (!linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
7660
8561
  return (0, itty_router.json)({
@@ -7701,14 +8602,37 @@ async function signalEntity(request, ctx) {
7701
8602
  //#region src/routing/entity-types-router.ts
7702
8603
  const jsonObjectSchema = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown());
7703
8604
  const schemaMapSchema = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), jsonObjectSchema);
8605
+ const slashCommandArgumentSchema = __sinclair_typebox.Type.Object({
8606
+ name: __sinclair_typebox.Type.String(),
8607
+ type: __sinclair_typebox.Type.Union([
8608
+ __sinclair_typebox.Type.Literal(`string`),
8609
+ __sinclair_typebox.Type.Literal(`number`),
8610
+ __sinclair_typebox.Type.Literal(`boolean`)
8611
+ ]),
8612
+ required: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Boolean()),
8613
+ description: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
8614
+ }, { additionalProperties: false });
8615
+ const slashCommandSchema = __sinclair_typebox.Type.Object({
8616
+ name: __sinclair_typebox.Type.String(),
8617
+ description: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
8618
+ arguments: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(slashCommandArgumentSchema))
8619
+ }, { additionalProperties: false });
8620
+ const typePermissionGrantInputSchema = __sinclair_typebox.Type.Object({
8621
+ subject_kind: __sinclair_typebox.Type.Union([__sinclair_typebox.Type.Literal(`principal`), __sinclair_typebox.Type.Literal(`principal_kind`)]),
8622
+ subject_value: __sinclair_typebox.Type.String(),
8623
+ permission: __sinclair_typebox.Type.Union([__sinclair_typebox.Type.Literal(`spawn`), __sinclair_typebox.Type.Literal(`manage`)]),
8624
+ expires_at: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
8625
+ }, { additionalProperties: false });
7704
8626
  const registerEntityTypeBodySchema = __sinclair_typebox.Type.Object({
7705
8627
  name: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
7706
8628
  description: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
7707
8629
  creation_schema: __sinclair_typebox.Type.Optional(jsonObjectSchema),
7708
8630
  inbox_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
7709
8631
  state_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
8632
+ slash_commands: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(slashCommandSchema)),
7710
8633
  serve_endpoint: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
7711
- default_dispatch_policy: __sinclair_typebox.Type.Optional(dispatchPolicySchema)
8634
+ default_dispatch_policy: __sinclair_typebox.Type.Optional(dispatchPolicySchema),
8635
+ permission_grants: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(typePermissionGrantInputSchema))
7712
8636
  }, { additionalProperties: false });
7713
8637
  const amendEntityTypeSchemasBodySchema = __sinclair_typebox.Type.Object({
7714
8638
  inbox_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
@@ -7716,20 +8640,56 @@ const amendEntityTypeSchemasBodySchema = __sinclair_typebox.Type.Object({
7716
8640
  }, { additionalProperties: false });
7717
8641
  const entityTypesRouter = (0, itty_router.Router)({ base: `/_electric/entity-types` });
7718
8642
  entityTypesRouter.get(`/`, listEntityTypes);
7719
- entityTypesRouter.post(`/`, withSchema(registerEntityTypeBodySchema), registerEntityType);
7720
- entityTypesRouter.patch(`/:name/schemas`, withSchema(amendEntityTypeSchemasBodySchema), amendSchemas);
7721
- entityTypesRouter.get(`/:name`, getEntityType);
7722
- entityTypesRouter.delete(`/:name`, deleteEntityType);
8643
+ entityTypesRouter.post(`/`, withSchema(registerEntityTypeBodySchema), withEntityTypeRegistrationPermission, registerEntityType);
8644
+ entityTypesRouter.patch(`/:name/schemas`, withExistingEntityType, withEntityTypeManagePermission, withSchema(amendEntityTypeSchemasBodySchema), amendSchemas);
8645
+ entityTypesRouter.get(`/:name`, withExistingEntityType, withEntityTypeSpawnPermission, getEntityType);
8646
+ entityTypesRouter.delete(`/:name`, withExistingEntityType, withEntityTypeManagePermission, deleteEntityType);
8647
+ entityTypesRouter.get(`/:name/grants`, withExistingEntityType, withEntityTypeManagePermission, listTypePermissionGrants);
8648
+ entityTypesRouter.post(`/:name/grants`, withExistingEntityType, withSchema(typePermissionGrantInputSchema), withEntityTypeManagePermission, createTypePermissionGrant);
8649
+ entityTypesRouter.delete(`/:name/grants/:grantId`, withExistingEntityType, withEntityTypeManagePermission, deleteTypePermissionGrant);
7723
8650
  async function registerEntityType(request, ctx) {
7724
8651
  const parsed = routeBody(request);
7725
8652
  const normalized = normalizeEntityTypeRequest(parsed);
7726
8653
  if (normalized.serve_endpoint && !normalized.description && !normalized.creation_schema) return await discoverServeEndpoint(ctx, normalized);
7727
8654
  const entityType = await ctx.entityManager.registerEntityType(normalized);
8655
+ await applyRegistrationPermissionGrants(ctx, entityType.name, normalized);
7728
8656
  return (0, itty_router.json)(toPublicEntityType(entityType), { status: 201 });
7729
8657
  }
7730
8658
  async function listEntityTypes(_request, ctx) {
7731
8659
  const entityTypes$1 = await ctx.entityManager.registry.listEntityTypes();
7732
- return (0, itty_router.json)(entityTypes$1.map((entityType) => toPublicEntityType(entityType)));
8660
+ const visible = [];
8661
+ for (const entityType of entityTypes$1) if (await canAccessEntityType(ctx, entityType, `spawn`)) visible.push(entityType);
8662
+ return (0, itty_router.json)(visible.map((entityType) => toPublicEntityType(entityType)));
8663
+ }
8664
+ async function withExistingEntityType(request, ctx) {
8665
+ const entityType = await ctx.entityManager.registry.getEntityType(request.params.name);
8666
+ if (!entityType) return apiError(404, ErrCodeNotFound, `Entity type not found`);
8667
+ request.entityTypeRoute = { entityType };
8668
+ return void 0;
8669
+ }
8670
+ async function withEntityTypeManagePermission(request, ctx) {
8671
+ const entityType = request.entityTypeRoute?.entityType;
8672
+ if (!entityType) throw new Error(`entity type middleware did not run`);
8673
+ if (await canAccessEntityType(ctx, entityType, `manage`, request)) return void 0;
8674
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to manage ${entityType.name}`);
8675
+ }
8676
+ async function withEntityTypeSpawnPermission(request, ctx) {
8677
+ const entityType = request.entityTypeRoute?.entityType;
8678
+ if (!entityType) throw new Error(`entity type middleware did not run`);
8679
+ if (await canAccessEntityType(ctx, entityType, `spawn`, request)) return void 0;
8680
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn ${entityType.name}`);
8681
+ }
8682
+ async function withEntityTypeRegistrationPermission(request, ctx) {
8683
+ const parsed = normalizeEntityTypeRequest(routeBody(request));
8684
+ if (!parsed.name) return void 0;
8685
+ const existing = await ctx.entityManager.registry.getEntityType(parsed.name);
8686
+ if (existing) {
8687
+ request.entityTypeRoute = { entityType: existing };
8688
+ if (await canAccessEntityType(ctx, existing, `manage`, request)) return void 0;
8689
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to manage ${existing.name}`);
8690
+ }
8691
+ if (await canRegisterEntityType(ctx, parsed, request)) return void 0;
8692
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to register entity types`);
7733
8693
  }
7734
8694
  async function discoverServeEndpoint(ctx, parsed) {
7735
8695
  try {
@@ -7738,17 +8698,17 @@ async function discoverServeEndpoint(ctx, parsed) {
7738
8698
  const manifest = await response.json();
7739
8699
  if (manifest.name !== parsed.name) return apiError(400, ErrCodeServeEndpointNameMismatch, `Serve endpoint returned name "${manifest.name}" but expected "${parsed.name}"`);
7740
8700
  manifest.serve_endpoint = parsed.serve_endpoint;
8701
+ manifest.permission_grants = parsed.permission_grants;
7741
8702
  const entityType = await ctx.entityManager.registerEntityType(normalizeEntityTypeRequest(manifest));
8703
+ await applyRegistrationPermissionGrants(ctx, entityType.name, manifest);
7742
8704
  return (0, itty_router.json)(toPublicEntityType(entityType), { status: 201 });
7743
8705
  } catch (err) {
7744
8706
  if (err instanceof ElectricAgentsError) throw err;
7745
8707
  return apiError(502, ErrCodeServeEndpointUnreachable, `Failed to reach serve endpoint: ${err instanceof Error ? err.message : String(err)}`);
7746
8708
  }
7747
8709
  }
7748
- async function getEntityType(request, ctx) {
7749
- const entityType = await ctx.entityManager.registry.getEntityType(request.params.name);
7750
- if (!entityType) return apiError(404, ErrCodeNotFound, `Entity type not found`);
7751
- return (0, itty_router.json)(toPublicEntityType(entityType));
8710
+ async function getEntityType(request) {
8711
+ return (0, itty_router.json)(toPublicEntityType(request.entityTypeRoute.entityType));
7752
8712
  }
7753
8713
  async function amendSchemas(request, ctx) {
7754
8714
  const parsed = routeBody(request);
@@ -7762,6 +8722,47 @@ async function deleteEntityType(request, ctx) {
7762
8722
  await ctx.entityManager.deleteEntityType(request.params.name);
7763
8723
  return (0, itty_router.status)(204);
7764
8724
  }
8725
+ async function listTypePermissionGrants(request, ctx) {
8726
+ const grants = await ctx.entityManager.registry.listEntityTypePermissionGrants(request.entityTypeRoute.entityType.name);
8727
+ return (0, itty_router.json)({ grants });
8728
+ }
8729
+ async function createTypePermissionGrant(request, ctx) {
8730
+ const parsed = routeBody(request);
8731
+ const grant = await ctx.entityManager.registry.createEntityTypePermissionGrant({
8732
+ entityType: request.entityTypeRoute.entityType.name,
8733
+ permission: parsed.permission,
8734
+ subjectKind: parsed.subject_kind,
8735
+ subjectValue: parsed.subject_value,
8736
+ expiresAt: parseExpiresAt(parsed.expires_at),
8737
+ createdBy: ctx.principal.url
8738
+ });
8739
+ return (0, itty_router.json)(grant, { status: 201 });
8740
+ }
8741
+ async function deleteTypePermissionGrant(request, ctx) {
8742
+ const deleted = await ctx.entityManager.registry.deleteEntityTypePermissionGrant(request.entityTypeRoute.entityType.name, parseGrantId(request));
8743
+ return deleted ? (0, itty_router.status)(204) : apiError(404, ErrCodeNotFound, `Grant not found`);
8744
+ }
8745
+ async function applyRegistrationPermissionGrants(ctx, entityType, request) {
8746
+ for (const grant of request.permission_grants ?? []) await ctx.entityManager.registry.ensureEntityTypePermissionGrant({
8747
+ entityType,
8748
+ permission: grant.permission,
8749
+ subjectKind: grant.subject_kind,
8750
+ subjectValue: grant.subject_value,
8751
+ expiresAt: parseExpiresAt(grant.expires_at),
8752
+ createdBy: ctx.principal.url
8753
+ });
8754
+ }
8755
+ function parseGrantId(request) {
8756
+ const grantId = Number.parseInt(String(request.params.grantId), 10);
8757
+ if (!Number.isSafeInteger(grantId) || grantId <= 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid grant id`, 400);
8758
+ return grantId;
8759
+ }
8760
+ function parseExpiresAt(value) {
8761
+ if (value === void 0) return void 0;
8762
+ const expiresAt = new Date(value);
8763
+ if (Number.isNaN(expiresAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid expires_at timestamp`, 400);
8764
+ return expiresAt;
8765
+ }
7765
8766
  function normalizeEntityTypeRequest(parsed) {
7766
8767
  const serveEndpoint = rewriteLoopbackWebhookUrl(parsed.serve_endpoint);
7767
8768
  return {
@@ -7770,11 +8771,13 @@ function normalizeEntityTypeRequest(parsed) {
7770
8771
  creation_schema: parsed.creation_schema,
7771
8772
  inbox_schemas: parsed.inbox_schemas,
7772
8773
  state_schemas: parsed.state_schemas,
8774
+ slash_commands: parsed.slash_commands,
7773
8775
  serve_endpoint: serveEndpoint,
7774
8776
  default_dispatch_policy: parsed.default_dispatch_policy ?? (serveEndpoint ? { targets: [{
7775
8777
  type: `webhook`,
7776
8778
  url: serveEndpoint
7777
- }] } : void 0)
8779
+ }] } : void 0),
8780
+ permission_grants: parsed.permission_grants
7778
8781
  };
7779
8782
  }
7780
8783
  function toPublicEntityType(entityType) {
@@ -7833,6 +8836,7 @@ function applyCors(response) {
7833
8836
  `content-type`,
7834
8837
  `authorization`,
7835
8838
  `electric-claim-token`,
8839
+ `electric-owner-entity`,
7836
8840
  ELECTRIC_PRINCIPAL_HEADER,
7837
8841
  `ngrok-skip-browser-warning`
7838
8842
  ].join(`, `));
@@ -7883,7 +8887,7 @@ observationsRouter.post(`/entities/ensure-stream`, withSchema(ensureEntitiesMemb
7883
8887
  observationsRouter.post(`/cron/ensure-stream`, withSchema(ensureCronStreamBodySchema), ensureCronStream);
7884
8888
  async function ensureEntitiesMembershipStream(request, ctx) {
7885
8889
  const parsed = routeBody(request);
7886
- const result = await ctx.entityManager.ensureEntitiesMembershipStream(parsed.tags ?? {});
8890
+ const result = await ctx.entityManager.ensureEntitiesMembershipStream(parsed.tags ?? {}, ctx.principal);
7887
8891
  return (0, itty_router.json)(result);
7888
8892
  }
7889
8893
  async function ensureCronStream(request, ctx) {