@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.js CHANGED
@@ -5,15 +5,15 @@ import { fileURLToPath } from "node:url";
5
5
  import { drizzle } from "drizzle-orm/postgres-js";
6
6
  import { migrate } from "drizzle-orm/postgres-js/migrator";
7
7
  import postgres from "postgres";
8
- import { and, desc, eq, lt, ne, sql } from "drizzle-orm";
8
+ import { and, desc, eq, inArray, lt, ne, sql } from "drizzle-orm";
9
9
  import { bigint, bigserial, boolean, check, index, integer, jsonb, pgTable, primaryKey, serial, text, timestamp, unique } from "drizzle-orm/pg-core";
10
10
  import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, randomUUID, sign } from "node:crypto";
11
- import { appendPathToUrl, assertTags, buildEventSourceManifestEntry, buildTagsIndex, entityStateSchema, eventSourceSubscriptionManifestKey, getCronStreamPath, getCronStreamPathFromSpec, getEntitiesStreamPath, getNextCronFireAt, getSharedStateStreamPath, getWebhookStreamPath, manifestChildKey, manifestSharedStateKey, manifestSourceKey, normalizeTags, parseCronStreamPath, resolveCronScheduleSpec, resolveEventSourceSubscription, sourceRefForTags, verifyWebhookSignature } from "@electric-ax/agents-runtime";
11
+ import { COMPOSER_INPUT_MESSAGE_TYPE, appendPathToUrl, assertTags, buildEventSourceManifestEntry, buildTagsIndex, entityStateSchema, eventSourceSubscriptionManifestKey, getCronStreamPath, getCronStreamPathFromSpec, getEntitiesStreamPath, getNextCronFireAt, getSharedStateStreamPath, getWebhookStreamPath, hashString, manifestChildKey, manifestSharedStateKey, manifestSourceKey, normalizeTags, parseCronStreamPath, resolveCronScheduleSpec, resolveEventSourceSubscription, sourceRefForTags, validateComposerInputPayload, validateSlashCommandDefinitions, verifyWebhookSignature } from "@electric-ax/agents-runtime";
12
12
  import { DurableStream, DurableStreamError, FetchError, IdempotentProducer } from "@durable-streams/client";
13
13
  import { ShapeStream, isChangeMessage, isControlMessage } from "@electric-sql/client";
14
14
  import pino from "pino";
15
- import fastq from "fastq";
16
15
  import { Type } from "@sinclair/typebox";
16
+ import fastq from "fastq";
17
17
  import Ajv from "ajv";
18
18
  import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
19
19
  import { AutoRouter, Router, json, status, withParams } from "itty-router";
@@ -26,11 +26,16 @@ __export(schema_exports, {
26
26
  entities: () => entities,
27
27
  entityBridges: () => entityBridges,
28
28
  entityDispatchState: () => entityDispatchState,
29
+ entityEffectivePermissions: () => entityEffectivePermissions,
30
+ entityLineage: () => entityLineage,
29
31
  entityManifestSources: () => entityManifestSources,
32
+ entityPermissionGrants: () => entityPermissionGrants,
33
+ entityTypePermissionGrants: () => entityTypePermissionGrants,
30
34
  entityTypes: () => entityTypes,
31
35
  runnerRuntimeDiagnostics: () => runnerRuntimeDiagnostics,
32
36
  runners: () => runners,
33
37
  scheduledTasks: () => scheduledTasks,
38
+ sharedStateLinks: () => sharedStateLinks,
34
39
  subscriptionWebhooks: () => subscriptionWebhooks,
35
40
  tagStreamOutbox: () => tagStreamOutbox,
36
41
  users: () => users,
@@ -44,6 +49,7 @@ const entityTypes = pgTable(`entity_types`, {
44
49
  creationSchema: jsonb(`creation_schema`),
45
50
  inboxSchemas: jsonb(`inbox_schemas`),
46
51
  stateSchemas: jsonb(`state_schemas`),
52
+ slashCommands: jsonb(`slash_commands`),
47
53
  serveEndpoint: text(`serve_endpoint`),
48
54
  defaultDispatchPolicy: jsonb(`default_dispatch_policy`),
49
55
  revision: integer(`revision`).notNull().default(1),
@@ -78,6 +84,94 @@ const entities = pgTable(`entities`, {
78
84
  index(`entities_tags_index_gin`).using(`gin`, table.tagsIndex),
79
85
  check(`chk_entities_status`, sql`${table.status} IN ('spawning', 'running', 'idle', 'paused', 'stopping', 'stopped', 'killed')`)
80
86
  ]);
87
+ const entityTypePermissionGrants = pgTable(`entity_type_permission_grants`, {
88
+ id: bigserial(`id`, { mode: `number` }).primaryKey(),
89
+ tenantId: text(`tenant_id`).notNull().default(`default`),
90
+ entityType: text(`entity_type`).notNull(),
91
+ permission: text(`permission`).notNull(),
92
+ subjectKind: text(`subject_kind`).notNull(),
93
+ subjectValue: text(`subject_value`).notNull(),
94
+ createdBy: text(`created_by`),
95
+ expiresAt: timestamp(`expires_at`, { withTimezone: true }),
96
+ createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow(),
97
+ updatedAt: timestamp(`updated_at`, { withTimezone: true }).notNull().defaultNow()
98
+ }, (table) => [
99
+ index(`idx_type_permission_grants_lookup`).on(table.tenantId, table.entityType, table.permission, table.subjectKind, table.subjectValue),
100
+ index(`idx_type_permission_grants_expiry`).on(table.tenantId, table.expiresAt),
101
+ check(`chk_type_permission_grants_permission`, sql`${table.permission} IN ('spawn', 'manage')`),
102
+ check(`chk_type_permission_grants_subject_kind`, sql`${table.subjectKind} IN ('principal', 'principal_kind')`)
103
+ ]);
104
+ const entityLineage = pgTable(`entity_lineage`, {
105
+ tenantId: text(`tenant_id`).notNull().default(`default`),
106
+ ancestorUrl: text(`ancestor_url`).notNull(),
107
+ descendantUrl: text(`descendant_url`).notNull(),
108
+ depth: integer(`depth`).notNull(),
109
+ createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow()
110
+ }, (table) => [
111
+ primaryKey({ columns: [
112
+ table.tenantId,
113
+ table.ancestorUrl,
114
+ table.descendantUrl
115
+ ] }),
116
+ index(`idx_entity_lineage_descendant`).on(table.tenantId, table.descendantUrl),
117
+ check(`chk_entity_lineage_depth`, sql`${table.depth} >= 0`)
118
+ ]);
119
+ const entityPermissionGrants = pgTable(`entity_permission_grants`, {
120
+ id: bigserial(`id`, { mode: `number` }).primaryKey(),
121
+ tenantId: text(`tenant_id`).notNull().default(`default`),
122
+ entityUrl: text(`entity_url`).notNull(),
123
+ permission: text(`permission`).notNull(),
124
+ subjectKind: text(`subject_kind`).notNull(),
125
+ subjectValue: text(`subject_value`).notNull(),
126
+ propagation: text(`propagation`).notNull().default(`self`),
127
+ copyToChildren: boolean(`copy_to_children`).notNull().default(false),
128
+ createdBy: text(`created_by`),
129
+ expiresAt: timestamp(`expires_at`, { withTimezone: true }),
130
+ createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow(),
131
+ updatedAt: timestamp(`updated_at`, { withTimezone: true }).notNull().defaultNow()
132
+ }, (table) => [
133
+ index(`idx_entity_permission_grants_entity`).on(table.tenantId, table.entityUrl),
134
+ index(`idx_entity_permission_grants_subject`).on(table.tenantId, table.permission, table.subjectKind, table.subjectValue),
135
+ index(`idx_entity_permission_grants_expiry`).on(table.tenantId, table.expiresAt),
136
+ check(`chk_entity_permission_grants_permission`, sql`${table.permission} IN ('read', 'write', 'delete', 'signal', 'fork', 'schedule', 'spawn', 'manage')`),
137
+ check(`chk_entity_permission_grants_subject_kind`, sql`${table.subjectKind} IN ('principal', 'principal_kind')`),
138
+ check(`chk_entity_permission_grants_propagation`, sql`${table.propagation} IN ('self', 'descendants')`)
139
+ ]);
140
+ const entityEffectivePermissions = pgTable(`entity_effective_permissions`, {
141
+ id: bigserial(`id`, { mode: `number` }).primaryKey(),
142
+ tenantId: text(`tenant_id`).notNull().default(`default`),
143
+ entityUrl: text(`entity_url`).notNull(),
144
+ sourceEntityUrl: text(`source_entity_url`).notNull(),
145
+ sourceGrantId: bigint(`source_grant_id`, { mode: `number` }).notNull(),
146
+ permission: text(`permission`).notNull(),
147
+ subjectKind: text(`subject_kind`).notNull(),
148
+ subjectValue: text(`subject_value`).notNull(),
149
+ expiresAt: timestamp(`expires_at`, { withTimezone: true }),
150
+ createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow()
151
+ }, (table) => [
152
+ unique(`uq_entity_effective_permission`).on(table.tenantId, table.entityUrl, table.sourceGrantId),
153
+ index(`idx_entity_effective_permissions_lookup`).on(table.tenantId, table.permission, table.subjectKind, table.subjectValue, table.entityUrl),
154
+ index(`idx_entity_effective_permissions_entity`).on(table.tenantId, table.entityUrl),
155
+ index(`idx_entity_effective_permissions_expiry`).on(table.tenantId, table.expiresAt),
156
+ check(`chk_entity_effective_permissions_permission`, sql`${table.permission} IN ('read', 'write', 'delete', 'signal', 'fork', 'schedule', 'spawn', 'manage')`),
157
+ check(`chk_entity_effective_permissions_subject_kind`, sql`${table.subjectKind} IN ('principal', 'principal_kind')`)
158
+ ]);
159
+ const sharedStateLinks = pgTable(`shared_state_links`, {
160
+ tenantId: text(`tenant_id`).notNull().default(`default`),
161
+ sharedStateId: text(`shared_state_id`).notNull(),
162
+ ownerEntityUrl: text(`owner_entity_url`).notNull(),
163
+ manifestKey: text(`manifest_key`).notNull(),
164
+ createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow(),
165
+ updatedAt: timestamp(`updated_at`, { withTimezone: true }).notNull().defaultNow()
166
+ }, (table) => [
167
+ primaryKey({ columns: [
168
+ table.tenantId,
169
+ table.ownerEntityUrl,
170
+ table.manifestKey
171
+ ] }),
172
+ index(`idx_shared_state_links_shared_state`).on(table.tenantId, table.sharedStateId),
173
+ index(`idx_shared_state_links_owner`).on(table.tenantId, table.ownerEntityUrl)
174
+ ]);
81
175
  const users = pgTable(`users`, {
82
176
  tenantId: text(`tenant_id`).notNull().default(`default`),
83
177
  id: text(`id`).notNull(),
@@ -264,12 +358,18 @@ const entityBridges = pgTable(`entity_bridges`, {
264
358
  sourceRef: text(`source_ref`).notNull(),
265
359
  tags: jsonb(`tags`).notNull(),
266
360
  streamUrl: text(`stream_url`).notNull(),
361
+ principalUrl: text(`principal_url`),
362
+ principalKind: text(`principal_kind`),
267
363
  shapeHandle: text(`shape_handle`),
268
364
  shapeOffset: text(`shape_offset`),
269
365
  lastObserverActivityAt: timestamp(`last_observer_activity_at`, { withTimezone: true }).notNull().defaultNow(),
270
366
  createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow(),
271
367
  updatedAt: timestamp(`updated_at`, { withTimezone: true }).notNull().defaultNow()
272
- }, (table) => [primaryKey({ columns: [table.tenantId, table.sourceRef] }), unique(`uq_entity_bridges_stream_url`).on(table.tenantId, table.streamUrl)]);
368
+ }, (table) => [
369
+ primaryKey({ columns: [table.tenantId, table.sourceRef] }),
370
+ unique(`uq_entity_bridges_stream_url`).on(table.tenantId, table.streamUrl),
371
+ index(`idx_entity_bridges_principal`).on(table.tenantId, table.principalKind, table.principalUrl)
372
+ ]);
273
373
  const entityManifestSources = pgTable(`entity_manifest_sources`, {
274
374
  tenantId: text(`tenant_id`).notNull().default(`default`),
275
375
  ownerEntityUrl: text(`owner_entity_url`).notNull(),
@@ -457,16 +557,26 @@ function isDuplicateUrlError(err) {
457
557
  return e.code === `23505`;
458
558
  }
459
559
  const DEFAULT_RUNNER_LEASE_MS = 3e4;
560
+ const PERMISSION_PRUNE_INTERVAL_MS = 3e4;
460
561
  function runnerWakeStream(runnerId) {
461
562
  return `/runners/${runnerId}/wake`;
462
563
  }
463
564
  var PostgresRegistry = class {
565
+ lastPermissionPruneStartedAt = 0;
566
+ permissionPrunePromise = null;
464
567
  constructor(db, tenantId = DEFAULT_TENANT_ID) {
465
568
  this.db = db;
466
569
  this.tenantId = tenantId;
467
570
  }
468
571
  async initialize() {}
469
572
  close() {}
573
+ async ensureUserForPrincipal(principal) {
574
+ if (principal.kind !== `user`) return;
575
+ await this.db.insert(users).values({
576
+ tenantId: this.tenantId,
577
+ id: principal.id
578
+ }).onConflictDoNothing();
579
+ }
470
580
  async createRunner(input) {
471
581
  const now = new Date();
472
582
  const wakeStream = input.wakeStream ?? runnerWakeStream(input.id);
@@ -711,6 +821,7 @@ var PostgresRegistry = class {
711
821
  creationSchema: et.creation_schema ?? null,
712
822
  inboxSchemas: et.inbox_schemas ?? null,
713
823
  stateSchemas: et.state_schemas ?? null,
824
+ slashCommands: et.slash_commands ?? null,
714
825
  serveEndpoint: et.serve_endpoint ?? null,
715
826
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
716
827
  revision: et.revision,
@@ -723,6 +834,7 @@ var PostgresRegistry = class {
723
834
  creationSchema: et.creation_schema ?? null,
724
835
  inboxSchemas: et.inbox_schemas ?? null,
725
836
  stateSchemas: et.state_schemas ?? null,
837
+ slashCommands: et.slash_commands ?? null,
726
838
  serveEndpoint: et.serve_endpoint ?? null,
727
839
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
728
840
  revision: et.revision,
@@ -740,6 +852,7 @@ var PostgresRegistry = class {
740
852
  creationSchema: et.creation_schema ?? null,
741
853
  inboxSchemas: et.inbox_schemas ?? null,
742
854
  stateSchemas: et.state_schemas ?? null,
855
+ slashCommands: et.slash_commands ?? null,
743
856
  serveEndpoint: et.serve_endpoint ?? null,
744
857
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
745
858
  revision: et.revision,
@@ -766,6 +879,7 @@ var PostgresRegistry = class {
766
879
  creationSchema: et.creation_schema ?? null,
767
880
  inboxSchemas: et.inbox_schemas ?? null,
768
881
  stateSchemas: et.state_schemas ?? null,
882
+ slashCommands: et.slash_commands ?? null,
769
883
  serveEndpoint: et.serve_endpoint ?? null,
770
884
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
771
885
  revision: et.revision,
@@ -801,6 +915,59 @@ var PostgresRegistry = class {
801
915
  pendingSourceStreams: [],
802
916
  updatedAt: new Date()
803
917
  }).onConflictDoNothing();
918
+ await tx.insert(entityLineage).values({
919
+ tenantId: this.tenantId,
920
+ ancestorUrl: entity.url,
921
+ descendantUrl: entity.url,
922
+ depth: 0
923
+ }).onConflictDoNothing();
924
+ if (entity.parent) await tx.execute(sql`
925
+ INSERT INTO ${entityLineage} (
926
+ tenant_id,
927
+ ancestor_url,
928
+ descendant_url,
929
+ depth
930
+ )
931
+ SELECT
932
+ ${this.tenantId},
933
+ ancestor_url,
934
+ ${entity.url},
935
+ depth + 1
936
+ FROM ${entityLineage}
937
+ WHERE tenant_id = ${this.tenantId}
938
+ AND descendant_url = ${entity.parent}
939
+ ON CONFLICT DO NOTHING
940
+ `);
941
+ await tx.execute(sql`
942
+ INSERT INTO ${entityEffectivePermissions} (
943
+ tenant_id,
944
+ entity_url,
945
+ source_entity_url,
946
+ source_grant_id,
947
+ permission,
948
+ subject_kind,
949
+ subject_value,
950
+ expires_at
951
+ )
952
+ SELECT
953
+ ${this.tenantId},
954
+ ${entity.url},
955
+ grants.entity_url,
956
+ grants.id,
957
+ grants.permission,
958
+ grants.subject_kind,
959
+ grants.subject_value,
960
+ grants.expires_at
961
+ FROM ${entityPermissionGrants} grants
962
+ JOIN ${entityLineage} lineage
963
+ ON lineage.tenant_id = grants.tenant_id
964
+ AND lineage.ancestor_url = grants.entity_url
965
+ AND lineage.descendant_url = ${entity.url}
966
+ WHERE grants.tenant_id = ${this.tenantId}
967
+ AND grants.propagation = 'descendants'
968
+ AND (grants.expires_at IS NULL OR grants.expires_at > now())
969
+ ON CONFLICT DO NOTHING
970
+ `);
804
971
  return parseInt(result[0].txid);
805
972
  });
806
973
  } catch (err) {
@@ -822,10 +989,8 @@ var PostgresRegistry = class {
822
989
  }
823
990
  async getEntityByStream(streamPath) {
824
991
  const mainSuffix = `/main`;
825
- const errorSuffix = `/error`;
826
992
  let entityUrl = null;
827
993
  if (streamPath.endsWith(mainSuffix)) entityUrl = streamPath.slice(0, -mainSuffix.length);
828
- else if (streamPath.endsWith(errorSuffix)) entityUrl = streamPath.slice(0, -errorSuffix.length);
829
994
  if (!entityUrl) return null;
830
995
  return this.getEntity(entityUrl);
831
996
  }
@@ -835,6 +1000,23 @@ var PostgresRegistry = class {
835
1000
  if (filter?.status) conditions.push(eq(entities.status, filter.status));
836
1001
  if (filter?.parent) conditions.push(eq(entities.parent, filter.parent));
837
1002
  if (filter?.created_by) conditions.push(eq(entities.createdBy, filter.created_by));
1003
+ if (filter?.readableBy && !filter.readableBy.bypass) conditions.push(sql`(
1004
+ ${entities.createdBy} = ${filter.readableBy.principalUrl}
1005
+ OR ${entities.url} IN (
1006
+ SELECT ${entityEffectivePermissions.entityUrl}
1007
+ FROM ${entityEffectivePermissions}
1008
+ WHERE ${entityEffectivePermissions.tenantId} = ${this.tenantId}
1009
+ AND ${entityEffectivePermissions.permission} IN ('read', 'manage')
1010
+ AND (${entityEffectivePermissions.expiresAt} IS NULL OR ${entityEffectivePermissions.expiresAt} > now())
1011
+ AND (
1012
+ (${entityEffectivePermissions.subjectKind} = 'principal'
1013
+ AND ${entityEffectivePermissions.subjectValue} = ${filter.readableBy.principalUrl})
1014
+ OR
1015
+ (${entityEffectivePermissions.subjectKind} = 'principal_kind'
1016
+ AND ${entityEffectivePermissions.subjectValue} = ${filter.readableBy.principalKind})
1017
+ )
1018
+ )
1019
+ )`);
838
1020
  const whereClause = and(...conditions);
839
1021
  const countResult = await this.db.select({ count: sql`count(*)` }).from(entities).where(whereClause);
840
1022
  const total = Number(countResult[0].count);
@@ -847,6 +1029,189 @@ var PostgresRegistry = class {
847
1029
  total
848
1030
  };
849
1031
  }
1032
+ async createEntityTypePermissionGrant(input) {
1033
+ const [row] = await this.db.insert(entityTypePermissionGrants).values({
1034
+ tenantId: this.tenantId,
1035
+ entityType: input.entityType,
1036
+ permission: input.permission,
1037
+ subjectKind: input.subjectKind,
1038
+ subjectValue: input.subjectValue,
1039
+ createdBy: input.createdBy ?? null,
1040
+ expiresAt: input.expiresAt ?? null
1041
+ }).returning();
1042
+ return this.rowToEntityTypePermissionGrant(row);
1043
+ }
1044
+ async ensureEntityTypePermissionGrant(input) {
1045
+ const [existing] = await this.db.select().from(entityTypePermissionGrants).where(and(eq(entityTypePermissionGrants.tenantId, this.tenantId), eq(entityTypePermissionGrants.entityType, input.entityType), eq(entityTypePermissionGrants.permission, input.permission), eq(entityTypePermissionGrants.subjectKind, input.subjectKind), eq(entityTypePermissionGrants.subjectValue, input.subjectValue), input.expiresAt ? eq(entityTypePermissionGrants.expiresAt, input.expiresAt) : sql`${entityTypePermissionGrants.expiresAt} IS NULL`)).limit(1);
1046
+ if (existing) return this.rowToEntityTypePermissionGrant(existing);
1047
+ return await this.createEntityTypePermissionGrant(input);
1048
+ }
1049
+ async listEntityTypePermissionGrants(entityType) {
1050
+ const rows = await this.db.select().from(entityTypePermissionGrants).where(and(eq(entityTypePermissionGrants.tenantId, this.tenantId), eq(entityTypePermissionGrants.entityType, entityType))).orderBy(entityTypePermissionGrants.id);
1051
+ return rows.map((row) => this.rowToEntityTypePermissionGrant(row));
1052
+ }
1053
+ async deleteEntityTypePermissionGrant(entityType, grantId) {
1054
+ const rows = await this.db.delete(entityTypePermissionGrants).where(and(eq(entityTypePermissionGrants.tenantId, this.tenantId), eq(entityTypePermissionGrants.entityType, entityType), eq(entityTypePermissionGrants.id, grantId))).returning({ id: entityTypePermissionGrants.id });
1055
+ return rows.length > 0;
1056
+ }
1057
+ async hasEntityTypePermission(entityType, permission, subject) {
1058
+ const permissions = [permission, `manage`];
1059
+ const rows = await this.db.select({ id: entityTypePermissionGrants.id }).from(entityTypePermissionGrants).where(and(eq(entityTypePermissionGrants.tenantId, this.tenantId), eq(entityTypePermissionGrants.entityType, entityType), inArray(entityTypePermissionGrants.permission, [...permissions]), sql`(${entityTypePermissionGrants.expiresAt} IS NULL OR ${entityTypePermissionGrants.expiresAt} > now())`, sql`(
1060
+ (${entityTypePermissionGrants.subjectKind} = 'principal'
1061
+ AND ${entityTypePermissionGrants.subjectValue} = ${subject.principalUrl})
1062
+ OR
1063
+ (${entityTypePermissionGrants.subjectKind} = 'principal_kind'
1064
+ AND ${entityTypePermissionGrants.subjectValue} = ${subject.principalKind})
1065
+ )`)).limit(1);
1066
+ return rows.length > 0;
1067
+ }
1068
+ async createEntityPermissionGrant(input) {
1069
+ return await this.db.transaction(async (tx) => {
1070
+ const [row] = await tx.insert(entityPermissionGrants).values({
1071
+ tenantId: this.tenantId,
1072
+ entityUrl: input.entityUrl,
1073
+ permission: input.permission,
1074
+ subjectKind: input.subjectKind,
1075
+ subjectValue: input.subjectValue,
1076
+ propagation: input.propagation ?? `self`,
1077
+ copyToChildren: input.copyToChildren ?? false,
1078
+ createdBy: input.createdBy ?? null,
1079
+ expiresAt: input.expiresAt ?? null
1080
+ }).returning();
1081
+ await this.materializeEntityPermissionGrant(tx, row);
1082
+ return this.rowToEntityPermissionGrant(row);
1083
+ });
1084
+ }
1085
+ async listEntityPermissionGrants(entityUrl) {
1086
+ const rows = await this.db.select().from(entityPermissionGrants).where(and(eq(entityPermissionGrants.tenantId, this.tenantId), eq(entityPermissionGrants.entityUrl, entityUrl))).orderBy(entityPermissionGrants.id);
1087
+ return rows.map((row) => this.rowToEntityPermissionGrant(row));
1088
+ }
1089
+ async deleteEntityPermissionGrant(entityUrl, grantId) {
1090
+ return await this.db.transaction(async (tx) => {
1091
+ await tx.delete(entityEffectivePermissions).where(and(eq(entityEffectivePermissions.tenantId, this.tenantId), eq(entityEffectivePermissions.sourceGrantId, grantId)));
1092
+ const rows = await tx.delete(entityPermissionGrants).where(and(eq(entityPermissionGrants.tenantId, this.tenantId), eq(entityPermissionGrants.entityUrl, entityUrl), eq(entityPermissionGrants.id, grantId))).returning({ id: entityPermissionGrants.id });
1093
+ return rows.length > 0;
1094
+ });
1095
+ }
1096
+ async copyEntityPermissionGrantsForSpawn(parentEntityUrl, childEntityUrl, createdBy) {
1097
+ const parentGrants = await this.db.select().from(entityPermissionGrants).where(and(eq(entityPermissionGrants.tenantId, this.tenantId), eq(entityPermissionGrants.entityUrl, parentEntityUrl), eq(entityPermissionGrants.copyToChildren, true), sql`(${entityPermissionGrants.expiresAt} IS NULL OR ${entityPermissionGrants.expiresAt} > now())`));
1098
+ const copied = [];
1099
+ for (const grant of parentGrants) copied.push(await this.createEntityPermissionGrant({
1100
+ entityUrl: childEntityUrl,
1101
+ permission: grant.permission,
1102
+ subjectKind: grant.subjectKind,
1103
+ subjectValue: grant.subjectValue,
1104
+ propagation: `self`,
1105
+ copyToChildren: grant.copyToChildren,
1106
+ createdBy,
1107
+ expiresAt: grant.expiresAt ?? void 0
1108
+ }));
1109
+ return copied;
1110
+ }
1111
+ async hasEntityPermission(entityUrl, permission, subject) {
1112
+ const permissions = [permission, `manage`];
1113
+ const rows = await this.db.select({ id: entityEffectivePermissions.id }).from(entityEffectivePermissions).where(and(eq(entityEffectivePermissions.tenantId, this.tenantId), eq(entityEffectivePermissions.entityUrl, entityUrl), inArray(entityEffectivePermissions.permission, [...permissions]), sql`(${entityEffectivePermissions.expiresAt} IS NULL OR ${entityEffectivePermissions.expiresAt} > now())`, sql`(
1114
+ (${entityEffectivePermissions.subjectKind} = 'principal'
1115
+ AND ${entityEffectivePermissions.subjectValue} = ${subject.principalUrl})
1116
+ OR
1117
+ (${entityEffectivePermissions.subjectKind} = 'principal_kind'
1118
+ AND ${entityEffectivePermissions.subjectValue} = ${subject.principalKind})
1119
+ )`)).limit(1);
1120
+ return rows.length > 0;
1121
+ }
1122
+ async replaceSharedStateLink(ownerEntityUrl, manifestKey, sharedStateId) {
1123
+ await this.db.delete(sharedStateLinks).where(and(eq(sharedStateLinks.tenantId, this.tenantId), eq(sharedStateLinks.ownerEntityUrl, ownerEntityUrl), eq(sharedStateLinks.manifestKey, manifestKey)));
1124
+ if (!sharedStateId) return;
1125
+ await this.db.insert(sharedStateLinks).values({
1126
+ tenantId: this.tenantId,
1127
+ ownerEntityUrl,
1128
+ manifestKey,
1129
+ sharedStateId
1130
+ }).onConflictDoUpdate({
1131
+ target: [
1132
+ sharedStateLinks.tenantId,
1133
+ sharedStateLinks.ownerEntityUrl,
1134
+ sharedStateLinks.manifestKey
1135
+ ],
1136
+ set: {
1137
+ sharedStateId,
1138
+ updatedAt: new Date()
1139
+ }
1140
+ });
1141
+ }
1142
+ async listSharedStateLinkedEntityUrls(sharedStateId) {
1143
+ const rows = await this.db.selectDistinct({ ownerEntityUrl: sharedStateLinks.ownerEntityUrl }).from(sharedStateLinks).where(and(eq(sharedStateLinks.tenantId, this.tenantId), eq(sharedStateLinks.sharedStateId, sharedStateId)));
1144
+ return rows.map((row) => row.ownerEntityUrl);
1145
+ }
1146
+ async pruneExpiredPermissionGrants(now = new Date(), options = {}) {
1147
+ if (this.permissionPrunePromise) return await this.permissionPrunePromise;
1148
+ const startedAt = Date.now();
1149
+ if (!options.force && startedAt - this.lastPermissionPruneStartedAt < PERMISSION_PRUNE_INTERVAL_MS) return;
1150
+ this.lastPermissionPruneStartedAt = startedAt;
1151
+ const promise = this.pruneExpiredPermissionGrantsNow(now);
1152
+ this.permissionPrunePromise = promise;
1153
+ try {
1154
+ await promise;
1155
+ } catch (error) {
1156
+ this.lastPermissionPruneStartedAt = 0;
1157
+ throw error;
1158
+ } finally {
1159
+ if (this.permissionPrunePromise === promise) this.permissionPrunePromise = null;
1160
+ }
1161
+ }
1162
+ async pruneExpiredPermissionGrantsNow(now) {
1163
+ await this.db.transaction(async (tx) => {
1164
+ const expiredEntityGrantIds = await tx.select({ id: entityPermissionGrants.id }).from(entityPermissionGrants).where(and(eq(entityPermissionGrants.tenantId, this.tenantId), sql`${entityPermissionGrants.expiresAt} IS NOT NULL`, lt(entityPermissionGrants.expiresAt, now)));
1165
+ const ids = expiredEntityGrantIds.map((row) => row.id);
1166
+ if (ids.length > 0) {
1167
+ await tx.delete(entityEffectivePermissions).where(and(eq(entityEffectivePermissions.tenantId, this.tenantId), inArray(entityEffectivePermissions.sourceGrantId, ids)));
1168
+ await tx.delete(entityPermissionGrants).where(and(eq(entityPermissionGrants.tenantId, this.tenantId), inArray(entityPermissionGrants.id, ids)));
1169
+ }
1170
+ await tx.delete(entityEffectivePermissions).where(and(eq(entityEffectivePermissions.tenantId, this.tenantId), sql`${entityEffectivePermissions.expiresAt} IS NOT NULL`, lt(entityEffectivePermissions.expiresAt, now)));
1171
+ await tx.delete(entityTypePermissionGrants).where(and(eq(entityTypePermissionGrants.tenantId, this.tenantId), sql`${entityTypePermissionGrants.expiresAt} IS NOT NULL`, lt(entityTypePermissionGrants.expiresAt, now)));
1172
+ });
1173
+ }
1174
+ async materializeEntityPermissionGrant(tx, grant) {
1175
+ await tx.delete(entityEffectivePermissions).where(and(eq(entityEffectivePermissions.tenantId, this.tenantId), eq(entityEffectivePermissions.sourceGrantId, grant.id)));
1176
+ if (grant.propagation === `descendants`) {
1177
+ await tx.execute(sql`
1178
+ INSERT INTO ${entityEffectivePermissions} (
1179
+ tenant_id,
1180
+ entity_url,
1181
+ source_entity_url,
1182
+ source_grant_id,
1183
+ permission,
1184
+ subject_kind,
1185
+ subject_value,
1186
+ expires_at
1187
+ )
1188
+ SELECT
1189
+ ${this.tenantId},
1190
+ descendant_url,
1191
+ ${grant.entityUrl},
1192
+ ${grant.id},
1193
+ ${grant.permission},
1194
+ ${grant.subjectKind},
1195
+ ${grant.subjectValue},
1196
+ ${grant.expiresAt}
1197
+ FROM ${entityLineage}
1198
+ WHERE tenant_id = ${this.tenantId}
1199
+ AND ancestor_url = ${grant.entityUrl}
1200
+ ON CONFLICT DO NOTHING
1201
+ `);
1202
+ return;
1203
+ }
1204
+ await tx.insert(entityEffectivePermissions).values({
1205
+ tenantId: this.tenantId,
1206
+ entityUrl: grant.entityUrl,
1207
+ sourceEntityUrl: grant.entityUrl,
1208
+ sourceGrantId: grant.id,
1209
+ permission: grant.permission,
1210
+ subjectKind: grant.subjectKind,
1211
+ subjectValue: grant.subjectValue,
1212
+ expiresAt: grant.expiresAt
1213
+ }).onConflictDoNothing();
1214
+ }
850
1215
  async updateStatus(entityUrl, status$1) {
851
1216
  const whereClause = isTerminalEntityStatus(status$1) ? this.entityWhere(entityUrl) : and(this.entityWhere(entityUrl), ne(entities.status, `stopped`), ne(entities.status, `killed`));
852
1217
  await this.db.update(entities).set({
@@ -948,7 +1313,9 @@ var PostgresRegistry = class {
948
1313
  tenantId: this.tenantId,
949
1314
  sourceRef: row.sourceRef,
950
1315
  tags: normalizeTags(row.tags),
951
- streamUrl: row.streamUrl
1316
+ streamUrl: row.streamUrl,
1317
+ principalUrl: row.principalUrl,
1318
+ principalKind: row.principalKind
952
1319
  }).onConflictDoNothing();
953
1320
  const existing = await this.getEntityBridge(row.sourceRef);
954
1321
  if (!existing) throw new Error(`Failed to load entity bridge ${row.sourceRef}`);
@@ -1103,6 +1470,7 @@ var PostgresRegistry = class {
1103
1470
  creation_schema: row.creationSchema,
1104
1471
  inbox_schemas: row.inboxSchemas,
1105
1472
  state_schemas: row.stateSchemas,
1473
+ slash_commands: row.slashCommands ?? void 0,
1106
1474
  serve_endpoint: row.serveEndpoint ?? void 0,
1107
1475
  default_dispatch_policy: row.defaultDispatchPolicy ?? void 0,
1108
1476
  revision: row.revision,
@@ -1110,15 +1478,40 @@ var PostgresRegistry = class {
1110
1478
  updated_at: row.updatedAt
1111
1479
  };
1112
1480
  }
1481
+ rowToEntityTypePermissionGrant(row) {
1482
+ return {
1483
+ id: row.id,
1484
+ entity_type: row.entityType,
1485
+ permission: row.permission,
1486
+ subject_kind: row.subjectKind,
1487
+ subject_value: row.subjectValue,
1488
+ created_by: row.createdBy ?? void 0,
1489
+ expires_at: row.expiresAt?.toISOString(),
1490
+ created_at: row.createdAt.toISOString(),
1491
+ updated_at: row.updatedAt.toISOString()
1492
+ };
1493
+ }
1494
+ rowToEntityPermissionGrant(row) {
1495
+ return {
1496
+ id: row.id,
1497
+ entity_url: row.entityUrl,
1498
+ permission: row.permission,
1499
+ subject_kind: row.subjectKind,
1500
+ subject_value: row.subjectValue,
1501
+ propagation: row.propagation,
1502
+ copy_to_children: row.copyToChildren,
1503
+ created_by: row.createdBy ?? void 0,
1504
+ expires_at: row.expiresAt?.toISOString(),
1505
+ created_at: row.createdAt.toISOString(),
1506
+ updated_at: row.updatedAt.toISOString()
1507
+ };
1508
+ }
1113
1509
  rowToEntity(row) {
1114
1510
  return {
1115
1511
  url: row.url,
1116
1512
  type: row.type,
1117
1513
  status: assertEntityStatus(row.status),
1118
- streams: {
1119
- main: `${row.url}/main`,
1120
- error: `${row.url}/error`
1121
- },
1514
+ streams: { main: `${row.url}/main` },
1122
1515
  subscription_id: row.subscriptionId,
1123
1516
  dispatch_policy: row.dispatchPolicy ?? void 0,
1124
1517
  write_token: row.writeToken,
@@ -1140,6 +1533,8 @@ var PostgresRegistry = class {
1140
1533
  sourceRef: row.sourceRef,
1141
1534
  tags: row.tags ?? {},
1142
1535
  streamUrl: row.streamUrl,
1536
+ principalUrl: row.principalUrl ?? void 0,
1537
+ principalKind: row.principalKind ?? void 0,
1143
1538
  shapeHandle: row.shapeHandle ?? void 0,
1144
1539
  shapeOffset: row.shapeOffset ?? void 0,
1145
1540
  lastObserverActivityAt: row.lastObserverActivityAt,
@@ -1294,6 +1689,93 @@ const serverLog = {
1294
1689
  }
1295
1690
  };
1296
1691
 
1692
+ //#endregion
1693
+ //#region src/principal.ts
1694
+ const ELECTRIC_PRINCIPAL_HEADER = `electric-principal`;
1695
+ const PRINCIPAL_KINDS = new Set([
1696
+ `user`,
1697
+ `agent`,
1698
+ `service`,
1699
+ `system`
1700
+ ]);
1701
+ function parsePrincipalKey(input) {
1702
+ const colon = input.indexOf(`:`);
1703
+ if (colon <= 0) throw new Error(`Invalid principal identifier`);
1704
+ const kind = input.slice(0, colon);
1705
+ const id = input.slice(colon + 1);
1706
+ if (!PRINCIPAL_KINDS.has(kind)) throw new Error(`Invalid principal kind`);
1707
+ if (!id || id.includes(`/`)) throw new Error(`Invalid principal id`);
1708
+ const key = `${kind}:${id}`;
1709
+ return {
1710
+ kind,
1711
+ id,
1712
+ key,
1713
+ url: `/principal/${encodeURIComponent(key)}`
1714
+ };
1715
+ }
1716
+ function principalUrl(key) {
1717
+ return parsePrincipalKey(key).url;
1718
+ }
1719
+ function parsePrincipalUrl(url) {
1720
+ if (!url.startsWith(`/principal/`)) return null;
1721
+ const segment = url.slice(`/principal/`.length);
1722
+ if (!segment || segment.includes(`/`)) return null;
1723
+ try {
1724
+ return parsePrincipalKey(decodeURIComponent(segment));
1725
+ } catch {
1726
+ return null;
1727
+ }
1728
+ }
1729
+ const BUILT_IN_SYSTEM_PRINCIPAL_IDS = new Set([
1730
+ `framework`,
1731
+ `auth-sync`,
1732
+ `dev-local`
1733
+ ]);
1734
+ function isBuiltInSystemPrincipalUrl(url) {
1735
+ if (!url?.startsWith(`/principal/`)) return false;
1736
+ try {
1737
+ const principal = parsePrincipalUrl(url);
1738
+ if (!principal) return false;
1739
+ return principal.kind === `system` && BUILT_IN_SYSTEM_PRINCIPAL_IDS.has(principal.id);
1740
+ } catch {
1741
+ return false;
1742
+ }
1743
+ }
1744
+ function principalFromCreatedBy(createdBy) {
1745
+ if (!createdBy) return void 0;
1746
+ const principal = parsePrincipalUrl(createdBy);
1747
+ if (!principal) return {
1748
+ url: createdBy,
1749
+ key: null
1750
+ };
1751
+ return {
1752
+ url: principal.url,
1753
+ key: principal.key,
1754
+ kind: principal.kind,
1755
+ id: principal.id
1756
+ };
1757
+ }
1758
+ const principalIdentityStateSchema = Type.Object({
1759
+ kind: Type.Union([
1760
+ Type.Literal(`user`),
1761
+ Type.Literal(`agent`),
1762
+ Type.Literal(`service`),
1763
+ Type.Literal(`system`)
1764
+ ]),
1765
+ id: Type.String(),
1766
+ key: Type.String(),
1767
+ url: Type.String(),
1768
+ updated_at: Type.String(),
1769
+ display_name: Type.Optional(Type.String()),
1770
+ email: Type.Optional(Type.String()),
1771
+ avatar_url: Type.Optional(Type.String()),
1772
+ auth_provider: Type.Optional(Type.String()),
1773
+ auth_subject: Type.Optional(Type.String()),
1774
+ claims: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
1775
+ created_at: Type.Optional(Type.String())
1776
+ }, { additionalProperties: false });
1777
+ const principalUpdateIdentityMessageSchema = Type.Object({ identity: principalIdentityStateSchema }, { additionalProperties: false });
1778
+
1297
1779
  //#endregion
1298
1780
  //#region src/entity-projector.ts
1299
1781
  const ENTITY_SHAPE_COLUMNS = [
@@ -1302,6 +1784,7 @@ const ENTITY_SHAPE_COLUMNS = [
1302
1784
  `type`,
1303
1785
  `status`,
1304
1786
  `tags`,
1787
+ `created_by`,
1305
1788
  `spawn_args`,
1306
1789
  `sandbox`,
1307
1790
  `parent`,
@@ -1321,6 +1804,12 @@ function sourceRefFromStreamPath(streamPath) {
1321
1804
  const match = streamPath.match(/^\/_entities\/([^/]+)$/);
1322
1805
  return match?.[1] ?? null;
1323
1806
  }
1807
+ function principalScopedSourceRef(tagSourceRef, principalUrl$1, principalKind) {
1808
+ return `${tagSourceRef}-${hashString(JSON.stringify({
1809
+ principalKind,
1810
+ principalUrl: principalUrl$1
1811
+ }))}`;
1812
+ }
1324
1813
  function sameMember(left, right) {
1325
1814
  return JSON.stringify(left) === JSON.stringify(right);
1326
1815
  }
@@ -1351,15 +1840,22 @@ var ProjectedEntityBridge = class {
1351
1840
  sourceRef;
1352
1841
  tags;
1353
1842
  streamUrl;
1843
+ principalUrl;
1844
+ principalKind;
1845
+ permissionBypass;
1354
1846
  currentMembers = new Map();
1355
1847
  producer = null;
1356
1848
  stopped = false;
1357
- constructor(row, streamClient) {
1849
+ constructor(row, registry, streamClient) {
1850
+ this.registry = registry;
1358
1851
  this.streamClient = streamClient;
1359
1852
  this.tenantId = row.tenantId;
1360
1853
  this.sourceRef = row.sourceRef;
1361
1854
  this.tags = normalizeTags(row.tags);
1362
1855
  this.streamUrl = row.streamUrl;
1856
+ this.principalUrl = row.principalUrl;
1857
+ this.principalKind = row.principalKind;
1858
+ this.permissionBypass = isBuiltInSystemPrincipalUrl(row.principalUrl);
1363
1859
  }
1364
1860
  async start(initialEntities) {
1365
1861
  await this.ensureStream();
@@ -1373,7 +1869,7 @@ var ProjectedEntityBridge = class {
1373
1869
  }
1374
1870
  });
1375
1871
  await this.loadCurrentMembers();
1376
- this.reconcile(initialEntities);
1872
+ await this.reconcile(initialEntities);
1377
1873
  }
1378
1874
  async stop() {
1379
1875
  this.stopped = true;
@@ -1385,12 +1881,13 @@ var ProjectedEntityBridge = class {
1385
1881
  this.producer = null;
1386
1882
  }
1387
1883
  }
1388
- reconcile(entities$1) {
1884
+ async reconcile(entities$1) {
1389
1885
  if (this.stopped) return;
1390
1886
  const staleMembers = new Map(this.currentMembers);
1391
1887
  for (const entity of entities$1) {
1392
1888
  if (entity.tenant_id !== this.tenantId) continue;
1393
1889
  if (!entityMatchesTags(entity, this.tags)) continue;
1890
+ if (!await this.canReadEntity(entity)) continue;
1394
1891
  staleMembers.delete(entity.url);
1395
1892
  this.upsertEntity(entity);
1396
1893
  }
@@ -1399,10 +1896,10 @@ var ProjectedEntityBridge = class {
1399
1896
  this.currentMembers.delete(url);
1400
1897
  }
1401
1898
  }
1402
- applyEntity(entity) {
1899
+ async applyEntity(entity) {
1403
1900
  if (this.stopped) return;
1404
1901
  if (entity.tenant_id !== this.tenantId) return;
1405
- if (!entityMatchesTags(entity, this.tags)) {
1902
+ if (!entityMatchesTags(entity, this.tags) || !await this.canReadEntity(entity)) {
1406
1903
  const existing = this.currentMembers.get(entity.url);
1407
1904
  if (!existing) return;
1408
1905
  this.append(`delete`, existing);
@@ -1431,6 +1928,15 @@ var ProjectedEntityBridge = class {
1431
1928
  this.currentMembers.set(entity.url, next);
1432
1929
  }
1433
1930
  }
1931
+ async canReadEntity(entity) {
1932
+ if (this.permissionBypass) return true;
1933
+ if (!this.principalUrl || !this.principalKind) return false;
1934
+ if (entity.created_by === this.principalUrl) return true;
1935
+ return await this.registry.hasEntityPermission(entity.url, `read`, {
1936
+ principalUrl: this.principalUrl,
1937
+ principalKind: this.principalKind
1938
+ });
1939
+ }
1434
1940
  async ensureStream() {
1435
1941
  if (!await this.streamClient.exists(this.streamUrl)) await this.streamClient.create(this.streamUrl, { contentType: `application/json` });
1436
1942
  }
@@ -1535,17 +2041,19 @@ var EntityProjector = class {
1535
2041
  this.activeReaders.clear();
1536
2042
  await Promise.all(projections.map((projection) => projection.stop()));
1537
2043
  }
1538
- async register(tenantId, registry, tagsInput) {
2044
+ async register(tenantId, registry, tagsInput, principalUrl$1, principalKind) {
1539
2045
  if (!this.electricUrl) throw new Error(`[entity-projector] Electric URL is required for entities()`);
1540
2046
  await this.start();
1541
2047
  this.registries.set(tenantId, registry);
1542
2048
  const tags = normalizeTags(assertTags(tagsInput));
1543
- const sourceRef = sourceRefForTags(tags);
2049
+ const sourceRef = principalScopedSourceRef(sourceRefForTags(tags), principalUrl$1, principalKind);
1544
2050
  const streamUrl = getEntitiesStreamPath(sourceRef);
1545
2051
  const row = await registry.upsertEntityBridge({
1546
2052
  sourceRef,
1547
2053
  tags,
1548
- streamUrl
2054
+ streamUrl,
2055
+ principalUrl: principalUrl$1,
2056
+ principalKind
1549
2057
  });
1550
2058
  await registry.touchEntityBridge(sourceRef);
1551
2059
  await this.ensureProjection(row);
@@ -1574,7 +2082,11 @@ var EntityProjector = class {
1574
2082
  await this.touchSourceRef(tenantId, registry, sourceRef, `read-close`);
1575
2083
  };
1576
2084
  }
1577
- async onEntityChanged(_tenantId, _entityUrl) {}
2085
+ async onEntityChanged(tenantId, entityUrl) {
2086
+ const entity = this.entities.get(entityKey(tenantId, entityUrl));
2087
+ if (!entity) return;
2088
+ for (const projection of this.projectionsForTenant(tenantId)) await projection.applyEntity(entity);
2089
+ }
1578
2090
  async loadTenantBridges(tenantId, registry = this.registryForTenant(tenantId)) {
1579
2091
  if (!this.started || !this.electricUrl) return;
1580
2092
  await this.loadPersistedBridgesForTenant(tenantId, registry);
@@ -1635,16 +2147,16 @@ var EntityProjector = class {
1635
2147
  }
1636
2148
  if (message.headers.control === `up-to-date`) {
1637
2149
  this.upToDate = true;
1638
- this.reconcileAll();
2150
+ await this.reconcileAll();
1639
2151
  this.readyResolve?.();
1640
2152
  }
1641
2153
  continue;
1642
2154
  }
1643
2155
  if (!isChangeMessage(message)) continue;
1644
- this.applyChangeMessage(message);
2156
+ await this.applyChangeMessage(message);
1645
2157
  }
1646
2158
  }
1647
- applyChangeMessage(message) {
2159
+ async applyChangeMessage(message) {
1648
2160
  const entity = message.value;
1649
2161
  const key = entityKey(entity.tenant_id, entity.url);
1650
2162
  if (message.headers.operation === `delete`) {
@@ -1653,7 +2165,7 @@ var EntityProjector = class {
1653
2165
  return;
1654
2166
  }
1655
2167
  this.entities.set(key, entity);
1656
- if (this.upToDate) for (const projection of this.projectionsForTenant(entity.tenant_id)) projection.applyEntity(entity);
2168
+ if (this.upToDate) for (const projection of this.projectionsForTenant(entity.tenant_id)) await projection.applyEntity(entity);
1657
2169
  }
1658
2170
  async loadPersistedBridges() {
1659
2171
  const registry = new PostgresRegistry(this.db);
@@ -1716,7 +2228,7 @@ var EntityProjector = class {
1716
2228
  }
1717
2229
  throw error;
1718
2230
  }
1719
- const projection = new ProjectedEntityBridge(row, streamClient);
2231
+ const projection = new ProjectedEntityBridge(row, this.registryForTenant(row.tenantId), streamClient);
1720
2232
  await projection.start(this.entitiesForTenant(row.tenantId));
1721
2233
  this.projections.set(key, projection);
1722
2234
  })().finally(() => {
@@ -1731,8 +2243,8 @@ var EntityProjector = class {
1731
2243
  projectionsForTenant(tenantId) {
1732
2244
  return [...this.projections.values()].filter((projection) => projection.tenantId === tenantId);
1733
2245
  }
1734
- reconcileAll() {
1735
- for (const projection of this.projections.values()) projection.reconcile(this.entitiesForTenant(projection.tenantId));
2246
+ async reconcileAll() {
2247
+ for (const projection of this.projections.values()) await projection.reconcile(this.entitiesForTenant(projection.tenantId));
1736
2248
  }
1737
2249
  async touchSourceRef(tenantId, registry, sourceRef, reason) {
1738
2250
  try {
@@ -1774,8 +2286,8 @@ var EntityProjectorTenantFacade = class {
1774
2286
  await this.projector.start();
1775
2287
  }
1776
2288
  async stop() {}
1777
- async register(tagsInput) {
1778
- return await this.projector.register(this.tenantId, this.registry, tagsInput);
2289
+ async register(tagsInput, principalUrl$1, principalKind) {
2290
+ return await this.projector.register(this.tenantId, this.registry, tagsInput, principalUrl$1, principalKind);
1779
2291
  }
1780
2292
  async onEntityChanged(entityUrl) {
1781
2293
  await this.projector.onEntityChanged(this.tenantId, entityUrl);
@@ -2657,93 +3169,6 @@ function assertSharedSandboxColocated(key, chosenIsRemote, dispatchPolicy) {
2657
3169
  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);
2658
3170
  }
2659
3171
 
2660
- //#endregion
2661
- //#region src/principal.ts
2662
- const ELECTRIC_PRINCIPAL_HEADER = `electric-principal`;
2663
- const PRINCIPAL_KINDS = new Set([
2664
- `user`,
2665
- `agent`,
2666
- `service`,
2667
- `system`
2668
- ]);
2669
- function parsePrincipalKey(input) {
2670
- const colon = input.indexOf(`:`);
2671
- if (colon <= 0) throw new Error(`Invalid principal identifier`);
2672
- const kind = input.slice(0, colon);
2673
- const id = input.slice(colon + 1);
2674
- if (!PRINCIPAL_KINDS.has(kind)) throw new Error(`Invalid principal kind`);
2675
- if (!id || id.includes(`/`)) throw new Error(`Invalid principal id`);
2676
- const key = `${kind}:${id}`;
2677
- return {
2678
- kind,
2679
- id,
2680
- key,
2681
- url: `/principal/${encodeURIComponent(key)}`
2682
- };
2683
- }
2684
- function principalUrl(key) {
2685
- return parsePrincipalKey(key).url;
2686
- }
2687
- function parsePrincipalUrl(url) {
2688
- if (!url.startsWith(`/principal/`)) return null;
2689
- const segment = url.slice(`/principal/`.length);
2690
- if (!segment || segment.includes(`/`)) return null;
2691
- try {
2692
- return parsePrincipalKey(decodeURIComponent(segment));
2693
- } catch {
2694
- return null;
2695
- }
2696
- }
2697
- const BUILT_IN_SYSTEM_PRINCIPAL_IDS = new Set([
2698
- `framework`,
2699
- `auth-sync`,
2700
- `dev-local`
2701
- ]);
2702
- function isBuiltInSystemPrincipalUrl(url) {
2703
- if (!url?.startsWith(`/principal/`)) return false;
2704
- try {
2705
- const principal = parsePrincipalUrl(url);
2706
- if (!principal) return false;
2707
- return principal.kind === `system` && BUILT_IN_SYSTEM_PRINCIPAL_IDS.has(principal.id);
2708
- } catch {
2709
- return false;
2710
- }
2711
- }
2712
- function principalFromCreatedBy(createdBy) {
2713
- if (!createdBy) return void 0;
2714
- const principal = parsePrincipalUrl(createdBy);
2715
- if (!principal) return {
2716
- url: createdBy,
2717
- key: null
2718
- };
2719
- return {
2720
- url: principal.url,
2721
- key: principal.key,
2722
- kind: principal.kind,
2723
- id: principal.id
2724
- };
2725
- }
2726
- const principalIdentityStateSchema = Type.Object({
2727
- kind: Type.Union([
2728
- Type.Literal(`user`),
2729
- Type.Literal(`agent`),
2730
- Type.Literal(`service`),
2731
- Type.Literal(`system`)
2732
- ]),
2733
- id: Type.String(),
2734
- key: Type.String(),
2735
- url: Type.String(),
2736
- updated_at: Type.String(),
2737
- display_name: Type.Optional(Type.String()),
2738
- email: Type.Optional(Type.String()),
2739
- avatar_url: Type.Optional(Type.String()),
2740
- auth_provider: Type.Optional(Type.String()),
2741
- auth_subject: Type.Optional(Type.String()),
2742
- claims: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
2743
- created_at: Type.Optional(Type.String())
2744
- }, { additionalProperties: false });
2745
- const principalUpdateIdentityMessageSchema = Type.Object({ identity: principalIdentityStateSchema }, { additionalProperties: false });
2746
-
2747
3172
  //#endregion
2748
3173
  //#region src/manifest-side-effects.ts
2749
3174
  function isRecord$1(value) {
@@ -2939,6 +3364,7 @@ var EntityManager = class {
2939
3364
  this.validateSchema(req.creation_schema);
2940
3365
  this.validateSchemaMap(req.inbox_schemas);
2941
3366
  this.validateSchemaMap(req.state_schemas);
3367
+ this.validateSlashCommands(req.slash_commands);
2942
3368
  const defaultDispatchPolicy = req.default_dispatch_policy ? this.validateDispatchPolicy(req.default_dispatch_policy, { label: `default_dispatch_policy` }) : void 0;
2943
3369
  const existing = await this.registry.getEntityType(req.name);
2944
3370
  const now = new Date().toISOString();
@@ -2948,6 +3374,7 @@ var EntityManager = class {
2948
3374
  creation_schema: req.creation_schema,
2949
3375
  inbox_schemas: req.inbox_schemas,
2950
3376
  state_schemas: req.state_schemas,
3377
+ slash_commands: req.slash_commands,
2951
3378
  serve_endpoint: req.serve_endpoint,
2952
3379
  default_dispatch_policy: defaultDispatchPolicy,
2953
3380
  revision: existing ? existing.revision + 1 : 1,
@@ -2979,7 +3406,10 @@ var EntityManager = class {
2979
3406
  }
2980
3407
  async ensurePrincipal(principal) {
2981
3408
  const existing = await this.registry.getEntity(principal.url);
2982
- if (existing) return existing;
3409
+ if (existing) {
3410
+ await this.ensureUserPrincipal(principal);
3411
+ return existing;
3412
+ }
2983
3413
  await this.ensurePrincipalEntityType();
2984
3414
  try {
2985
3415
  const entity = await this.spawn(`principal`, {
@@ -3008,15 +3438,22 @@ var EntityManager = class {
3008
3438
  updated_at: now
3009
3439
  }
3010
3440
  }));
3441
+ await this.ensureUserPrincipal(principal);
3011
3442
  return entity;
3012
3443
  } catch (error) {
3013
3444
  if (error instanceof ElectricAgentsError && error.code === ErrCodeDuplicateURL) {
3014
3445
  const raced = await this.registry.getEntity(principal.url);
3015
- if (raced) return raced;
3446
+ if (raced) {
3447
+ await this.ensureUserPrincipal(principal);
3448
+ return raced;
3449
+ }
3016
3450
  }
3017
3451
  throw error;
3018
3452
  }
3019
3453
  }
3454
+ async ensureUserPrincipal(principal) {
3455
+ if (principal.kind === `user`) await this.registry.ensureUserForPrincipal(principal);
3456
+ }
3020
3457
  /**
3021
3458
  * Spawn a new entity of the given type with durable streams.
3022
3459
  */
@@ -3046,7 +3483,6 @@ var EntityManager = class {
3046
3483
  const writeToken = randomUUID();
3047
3484
  const entityURL = typeName === `principal` ? principalUrl(instanceId) : `/${typeName}/${instanceId}`;
3048
3485
  const mainPath = `${entityURL}/main`;
3049
- const errorPath = `${entityURL}/error`;
3050
3486
  const subscriptionId = `${typeName}-handler`;
3051
3487
  const spawnT0 = performance.now();
3052
3488
  const existingByURL = await this.registry.getEntity(entityURL);
@@ -3063,10 +3499,7 @@ var EntityManager = class {
3063
3499
  type: typeName,
3064
3500
  status: `idle`,
3065
3501
  url: entityURL,
3066
- streams: {
3067
- main: mainPath,
3068
- error: errorPath
3069
- },
3502
+ streams: { main: mainPath },
3070
3503
  subscription_id: subscriptionId,
3071
3504
  dispatch_policy: dispatchPolicy,
3072
3505
  write_token: writeToken,
@@ -3103,6 +3536,18 @@ var EntityManager = class {
3103
3536
  }
3104
3537
  });
3105
3538
  const initialEvents = [createdEvent];
3539
+ const slashCommandTimestamp = new Date().toISOString();
3540
+ for (const command of entityType.slash_commands ?? []) {
3541
+ const slashCommandEvent = entityStateSchema.slashCommands.insert({
3542
+ key: command.name,
3543
+ value: {
3544
+ ...command,
3545
+ source: `static`,
3546
+ updated_at: slashCommandTimestamp
3547
+ }
3548
+ });
3549
+ initialEvents.push(slashCommandEvent);
3550
+ }
3106
3551
  if (req.initialMessage !== void 0) {
3107
3552
  const msgNow = new Date().toISOString();
3108
3553
  const inboxEvent = entityStateSchema.inbox.insert({
@@ -3110,6 +3555,7 @@ var EntityManager = class {
3110
3555
  value: {
3111
3556
  from: req.created_by ?? req.parent ?? `spawn`,
3112
3557
  payload: req.initialMessage,
3558
+ message_type: req.initialMessageType,
3113
3559
  timestamp: msgNow
3114
3560
  }
3115
3561
  });
@@ -3119,55 +3565,43 @@ var EntityManager = class {
3119
3565
  const queueEnterT0 = performance.now();
3120
3566
  const queueWaiting = this.spawnPersistQueue.length();
3121
3567
  const queueRunning = this.spawnPersistQueue.running();
3122
- const [mainStreamResult, errorStreamResult, entityResult] = await this.spawnPersistQueue.push(async () => {
3568
+ const [mainStreamResult, entityResult] = await this.spawnPersistQueue.push(async () => {
3123
3569
  let entityTxid;
3124
3570
  try {
3125
3571
  entityTxid = await withSpan(`db.createEntity`, () => this.registry.createEntity(entityData));
3126
3572
  } catch (err) {
3127
- return [
3128
- {
3129
- status: `fulfilled`,
3130
- value: void 0
3131
- },
3132
- {
3133
- status: `fulfilled`,
3134
- value: void 0
3135
- },
3136
- {
3137
- status: `rejected`,
3138
- reason: err
3139
- }
3140
- ];
3573
+ return [{
3574
+ status: `fulfilled`,
3575
+ value: void 0
3576
+ }, {
3577
+ status: `rejected`,
3578
+ reason: err
3579
+ }];
3141
3580
  }
3142
- const [mainStreamResult$1, errorStreamResult$1] = await Promise.allSettled([this.streamClient.create(mainPath, {
3581
+ const [mainStreamResult$1] = await Promise.allSettled([this.streamClient.create(mainPath, {
3143
3582
  contentType,
3144
3583
  body: initialBody
3145
- }), this.streamClient.create(errorPath, { contentType })]);
3146
- return [
3147
- mainStreamResult$1,
3148
- errorStreamResult$1,
3149
- {
3150
- status: `fulfilled`,
3151
- value: entityTxid
3152
- }
3153
- ];
3584
+ })]);
3585
+ return [mainStreamResult$1, {
3586
+ status: `fulfilled`,
3587
+ value: entityTxid
3588
+ }];
3154
3589
  });
3155
3590
  const parallelMs = +(performance.now() - queueEnterT0).toFixed(2);
3156
- if (mainStreamResult.status === `rejected` || errorStreamResult.status === `rejected` || entityResult.status === `rejected`) {
3591
+ if (mainStreamResult.status === `rejected` || entityResult.status === `rejected`) {
3157
3592
  const entityReason = entityResult.status === `rejected` ? entityResult.reason : null;
3158
- const streamReason = mainStreamResult.status === `rejected` ? mainStreamResult.reason : errorStreamResult.status === `rejected` ? errorStreamResult.reason : null;
3593
+ const streamReason = mainStreamResult.status === `rejected` ? mainStreamResult.reason : null;
3159
3594
  const isDuplicate = entityReason instanceof EntityAlreadyExistsError;
3160
3595
  const isStreamConflict = !!streamReason && typeof streamReason === `object` && (`status` in streamReason && streamReason.status === 409 || `code` in streamReason && streamReason.code === `CONFLICT_SEQ`);
3161
3596
  const rollbacks = [];
3162
3597
  if (!isDuplicate && !isStreamConflict) {
3163
3598
  if (mainStreamResult.status === `fulfilled`) rollbacks.push(this.streamClient.delete(mainPath));
3164
- if (errorStreamResult.status === `fulfilled`) rollbacks.push(this.streamClient.delete(errorPath));
3165
3599
  if (entityResult.status === `fulfilled`) rollbacks.push(this.registry.deleteEntity(entityURL));
3166
3600
  if (req.wake) rollbacks.push(this.wakeRegistry.unregisterBySubscriberAndSource(req.wake.subscriberUrl, entityURL, this.tenantId));
3167
3601
  await Promise.allSettled(rollbacks);
3168
3602
  }
3169
3603
  if (isDuplicate || isStreamConflict) throw new ElectricAgentsError(ErrCodeDuplicateURL, `Entity already exists at URL "${entityURL}"`, 409);
3170
- const failure = mainStreamResult.status === `rejected` ? mainStreamResult.reason : errorStreamResult.status === `rejected` ? errorStreamResult.reason : entityResult.reason;
3604
+ const failure = mainStreamResult.status === `rejected` ? mainStreamResult.reason : entityResult.reason;
3171
3605
  if (failure instanceof Error) throw failure;
3172
3606
  throw new ElectricAgentsError(`SPAWN_FAILED`, `Spawn failed: ${String(failure)}`, 500);
3173
3607
  }
@@ -3252,7 +3686,7 @@ var EntityManager = class {
3252
3686
  });
3253
3687
  const sharedStateIdMap = await this.buildForkSharedStateIdMap(snapshot.sharedStateIds, suffix);
3254
3688
  const stringMap = this.buildForkStringMap(entityUrlMap, sharedStateIdMap);
3255
- const entityPlans = this.buildForkEntityPlans(effectiveSubtree, entityUrlMap, stringMap);
3689
+ const entityPlans = this.buildForkEntityPlans(effectiveSubtree, entityUrlMap, stringMap, opts.createdBy);
3256
3690
  this.addForkLocks(this.forkWriteLockedEntities, effectiveSubtree.map((entity) => entity.url), writeEntityLocks);
3257
3691
  this.addForkLocks(this.forkWriteLockedStreams, [...snapshot.sharedStateIds].map((id) => getSharedStateStreamPath(id)), writeStreamLocks);
3258
3692
  const createdStreams = [];
@@ -3263,8 +3697,6 @@ var EntityManager = class {
3263
3697
  const isRoot = plan.source.url === rootUrl;
3264
3698
  await this.streamClient.fork(plan.fork.streams.main, plan.source.streams.main, isRoot && opts.forkPointer ? { forkPointer: opts.forkPointer } : void 0);
3265
3699
  createdStreams.push(plan.fork.streams.main);
3266
- await this.streamClient.fork(plan.fork.streams.error, plan.source.streams.error);
3267
- createdStreams.push(plan.fork.streams.error);
3268
3700
  }
3269
3701
  for (const [sourceId, forkId] of sharedStateIdMap) {
3270
3702
  const sourcePath = getSharedStateStreamPath(sourceId);
@@ -3598,7 +4030,6 @@ var EntityManager = class {
3598
4030
  for (const [sourceUrl, forkUrl] of entityUrlMap) {
3599
4031
  stringMap.set(sourceUrl, forkUrl);
3600
4032
  stringMap.set(`${sourceUrl}/main`, `${forkUrl}/main`);
3601
- stringMap.set(`${sourceUrl}/error`, `${forkUrl}/error`);
3602
4033
  }
3603
4034
  for (const [sourceId, forkId] of sharedStateIdMap) {
3604
4035
  stringMap.set(sourceId, forkId);
@@ -3606,7 +4037,7 @@ var EntityManager = class {
3606
4037
  }
3607
4038
  return stringMap;
3608
4039
  }
3609
- buildForkEntityPlans(entitiesToFork, entityUrlMap, stringMap) {
4040
+ buildForkEntityPlans(entitiesToFork, entityUrlMap, stringMap, createdBy) {
3610
4041
  const now = Date.now();
3611
4042
  return entitiesToFork.map((source) => {
3612
4043
  const forkUrl = entityUrlMap.get(source.url);
@@ -3619,14 +4050,12 @@ var EntityManager = class {
3619
4050
  url: forkUrl,
3620
4051
  type,
3621
4052
  status: `idle`,
3622
- streams: {
3623
- main: `${forkUrl}/main`,
3624
- error: `${forkUrl}/error`
3625
- },
4053
+ streams: { main: `${forkUrl}/main` },
3626
4054
  subscription_id: `${type}-handler`,
3627
4055
  write_token: randomUUID(),
3628
4056
  spawn_args: spawnArgs,
3629
4057
  parent,
4058
+ created_by: createdBy ?? source.created_by,
3630
4059
  created_at: now,
3631
4060
  updated_at: now
3632
4061
  };
@@ -3860,7 +4289,7 @@ var EntityManager = class {
3860
4289
  }
3861
4290
  async materializeForkManifestSideEffects(entityUrl, manifests) {
3862
4291
  for (const [manifestKey, manifest] of manifests) {
3863
- await this.syncEntitiesManifestSource(entityUrl, manifestKey, `upsert`, manifest);
4292
+ await this.syncManifestLinks(entityUrl, manifestKey, `upsert`, manifest);
3864
4293
  const wake = buildManifestWakeRegistration(entityUrl, manifest, manifestKey);
3865
4294
  if (wake) await this.wakeRegistry.register({
3866
4295
  ...wake,
@@ -3890,6 +4319,7 @@ var EntityManager = class {
3890
4319
  await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
3891
4320
  entityUrl: targetUrl,
3892
4321
  from: senderUrl,
4322
+ from_agent: senderUrl,
3893
4323
  payload: manifest.payload,
3894
4324
  key: `scheduled-${producerId}`,
3895
4325
  type: typeof manifest.messageType === `string` ? manifest.messageType : void 0,
@@ -3929,12 +4359,14 @@ var EntityManager = class {
3929
4359
  const now = new Date().toISOString();
3930
4360
  const key = req.key ?? `msg-in-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
3931
4361
  const value = {
3932
- from: req.from,
4362
+ from: req.from_principal ?? req.from,
3933
4363
  payload: req.payload,
3934
4364
  timestamp: now,
3935
4365
  mode: req.mode ?? `immediate`,
3936
4366
  status: req.mode === `queued` || req.mode === `paused` ? `pending` : `processed`
3937
4367
  };
4368
+ if (req.from_principal) value.from_principal = req.from_principal;
4369
+ if (req.from_agent) value.from_agent = req.from_agent;
3938
4370
  if (req.type) value.message_type = req.type;
3939
4371
  if (req.position) value.position = req.position;
3940
4372
  else if (value.mode === `queued` || value.mode === `paused`) value.position = createInitialQueuePosition(new Date(now));
@@ -4106,9 +4538,9 @@ var EntityManager = class {
4106
4538
  if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
4107
4539
  return updated;
4108
4540
  }
4109
- async ensureEntitiesMembershipStream(tags) {
4541
+ async ensureEntitiesMembershipStream(tags, principal) {
4110
4542
  if (!this.entityBridgeManager) throw new Error(`Entity bridge manager not configured`);
4111
- return this.entityBridgeManager.register(this.validateTags(tags));
4543
+ return this.entityBridgeManager.register(this.validateTags(tags), principal.url, principal.kind);
4112
4544
  }
4113
4545
  async writeManifestEntry(entityUrl, key, operation, value, opts) {
4114
4546
  const entity = await this.registry.getEntity(entityUrl);
@@ -4126,11 +4558,11 @@ var EntityManager = class {
4126
4558
  const encoded = this.encodeChangeEvent(event);
4127
4559
  if (opts?.producerId) {
4128
4560
  await this.streamClient.appendIdempotent(entity.streams.main, encoded, { producerId: opts.producerId });
4129
- await this.syncEntitiesManifestSource(entityUrl, key, operation, value);
4561
+ await this.syncManifestLinks(entityUrl, key, operation, value);
4130
4562
  return;
4131
4563
  }
4132
4564
  await this.streamClient.append(entity.streams.main, encoded);
4133
- await this.syncEntitiesManifestSource(entityUrl, key, operation, value);
4565
+ await this.syncManifestLinks(entityUrl, key, operation, value);
4134
4566
  }
4135
4567
  async upsertCronSchedule(entityUrl, req) {
4136
4568
  if (req.payload === void 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: payload`, 400);
@@ -4279,6 +4711,8 @@ var EntityManager = class {
4279
4711
  await this.scheduler.enqueueDelayedSend({
4280
4712
  entityUrl,
4281
4713
  from: req.from,
4714
+ from_principal: req.from_principal,
4715
+ from_agent: req.from_agent,
4282
4716
  payload: req.payload,
4283
4717
  key: req.key,
4284
4718
  type: req.type,
@@ -4321,14 +4755,23 @@ var EntityManager = class {
4321
4755
  await this.streamClient.appendIdempotent(subscriber.streams.main, this.encodeChangeEvent(wakeEvent), { producerId: `wake-reg-${result.registrationDbId}-${result.sourceEventKey}` });
4322
4756
  });
4323
4757
  }
4324
- async syncEntitiesManifestSource(entityUrl, manifestKey, operation, value) {
4758
+ async syncManifestLinks(entityUrl, manifestKey, operation, value) {
4325
4759
  const sourceRef = operation === `delete` ? void 0 : this.extractEntitiesSourceRef(value);
4326
4760
  await this.registry.replaceEntityManifestSource(entityUrl, manifestKey, sourceRef);
4761
+ const sharedStateId = operation === `delete` ? void 0 : this.extractSharedStateId(value);
4762
+ await this.registry.replaceSharedStateLink(entityUrl, manifestKey, sharedStateId);
4327
4763
  }
4328
4764
  extractEntitiesSourceRef(manifest) {
4329
4765
  if (manifest?.kind === `source` && manifest.sourceType === `entities` && typeof manifest.sourceRef === `string`) return manifest.sourceRef;
4330
4766
  return void 0;
4331
4767
  }
4768
+ extractSharedStateId(manifest) {
4769
+ if (manifest?.kind === `shared-state` && typeof manifest.id === `string`) return manifest.id;
4770
+ if (manifest?.kind !== `source` || manifest.sourceType !== `db`) return void 0;
4771
+ if (typeof manifest.sourceRef === `string`) return manifest.sourceRef;
4772
+ const config = isRecord(manifest.config) ? manifest.config : void 0;
4773
+ return typeof config?.id === `string` ? config.id : void 0;
4774
+ }
4332
4775
  /**
4333
4776
  * Read a child entity's stream and extract concatenated text deltas
4334
4777
  * for a specific run, plus any error messages for that run.
@@ -4492,14 +4935,7 @@ var EntityManager = class {
4492
4935
  await this.streamClient.append(entity.streams.main, signalData);
4493
4936
  return;
4494
4937
  }
4495
- const errorCloseEvent = {
4496
- type: `signal`,
4497
- key: signalEvent.key,
4498
- value: signalEvent.value,
4499
- headers: signalEvent.headers
4500
- };
4501
- const errorSignalData = this.encodeChangeEvent(errorCloseEvent);
4502
- for (const [streamPath, data] of [[entity.streams.main, signalData], [entity.streams.error, errorSignalData]]) try {
4938
+ for (const [streamPath, data] of [[entity.streams.main, signalData]]) try {
4503
4939
  await this.streamClient.append(streamPath, data, { close: true });
4504
4940
  } catch (err) {
4505
4941
  const message = err instanceof Error ? err.message : String(err);
@@ -4565,7 +5001,9 @@ var EntityManager = class {
4565
5001
  creation_schema: existing.creation_schema,
4566
5002
  inbox_schemas: mergedInbox,
4567
5003
  state_schemas: mergedState,
5004
+ slash_commands: existing.slash_commands,
4568
5005
  serve_endpoint: existing.serve_endpoint,
5006
+ default_dispatch_policy: existing.default_dispatch_policy,
4569
5007
  revision: nextRevision,
4570
5008
  created_at: existing.created_at,
4571
5009
  updated_at: now
@@ -4619,11 +5057,19 @@ var EntityManager = class {
4619
5057
  throw new ElectricAgentsError(ErrCodeInvalidRequest, error instanceof Error ? error.message : `Invalid tags`, 400);
4620
5058
  }
4621
5059
  }
5060
+ validateSlashCommands(input) {
5061
+ const validationError = validateSlashCommandDefinitions(input);
5062
+ if (!validationError) return;
5063
+ throw new ElectricAgentsError(ErrCodeSchemaValidationFailed, validationError.message, 422, validationError.details);
5064
+ }
4622
5065
  async validateSendRequest(entityUrl, req) {
4623
5066
  const entity = await this.registry.getEntity(entityUrl);
4624
5067
  if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
4625
5068
  if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
4626
- if (req.type && entity.type) {
5069
+ if (req.type === COMPOSER_INPUT_MESSAGE_TYPE) {
5070
+ const valErr = validateComposerInputPayload(req.payload);
5071
+ if (valErr) throw new ElectricAgentsError(ErrCodeSchemaValidationFailed, valErr.message, 422, valErr.details);
5072
+ } else if (req.type && entity.type) {
4627
5073
  const { inboxSchemas } = await this.getEffectiveSchemas(entity);
4628
5074
  if (inboxSchemas) {
4629
5075
  const schema = inboxSchemas[req.type];
@@ -5482,6 +5928,8 @@ var ElectricAgentsTenantRuntime = class {
5482
5928
  try {
5483
5929
  await this.manager.send(payload.entityUrl, {
5484
5930
  from: payload.from,
5931
+ from_principal: payload.from_principal,
5932
+ from_agent: payload.from_agent,
5485
5933
  payload: payload.payload,
5486
5934
  key: payload.key ?? `scheduled-task-${taskId}`,
5487
5935
  type: payload.type
@@ -5554,6 +6002,7 @@ var ElectricAgentsTenantRuntime = class {
5554
6002
  await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
5555
6003
  entityUrl: targetUrl,
5556
6004
  from: senderUrl,
6005
+ from_agent: senderUrl,
5557
6006
  payload: value.payload,
5558
6007
  key: `scheduled-${producerId}`,
5559
6008
  type: typeof value.messageType === `string` ? value.messageType : void 0,
@@ -5578,11 +6027,20 @@ var ElectricAgentsTenantRuntime = class {
5578
6027
  async applyManifestEntitySource(ownerEntityUrl, manifestKey, operation, value) {
5579
6028
  const sourceRef = operation === `delete` ? void 0 : this.extractEntitiesSourceRef(value);
5580
6029
  await this.manager.registry.replaceEntityManifestSource(ownerEntityUrl, manifestKey, sourceRef);
6030
+ const sharedStateId = operation === `delete` ? void 0 : this.extractSharedStateId(value);
6031
+ await this.manager.registry.replaceSharedStateLink(ownerEntityUrl, manifestKey, sharedStateId);
5581
6032
  }
5582
6033
  extractEntitiesSourceRef(manifest) {
5583
6034
  if (manifest?.kind === `source` && manifest.sourceType === `entities` && typeof manifest.sourceRef === `string`) return manifest.sourceRef;
5584
6035
  return void 0;
5585
6036
  }
6037
+ extractSharedStateId(manifest) {
6038
+ if (manifest?.kind === `shared-state` && typeof manifest.id === `string`) return manifest.id;
6039
+ if (manifest?.kind !== `source` || manifest.sourceType !== `db`) return void 0;
6040
+ if (typeof manifest.sourceRef === `string`) return manifest.sourceRef;
6041
+ const config = typeof manifest.config === `object` && manifest.config !== null && !Array.isArray(manifest.config) ? manifest.config : void 0;
6042
+ return typeof config?.id === `string` ? config.id : void 0;
6043
+ }
5586
6044
  async maybeMarkEntityIdleAfterRunFinished(entityUrl) {
5587
6045
  const primaryStream = `${entityUrl}/main`;
5588
6046
  const callbacks = await this.db.select().from(consumerCallbacks).where(and(eq(consumerCallbacks.tenantId, this.serviceId), eq(consumerCallbacks.primaryStream, primaryStream))).limit(1);
@@ -6263,6 +6721,8 @@ var WakeRegistry = class {
6263
6721
  if (eventType === `inbox`) {
6264
6722
  const value = event.value;
6265
6723
  if (typeof value?.from === `string`) change.from = value.from;
6724
+ if (typeof value?.from_principal === `string`) change.from_principal = value.from_principal;
6725
+ if (typeof value?.from_agent === `string`) change.from_agent = value.from_agent;
6266
6726
  if (`payload` in (value ?? {})) change.payload = value?.payload;
6267
6727
  if (typeof value?.timestamp === `string`) change.timestamp = value.timestamp;
6268
6728
  if (typeof value?.message_type === `string`) change.message_type = value.message_type;
@@ -6674,29 +7134,136 @@ function buildElectricProxyTarget(options) {
6674
7134
  if (options.electricSecret) target.searchParams.set(`secret`, options.electricSecret);
6675
7135
  const table = options.incomingUrl.searchParams.get(`table`);
6676
7136
  if (table === `entities`) {
6677
- 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"`);
6678
- applyTenantShapeWhere(target, options.tenantId);
7137
+ 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"`);
7138
+ applyShapeWhere(target, buildReadableEntitiesWhere({
7139
+ tenantId: options.tenantId,
7140
+ principalUrl: options.principalUrl ?? ``,
7141
+ principalKind: options.principalKind ?? ``,
7142
+ permissionBypass: options.permissionBypass
7143
+ }));
6679
7144
  } else if (table === `entity_types`) {
6680
- target.searchParams.set(`columns`, `"tenant_id","name","description","creation_schema","inbox_schemas","state_schemas","serve_endpoint","default_dispatch_policy","revision","created_at","updated_at"`);
6681
- applyTenantShapeWhere(target, options.tenantId);
7145
+ 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"`);
7146
+ applyShapeWhere(target, buildSpawnableEntityTypesWhere({
7147
+ tenantId: options.tenantId,
7148
+ principalUrl: options.principalUrl ?? ``,
7149
+ principalKind: options.principalKind ?? ``,
7150
+ permissionBypass: options.permissionBypass
7151
+ }));
6682
7152
  } else if (table === `runners`) {
6683
7153
  target.searchParams.set(`columns`, `"tenant_id","id","owner_principal","label","kind","admin_status","wake_stream","sandbox_profiles","created_at","updated_at"`);
6684
7154
  applyTenantShapeWhere(target, options.tenantId, [`owner_principal = ${sqlStringLiteral(options.principalUrl ?? ``)}`]);
7155
+ } else if (table === `users`) {
7156
+ target.searchParams.set(`columns`, `"tenant_id","id","display_name","email","avatar_url","created_at","updated_at"`);
7157
+ applyTenantShapeWhere(target, options.tenantId);
7158
+ } else if (table === `entity_effective_permissions`) {
7159
+ target.searchParams.set(`columns`, `"tenant_id","id","entity_url","source_entity_url","source_grant_id","permission","subject_kind","subject_value","expires_at","created_at"`);
7160
+ applyShapeWhere(target, buildCurrentPrincipalEntityEffectivePermissionsWhere({
7161
+ tenantId: options.tenantId,
7162
+ principalUrl: options.principalUrl ?? ``,
7163
+ principalKind: options.principalKind ?? ``,
7164
+ permissionBypass: options.permissionBypass
7165
+ }));
6685
7166
  } else if (table === `runner_runtime_diagnostics`) {
6686
7167
  target.searchParams.set(`columns`, `"tenant_id","runner_id","owner_principal","wake_stream_offset","last_seen_at","liveness_lease_expires_at","diagnostics","updated_at"`);
6687
7168
  applyTenantShapeWhere(target, options.tenantId, [`owner_principal = ${sqlStringLiteral(options.principalUrl ?? ``)}`]);
6688
7169
  } else if (table === `entity_dispatch_state`) {
6689
7170
  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"`);
6690
- applyTenantShapeWhere(target, options.tenantId);
7171
+ applyShapeWhere(target, buildReadableEntityUrlWhere({
7172
+ tenantId: options.tenantId,
7173
+ principalUrl: options.principalUrl ?? ``,
7174
+ principalKind: options.principalKind ?? ``,
7175
+ permissionBypass: options.permissionBypass
7176
+ }));
6691
7177
  } else if (table === `wake_notifications`) {
6692
7178
  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"`);
6693
- applyTenantShapeWhere(target, options.tenantId);
7179
+ applyShapeWhere(target, buildReadableEntityUrlWhere({
7180
+ tenantId: options.tenantId,
7181
+ principalUrl: options.principalUrl ?? ``,
7182
+ principalKind: options.principalKind ?? ``,
7183
+ permissionBypass: options.permissionBypass
7184
+ }));
6694
7185
  } else if (table === `consumer_claims`) {
6695
7186
  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"`);
6696
- applyTenantShapeWhere(target, options.tenantId);
7187
+ applyShapeWhere(target, buildReadableEntityUrlWhere({
7188
+ tenantId: options.tenantId,
7189
+ principalUrl: options.principalUrl ?? ``,
7190
+ principalKind: options.principalKind ?? ``,
7191
+ permissionBypass: options.permissionBypass
7192
+ }));
6697
7193
  }
6698
7194
  return target;
6699
7195
  }
7196
+ function buildReadableEntitiesWhere(options) {
7197
+ const tenant = sqlStringLiteral(options.tenantId);
7198
+ if (options.permissionBypass) return `tenant_id = ${tenant}`;
7199
+ const principalUrl$1 = sqlStringLiteral(options.principalUrl);
7200
+ const principalKind = sqlStringLiteral(options.principalKind);
7201
+ return [
7202
+ `tenant_id = ${tenant}`,
7203
+ `AND (`,
7204
+ ` created_by = ${principalUrl$1}`,
7205
+ ` OR url IN (`,
7206
+ ` SELECT entity_url`,
7207
+ ` FROM entity_effective_permissions`,
7208
+ ` WHERE tenant_id = ${tenant}`,
7209
+ ` AND permission IN ('read', 'manage')`,
7210
+ ` AND (`,
7211
+ ` (subject_kind = 'principal' AND subject_value = ${principalUrl$1})`,
7212
+ ` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
7213
+ ` )`,
7214
+ ` )`,
7215
+ `)`
7216
+ ].join(`\n`);
7217
+ }
7218
+ function buildReadableEntityUrlWhere(options) {
7219
+ const tenant = sqlStringLiteral(options.tenantId);
7220
+ if (options.permissionBypass) return `tenant_id = ${tenant}`;
7221
+ return [
7222
+ `tenant_id = ${tenant}`,
7223
+ `AND entity_url IN (`,
7224
+ ` SELECT url`,
7225
+ ` FROM entities`,
7226
+ ` WHERE ${indentWhere(buildReadableEntitiesWhere(options), ` `).trimStart()}`,
7227
+ `)`
7228
+ ].join(`\n`);
7229
+ }
7230
+ function buildCurrentPrincipalEntityEffectivePermissionsWhere(options) {
7231
+ const tenant = sqlStringLiteral(options.tenantId);
7232
+ if (options.permissionBypass) return `tenant_id = ${tenant}`;
7233
+ const principalUrl$1 = sqlStringLiteral(options.principalUrl);
7234
+ const principalKind = sqlStringLiteral(options.principalKind);
7235
+ return [
7236
+ `tenant_id = ${tenant}`,
7237
+ `AND (`,
7238
+ ` (subject_kind = 'principal' AND subject_value = ${principalUrl$1})`,
7239
+ ` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
7240
+ `)`,
7241
+ `AND entity_url IN (`,
7242
+ ` SELECT url`,
7243
+ ` FROM entities`,
7244
+ ` WHERE ${buildReadableEntitiesWhere(options)}`,
7245
+ `)`
7246
+ ].join(`\n`);
7247
+ }
7248
+ function buildSpawnableEntityTypesWhere(options) {
7249
+ const tenant = sqlStringLiteral(options.tenantId);
7250
+ if (options.permissionBypass) return `tenant_id = ${tenant}`;
7251
+ const principalUrl$1 = sqlStringLiteral(options.principalUrl);
7252
+ const principalKind = sqlStringLiteral(options.principalKind);
7253
+ return [
7254
+ `tenant_id = ${tenant}`,
7255
+ `AND name IN (`,
7256
+ ` SELECT entity_type`,
7257
+ ` FROM entity_type_permission_grants`,
7258
+ ` WHERE tenant_id = ${tenant}`,
7259
+ ` AND permission IN ('spawn', 'manage')`,
7260
+ ` AND (`,
7261
+ ` (subject_kind = 'principal' AND subject_value = ${principalUrl$1})`,
7262
+ ` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
7263
+ ` )`,
7264
+ `)`
7265
+ ].join(`\n`);
7266
+ }
6700
7267
  async function forwardFetchRequest(options) {
6701
7268
  const routingAdapter = resolveDurableStreamsRoutingAdapter(options.durableStreamsRouting, options.durableStreamsUrl);
6702
7269
  const routingInput = {
@@ -6731,13 +7298,170 @@ function decodeJsonObject(body) {
6731
7298
  return null;
6732
7299
  }
6733
7300
  function applyTenantShapeWhere(target, tenantId, extraConditions = []) {
6734
- const tenantWhere = [`tenant_id = ${sqlStringLiteral(tenantId)}`, ...extraConditions].join(` AND `);
7301
+ applyShapeWhere(target, [`tenant_id = ${sqlStringLiteral(tenantId)}`, ...extraConditions].join(` AND `));
7302
+ }
7303
+ function applyShapeWhere(target, enforcedWhere) {
6735
7304
  const existingWhere = target.searchParams.get(`where`);
6736
- target.searchParams.set(`where`, existingWhere ? `${tenantWhere} AND (${existingWhere})` : tenantWhere);
7305
+ target.searchParams.set(`where`, existingWhere ? `${enforcedWhere} AND (${existingWhere})` : enforcedWhere);
6737
7306
  }
6738
7307
  function sqlStringLiteral(value) {
6739
7308
  return `'${value.replace(/'/g, `''`)}'`;
6740
7309
  }
7310
+ function indentWhere(where, prefix) {
7311
+ return where.split(`\n`).map((line) => `${prefix}${line}`).join(`\n`);
7312
+ }
7313
+
7314
+ //#endregion
7315
+ //#region src/permissions.ts
7316
+ const authzDecisionCache = new WeakMap();
7317
+ function principalSubject(principal) {
7318
+ return {
7319
+ principalUrl: principal.url,
7320
+ principalKind: principal.kind
7321
+ };
7322
+ }
7323
+ function isPermissionBypassPrincipal(ctx) {
7324
+ return isBuiltInSystemPrincipalUrl(ctx.principal.url);
7325
+ }
7326
+ async function canAccessEntity(ctx, entity, permission, request) {
7327
+ if (isPermissionBypassPrincipal(ctx)) return true;
7328
+ await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
7329
+ const builtInAllowed = entity.created_by === ctx.principal.url || await ctx.entityManager.registry.hasEntityPermission(entity.url, permission, principalSubject(ctx.principal));
7330
+ return await applyAuthorizationHook(ctx, {
7331
+ verb: permission,
7332
+ resourceKey: `entity:${entity.url}`,
7333
+ resource: {
7334
+ kind: `entity`,
7335
+ entity
7336
+ },
7337
+ builtInAllowed,
7338
+ request
7339
+ });
7340
+ }
7341
+ async function canAccessEntityType(ctx, entityType, permission, request) {
7342
+ if (isPermissionBypassPrincipal(ctx)) return true;
7343
+ await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
7344
+ const builtInAllowed = await ctx.entityManager.registry.hasEntityTypePermission(entityType.name, permission, principalSubject(ctx.principal));
7345
+ return await applyAuthorizationHook(ctx, {
7346
+ verb: permission,
7347
+ resourceKey: `entity_type:${entityType.name}`,
7348
+ resource: {
7349
+ kind: `entity_type`,
7350
+ entityType
7351
+ },
7352
+ builtInAllowed,
7353
+ request
7354
+ });
7355
+ }
7356
+ async function canRegisterEntityType(ctx, input, request) {
7357
+ if (isPermissionBypassPrincipal(ctx)) return true;
7358
+ return await applyAuthorizationHook(ctx, {
7359
+ verb: `manage`,
7360
+ resourceKey: `entity_type_registration:${input.name}`,
7361
+ resource: {
7362
+ kind: `entity_type_registration`,
7363
+ entityTypeName: input.name
7364
+ },
7365
+ builtInAllowed: true,
7366
+ request
7367
+ });
7368
+ }
7369
+ async function canAccessSharedState(ctx, sharedStateId, permission, request, ownerEntityUrl) {
7370
+ if (isPermissionBypassPrincipal(ctx)) return true;
7371
+ await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
7372
+ const storedLinkedEntityUrls = await ctx.entityManager.registry.listSharedStateLinkedEntityUrls(sharedStateId);
7373
+ const bootstrapEntityUrls = storedLinkedEntityUrls.length === 0 && ownerEntityUrl ? [ownerEntityUrl] : [];
7374
+ const linkedEntityUrls = [...new Set([...storedLinkedEntityUrls, ...bootstrapEntityUrls])];
7375
+ for (const entityUrl of linkedEntityUrls) {
7376
+ const entity = await ctx.entityManager.registry.getEntity(entityUrl);
7377
+ if (!entity) continue;
7378
+ if (entity.created_by === ctx.principal.url || await ctx.entityManager.registry.hasEntityPermission(entity.url, permission, principalSubject(ctx.principal))) return await applyAuthorizationHook(ctx, {
7379
+ verb: permission,
7380
+ resourceKey: `shared_state:${sharedStateId}`,
7381
+ resource: {
7382
+ kind: `shared_state`,
7383
+ sharedStateId,
7384
+ linkedEntityUrls
7385
+ },
7386
+ builtInAllowed: true,
7387
+ request
7388
+ });
7389
+ }
7390
+ return await applyAuthorizationHook(ctx, {
7391
+ verb: permission,
7392
+ resourceKey: `shared_state:${sharedStateId}`,
7393
+ resource: {
7394
+ kind: `shared_state`,
7395
+ sharedStateId,
7396
+ linkedEntityUrls
7397
+ },
7398
+ builtInAllowed: false,
7399
+ request
7400
+ });
7401
+ }
7402
+ async function applyAuthorizationHook(ctx, input) {
7403
+ const hook = ctx.authorizeRequest;
7404
+ if (!hook) return input.builtInAllowed;
7405
+ const cacheKey = [
7406
+ ctx.service,
7407
+ ctx.principal.url,
7408
+ input.verb,
7409
+ input.resourceKey
7410
+ ].join(`|`);
7411
+ const cached = getCachedDecision(hook, cacheKey);
7412
+ if (cached) return cached.decision === `allow`;
7413
+ let decision;
7414
+ try {
7415
+ decision = await hook({
7416
+ tenant: ctx.service,
7417
+ principal: ctx.principal,
7418
+ verb: input.verb,
7419
+ resource: input.resource,
7420
+ request: input.request ? requestMetadata(input.request) : void 0,
7421
+ builtInAllowed: input.builtInAllowed
7422
+ });
7423
+ } catch (error) {
7424
+ serverLog.warn(`[agent-server] authorization hook failed:`, error);
7425
+ return false;
7426
+ }
7427
+ cacheDecision(hook, cacheKey, decision);
7428
+ return decision.decision === `allow`;
7429
+ }
7430
+ function getCachedDecision(hook, cacheKey) {
7431
+ const cache = authzDecisionCache.get(hook);
7432
+ const entry = cache?.get(cacheKey);
7433
+ if (!entry) return null;
7434
+ if (entry.expiresAt <= Date.now()) {
7435
+ cache?.delete(cacheKey);
7436
+ return null;
7437
+ }
7438
+ return { decision: entry.decision };
7439
+ }
7440
+ function cacheDecision(hook, cacheKey, decision) {
7441
+ if (!decision.expires_at) return;
7442
+ const expiresAt = Date.parse(decision.expires_at);
7443
+ if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) return;
7444
+ let cache = authzDecisionCache.get(hook);
7445
+ if (!cache) {
7446
+ cache = new Map();
7447
+ authzDecisionCache.set(hook, cache);
7448
+ }
7449
+ cache.set(cacheKey, {
7450
+ decision: decision.decision,
7451
+ expiresAt
7452
+ });
7453
+ }
7454
+ function requestMetadata(request) {
7455
+ const headers = {};
7456
+ request.headers.forEach((value, key) => {
7457
+ headers[key] = value;
7458
+ });
7459
+ return {
7460
+ method: request.method,
7461
+ url: request.url,
7462
+ headers
7463
+ };
7464
+ }
6741
7465
 
6742
7466
  //#endregion
6743
7467
  //#region src/webhook-signing.ts
@@ -6829,6 +7553,7 @@ const subscriptionControlActions = [
6829
7553
  `ack`,
6830
7554
  `release`
6831
7555
  ];
7556
+ const SHARED_STATE_OWNER_ENTITY_HEADER = `electric-owner-entity`;
6832
7557
  const durableStreamsRouter = Router();
6833
7558
  durableStreamsRouter.put(`/__ds/subscriptions/:subscriptionId`, putSubscriptionBase);
6834
7559
  durableStreamsRouter.get(`/__ds/subscriptions/:subscriptionId`, getSubscriptionBase);
@@ -7046,6 +7771,8 @@ async function webhookJwks(_request, ctx) {
7046
7771
  });
7047
7772
  }
7048
7773
  async function streamAppend(request, ctx) {
7774
+ const auth = await authorizeDurableStreamAccess(request, ctx);
7775
+ if (auth) return auth;
7049
7776
  return await electricAgentsStreamAppendRouter.fetch(createStreamAppendRouteRequest(request), ctx.runtime, (req, body) => forwardFetchRequest({
7050
7777
  request: {
7051
7778
  method: req.method,
@@ -7062,8 +7789,9 @@ async function streamAppend(request, ctx) {
7062
7789
  }));
7063
7790
  }
7064
7791
  async function proxyPassThrough(request, ctx) {
7792
+ const auth = await authorizeDurableStreamAccess(request, ctx);
7793
+ if (auth) return auth;
7065
7794
  const streamPath = new URL(request.url).pathname;
7066
- if (ctx.entityManager?.isAttachmentStreamPath(streamPath)) return new Response(null, { status: 404 });
7067
7795
  const upstream = await forwardToDurableStreams(ctx, request);
7068
7796
  const method = request.method.toUpperCase();
7069
7797
  const endTrackedRead = method === `GET` ? await ctx.entityBridgeManager.beginClientRead(streamPath) : null;
@@ -7074,6 +7802,51 @@ async function proxyPassThrough(request, ctx) {
7074
7802
  await endTrackedRead?.();
7075
7803
  }
7076
7804
  }
7805
+ async function authorizeDurableStreamAccess(request, ctx) {
7806
+ const method = request.method.toUpperCase();
7807
+ const streamPath = new URL(request.url).pathname;
7808
+ if (method === `GET` || method === `HEAD`) {
7809
+ const registry = ctx.entityManager?.registry;
7810
+ const entity = registry?.getEntityByStream ? await registry.getEntityByStream(streamPath) : null;
7811
+ if (entity) {
7812
+ if (await canAccessEntity(ctx, entity, `read`, request)) return void 0;
7813
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to read ${entity.url}`);
7814
+ }
7815
+ const attachmentEntityUrl = entityUrlFromAttachmentStreamPath(streamPath);
7816
+ if (attachmentEntityUrl) {
7817
+ const attachmentEntity = registry?.getEntity ? await registry.getEntity(attachmentEntityUrl) : null;
7818
+ if (!attachmentEntity) return apiError(404, ErrCodeNotFound, `Entity not found`);
7819
+ if (await canAccessEntity(ctx, attachmentEntity, `read`, request)) return void 0;
7820
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to read ${attachmentEntity.url}`);
7821
+ }
7822
+ }
7823
+ const sharedStateId = sharedStateIdFromPath(streamPath);
7824
+ if (!sharedStateId) return void 0;
7825
+ if (method === `GET` || method === `HEAD`) {
7826
+ if (await canAccessSharedState(ctx, sharedStateId, `read`, request)) return void 0;
7827
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to read shared state`);
7828
+ }
7829
+ if (method === `PUT` || method === `POST`) {
7830
+ const ownerEntityUrl = request.headers.get(SHARED_STATE_OWNER_ENTITY_HEADER)?.trim() || void 0;
7831
+ if (await canAccessSharedState(ctx, sharedStateId, `write`, request, ownerEntityUrl)) return void 0;
7832
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to write shared state`);
7833
+ }
7834
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to access shared state`);
7835
+ }
7836
+ function entityUrlFromAttachmentStreamPath(path$1) {
7837
+ const match = path$1.match(/^\/([^/]+)\/([^/]+)\/attachments\/[^/]+$/);
7838
+ if (!match) return null;
7839
+ return `/${match[1]}/${match[2]}`;
7840
+ }
7841
+ function sharedStateIdFromPath(path$1) {
7842
+ const match = path$1.match(/^\/_electric\/shared-state\/([^/]+)$/);
7843
+ if (!match) return null;
7844
+ try {
7845
+ return decodeURIComponent(match[1]);
7846
+ } catch {
7847
+ return match[1];
7848
+ }
7849
+ }
7077
7850
 
7078
7851
  //#endregion
7079
7852
  //#region src/routing/electric-proxy-router.ts
@@ -7081,12 +7854,15 @@ const electricProxyRouter = Router({ base: `/_electric/electric` });
7081
7854
  electricProxyRouter.get(`/*`, proxyElectric);
7082
7855
  async function proxyElectric(request, ctx) {
7083
7856
  if (!ctx.electricUrl) return apiError(500, `ELECTRIC_PROXY_FAILED`, `Electric URL not configured`);
7857
+ await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
7084
7858
  const target = buildElectricProxyTarget({
7085
7859
  incomingUrl: new URL(request.url),
7086
7860
  electricUrl: ctx.electricUrl,
7087
7861
  electricSecret: ctx.electricSecret,
7088
7862
  tenantId: ctx.service,
7089
- principalUrl: ctx.principal.url
7863
+ principalUrl: ctx.principal.url,
7864
+ principalKind: ctx.principal.kind,
7865
+ permissionBypass: isPermissionBypassPrincipal(ctx)
7090
7866
  });
7091
7867
  const headers = new Headers(request.headers);
7092
7868
  headers.delete(`host`);
@@ -7145,6 +7921,27 @@ const wakeConditionSchema = Type.Union([Type.Literal(`runFinished`), Type.Object
7145
7921
  Type.Literal(`delete`)
7146
7922
  ])))
7147
7923
  })]);
7924
+ const permissionSubjectSchema = Type.Object({
7925
+ subject_kind: Type.Union([Type.Literal(`principal`), Type.Literal(`principal_kind`)]),
7926
+ subject_value: Type.String()
7927
+ }, { additionalProperties: false });
7928
+ const entityPermissionSchema = Type.Union([
7929
+ Type.Literal(`read`),
7930
+ Type.Literal(`write`),
7931
+ Type.Literal(`delete`),
7932
+ Type.Literal(`signal`),
7933
+ Type.Literal(`fork`),
7934
+ Type.Literal(`schedule`),
7935
+ Type.Literal(`spawn`),
7936
+ Type.Literal(`manage`)
7937
+ ]);
7938
+ const entityPermissionGrantInputSchema = Type.Object({
7939
+ ...permissionSubjectSchema.properties,
7940
+ permission: entityPermissionSchema,
7941
+ propagation: Type.Optional(Type.Union([Type.Literal(`self`), Type.Literal(`descendants`)])),
7942
+ copy_to_children: Type.Optional(Type.Boolean()),
7943
+ expires_at: Type.Optional(Type.String())
7944
+ }, { additionalProperties: false });
7148
7945
  const spawnBodySchema = Type.Object({
7149
7946
  args: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
7150
7947
  tags: Type.Optional(stringRecordSchema$1),
@@ -7152,6 +7949,8 @@ const spawnBodySchema = Type.Object({
7152
7949
  dispatch_policy: Type.Optional(dispatchPolicySchema),
7153
7950
  sandbox: Type.Optional(sandboxChoiceSchema),
7154
7951
  initialMessage: Type.Optional(Type.Unknown()),
7952
+ grants: Type.Optional(Type.Array(entityPermissionGrantInputSchema)),
7953
+ initialMessageType: Type.Optional(Type.String()),
7155
7954
  wake: Type.Optional(Type.Object({
7156
7955
  subscriberUrl: Type.String(),
7157
7956
  condition: wakeConditionSchema,
@@ -7173,8 +7972,22 @@ const sendBodySchema = Type.Object({
7173
7972
  ])),
7174
7973
  position: Type.Optional(Type.String()),
7175
7974
  afterMs: Type.Optional(Type.Number()),
7176
- from: Type.Optional(Type.String())
7975
+ from: Type.Optional(Type.String()),
7976
+ from_principal: Type.Optional(Type.String()),
7977
+ from_agent: Type.Optional(Type.String())
7177
7978
  });
7979
+ function agentUrlForPrincipal(principal) {
7980
+ if (principal.kind === `agent`) return `/${principal.id}`;
7981
+ if (principal.key.startsWith(`entity:`)) return `/${principal.key.slice(`entity:`.length)}`;
7982
+ return null;
7983
+ }
7984
+ function agentUrlPath(value) {
7985
+ try {
7986
+ return new URL(value).pathname;
7987
+ } catch {
7988
+ return value;
7989
+ }
7990
+ }
7178
7991
  const inboxMessageBodySchema = Type.Object({
7179
7992
  payload: Type.Optional(Type.Unknown()),
7180
7993
  position: Type.Optional(Type.String()),
@@ -7253,24 +8066,27 @@ const attachmentSubjectTypes = new Set([
7253
8066
  ]);
7254
8067
  const entitiesRouter = Router({ base: `/_electric/entities` });
7255
8068
  entitiesRouter.get(`/`, listEntities);
7256
- entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spawnBodySchema), spawnEntity);
7257
- entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, getEntity);
7258
- entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity);
7259
- entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, killEntity);
7260
- entitiesRouter.post(`/:type/:instanceId/signal`, withExistingEntity, withSchema(signalBodySchema), signalEntity);
7261
- entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), sendEntity);
7262
- entitiesRouter.post(`/:type/:instanceId/attachments`, withExistingEntity, createAttachment);
7263
- entitiesRouter.get(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, readAttachment);
7264
- entitiesRouter.delete(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, deleteAttachment);
7265
- entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), updateInboxMessage);
7266
- entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, deleteInboxMessage);
7267
- entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), forkEntity);
7268
- entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), setTag);
7269
- entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, deleteTag);
7270
- entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), upsertSchedule);
7271
- entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, deleteSchedule);
7272
- entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), upsertEventSourceSubscription);
7273
- entitiesRouter.delete(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, deleteEventSourceSubscription);
8069
+ entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spawnBodySchema), withSpawnPermission, spawnEntity);
8070
+ entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`read`), getEntity);
8071
+ entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`read`), headEntity);
8072
+ entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`delete`), killEntity);
8073
+ entitiesRouter.post(`/:type/:instanceId/signal`, withExistingEntity, withSchema(signalBodySchema), withEntityPermission(`signal`), signalEntity);
8074
+ entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), withEntityPermission(`write`), sendEntity);
8075
+ entitiesRouter.post(`/:type/:instanceId/attachments`, withExistingEntity, withEntityPermission(`write`), createAttachment);
8076
+ entitiesRouter.get(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, withEntityPermission(`read`), readAttachment);
8077
+ entitiesRouter.delete(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, withEntityPermission(`write`), deleteAttachment);
8078
+ entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), withEntityPermission(`write`), updateInboxMessage);
8079
+ entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withEntityPermission(`write`), deleteInboxMessage);
8080
+ entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), withEntityPermission(`fork`), forkEntity);
8081
+ entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), withEntityPermission(`write`), setTag);
8082
+ entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withEntityPermission(`write`), deleteTag);
8083
+ entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), withEntityPermission(`schedule`), upsertSchedule);
8084
+ entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withEntityPermission(`schedule`), deleteSchedule);
8085
+ entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), withEntityPermission(`write`), upsertEventSourceSubscription);
8086
+ entitiesRouter.delete(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withEntityPermission(`write`), deleteEventSourceSubscription);
8087
+ entitiesRouter.get(`/:type/:instanceId/grants`, withExistingEntity, withEntityPermission(`manage`), listEntityPermissionGrants);
8088
+ entitiesRouter.post(`/:type/:instanceId/grants`, withExistingEntity, withSchema(entityPermissionGrantInputSchema), withEntityPermission(`manage`), createEntityPermissionGrant);
8089
+ entitiesRouter.delete(`/:type/:instanceId/grants/:grantId`, withExistingEntity, withEntityPermission(`manage`), deleteEntityPermissionGrant);
7274
8090
  function entityUrlFromSegments(type, instanceId) {
7275
8091
  if (!type || !instanceId) return null;
7276
8092
  if (type.startsWith(`_`) || type.includes(`*`) || instanceId.includes(`*`)) return null;
@@ -7369,6 +8185,17 @@ function rejectPrincipalEntityMutation(request, action) {
7369
8185
  if (entity.type !== `principal`) return void 0;
7370
8186
  return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be ${action}`);
7371
8187
  }
8188
+ function parseExpiresAt$1(value) {
8189
+ if (value === void 0) return void 0;
8190
+ const expiresAt = new Date(value);
8191
+ if (Number.isNaN(expiresAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid expires_at timestamp`, 400);
8192
+ return expiresAt;
8193
+ }
8194
+ function parseGrantId$1(request) {
8195
+ const grantId = Number.parseInt(String(request.params.grantId), 10);
8196
+ if (!Number.isSafeInteger(grantId) || grantId <= 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid grant id`, 400);
8197
+ return grantId;
8198
+ }
7372
8199
  async function withExistingEntity(request, ctx) {
7373
8200
  const entityUrl = entityUrlFromSegments(request.params.type, request.params.instanceId);
7374
8201
  if (!entityUrl) return void 0;
@@ -7399,17 +8226,76 @@ async function withSpawnableEntityType(request, ctx) {
7399
8226
  if (request.params.type === `principal`) return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be spawned directly`);
7400
8227
  const entityType = await ctx.entityManager.registry.getEntityType(request.params.type);
7401
8228
  if (!entityType) return apiError(404, ErrCodeUnknownEntityType, `Entity type "${request.params.type}" not found`);
8229
+ request.spawnRoute = { entityType };
7402
8230
  return void 0;
7403
8231
  }
8232
+ function withEntityPermission(permission) {
8233
+ return async (request, ctx) => {
8234
+ const { entity } = requireExistingEntityRoute(request);
8235
+ if (await canAccessEntity(ctx, entity, permission, request)) return void 0;
8236
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to ${permission} ${entity.url}`);
8237
+ };
8238
+ }
8239
+ async function withSpawnPermission(request, ctx) {
8240
+ const parsed = routeBody(request);
8241
+ const entityType = request.spawnRoute?.entityType;
8242
+ if (!entityType) throw new Error(`spawnable entity type middleware did not run`);
8243
+ if (!await canAccessEntityType(ctx, entityType, `spawn`, request)) return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn ${entityType.name}`);
8244
+ if (!parsed.parent) return void 0;
8245
+ const parent = await ctx.entityManager.registry.getEntity(parsed.parent);
8246
+ if (!parent) return apiError(404, ErrCodeNotFound, `Parent entity not found`);
8247
+ if (await canAccessEntity(ctx, parent, `spawn`, request)) return await validateParentedSpawnGrants(request, ctx, parent, parsed);
8248
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn children from ${parent.url}`);
8249
+ }
8250
+ async function validateParentedSpawnGrants(request, ctx, parent, parsed) {
8251
+ const needsParentManage = (parsed.grants ?? []).some(requiresParentManageForInitialGrant);
8252
+ if (!needsParentManage) return void 0;
8253
+ if (await canAccessEntity(ctx, parent, `manage`, request)) return void 0;
8254
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to delegate broad grants from ${parent.url}`);
8255
+ }
8256
+ function requiresParentManageForInitialGrant(grant) {
8257
+ return grant.permission === `manage` || grant.subject_kind === `principal_kind` || grant.propagation === `descendants` || grant.copy_to_children === true;
8258
+ }
7404
8259
  async function listEntities({ query }, ctx) {
7405
8260
  const { entities: entities$1 } = await ctx.entityManager.registry.listEntities({
7406
8261
  type: firstQueryValue$1(query.type),
7407
8262
  status: firstQueryValue$1(query.status),
7408
8263
  parent: firstQueryValue$1(query.parent),
7409
- created_by: firstQueryValue$1(query.created_by)
8264
+ created_by: firstQueryValue$1(query.created_by),
8265
+ readableBy: {
8266
+ ...principalSubject(ctx.principal),
8267
+ bypass: isPermissionBypassPrincipal(ctx)
8268
+ }
7410
8269
  });
7411
8270
  return json(entities$1.map((entity) => toPublicEntity(entity)));
7412
8271
  }
8272
+ async function listEntityPermissionGrants(request, ctx) {
8273
+ const { entityUrl } = requireExistingEntityRoute(request);
8274
+ const grants = await ctx.entityManager.registry.listEntityPermissionGrants(entityUrl);
8275
+ return json({ grants });
8276
+ }
8277
+ async function createEntityPermissionGrant(request, ctx) {
8278
+ const { entityUrl } = requireExistingEntityRoute(request);
8279
+ const parsed = routeBody(request);
8280
+ const grant = await ctx.entityManager.registry.createEntityPermissionGrant({
8281
+ entityUrl,
8282
+ permission: parsed.permission,
8283
+ subjectKind: parsed.subject_kind,
8284
+ subjectValue: parsed.subject_value,
8285
+ propagation: parsed.propagation,
8286
+ copyToChildren: parsed.copy_to_children,
8287
+ expiresAt: parseExpiresAt$1(parsed.expires_at),
8288
+ createdBy: ctx.principal.url
8289
+ });
8290
+ await ctx.entityBridgeManager.onEntityChanged(entityUrl);
8291
+ return json(grant, { status: 201 });
8292
+ }
8293
+ async function deleteEntityPermissionGrant(request, ctx) {
8294
+ const { entityUrl } = requireExistingEntityRoute(request);
8295
+ const deleted = await ctx.entityManager.registry.deleteEntityPermissionGrant(entityUrl, parseGrantId$1(request));
8296
+ if (deleted) await ctx.entityBridgeManager.onEntityChanged(entityUrl);
8297
+ return deleted ? status(204) : apiError(404, ErrCodeNotFound, `Grant not found`);
8298
+ }
7413
8299
  async function upsertSchedule(request, ctx) {
7414
8300
  const principalMutationError = rejectPrincipalEntityMutation(request, `scheduled`);
7415
8301
  if (principalMutationError) return principalMutationError;
@@ -7515,6 +8401,7 @@ async function forkEntity(request, ctx) {
7515
8401
  const result = await ctx.entityManager.forkSubtree(entityUrl, {
7516
8402
  rootInstanceId: parsed.instance_id,
7517
8403
  waitTimeoutMs: parsed.waitTimeoutMs,
8404
+ createdBy: ctx.principal.url,
7518
8405
  ...parsed.fork_pointer && { forkPointer: {
7519
8406
  offset: parsed.fork_pointer.offset,
7520
8407
  subOffset: parsed.fork_pointer.sub_offset
@@ -7530,26 +8417,27 @@ async function sendEntity(request, ctx) {
7530
8417
  const parsed = routeBody(request);
7531
8418
  const principal = ctx.principal;
7532
8419
  if (parsed.from !== void 0 && parsed.from !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from must match Electric-Principal`);
8420
+ if (parsed.from_principal !== void 0 && parsed.from_principal !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from_principal must match Electric-Principal`);
8421
+ if (parsed.from_agent !== void 0) {
8422
+ const principalAgentUrl = agentUrlForPrincipal(principal);
8423
+ if (agentUrlPath(parsed.from_agent) !== principalAgentUrl) return apiError(400, ErrCodeInvalidRequest, `Request from_agent must match authenticated agent principal`);
8424
+ }
7533
8425
  await ctx.entityManager.ensurePrincipal(principal);
7534
8426
  const { entityUrl, entity } = requireExistingEntityRoute(request);
7535
8427
  const dispatchEntity = entity.dispatch_policy ? entity : await backfillEntityDispatchPolicy(ctx, entity);
7536
8428
  await linkEntityDispatchSubscription(ctx, dispatchEntity);
7537
- if (parsed.afterMs && parsed.afterMs > 0) await ctx.entityManager.enqueueDelayedSend(entityUrl, {
7538
- from: principal.url,
7539
- payload: parsed.payload,
7540
- key: parsed.key,
7541
- type: parsed.type,
7542
- mode: parsed.mode,
7543
- position: parsed.position
7544
- }, new Date(Date.now() + parsed.afterMs));
7545
- else await ctx.entityManager.send(entityUrl, {
8429
+ const sendReq = {
7546
8430
  from: principal.url,
8431
+ from_principal: principal.url,
8432
+ from_agent: parsed.from_agent,
7547
8433
  payload: parsed.payload,
7548
8434
  key: parsed.key,
7549
8435
  type: parsed.type,
7550
8436
  mode: parsed.mode,
7551
8437
  position: parsed.position
7552
- });
8438
+ };
8439
+ if (parsed.afterMs && parsed.afterMs > 0) await ctx.entityManager.enqueueDelayedSend(entityUrl, sendReq, new Date(Date.now() + parsed.afterMs));
8440
+ else await ctx.entityManager.send(entityUrl, sendReq);
7553
8441
  return status(204);
7554
8442
  }
7555
8443
  async function createAttachment(request, ctx) {
@@ -7618,14 +8506,27 @@ async function spawnEntity(request, ctx) {
7618
8506
  dispatch_policy: dispatchPolicy,
7619
8507
  sandbox: parsed.sandbox,
7620
8508
  initialMessage: void 0,
8509
+ initialMessageType: void 0,
7621
8510
  wake: parsed.wake,
7622
8511
  created_by: principal.url
7623
8512
  });
8513
+ if (parsed.parent) await ctx.entityManager.registry.copyEntityPermissionGrantsForSpawn(parsed.parent, entity.url, principal.url);
8514
+ for (const grant of parsed.grants ?? []) await ctx.entityManager.registry.createEntityPermissionGrant({
8515
+ entityUrl: entity.url,
8516
+ permission: grant.permission,
8517
+ subjectKind: grant.subject_kind,
8518
+ subjectValue: grant.subject_value,
8519
+ propagation: grant.propagation,
8520
+ copyToChildren: grant.copy_to_children,
8521
+ expiresAt: parseExpiresAt$1(grant.expires_at),
8522
+ createdBy: principal.url
8523
+ });
7624
8524
  const linkBeforeInitialMessage = parsed.initialMessage !== void 0 && shouldLinkDispatchBeforeInitialMessage(dispatchPolicy);
7625
8525
  if (linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
7626
8526
  if (parsed.initialMessage !== void 0) await ctx.entityManager.send(entity.url, {
7627
8527
  from: principal.url,
7628
- payload: parsed.initialMessage
8528
+ payload: parsed.initialMessage,
8529
+ type: parsed.initialMessageType
7629
8530
  });
7630
8531
  if (!linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
7631
8532
  return json({
@@ -7672,14 +8573,37 @@ async function signalEntity(request, ctx) {
7672
8573
  //#region src/routing/entity-types-router.ts
7673
8574
  const jsonObjectSchema = Type.Record(Type.String(), Type.Unknown());
7674
8575
  const schemaMapSchema = Type.Record(Type.String(), jsonObjectSchema);
8576
+ const slashCommandArgumentSchema = Type.Object({
8577
+ name: Type.String(),
8578
+ type: Type.Union([
8579
+ Type.Literal(`string`),
8580
+ Type.Literal(`number`),
8581
+ Type.Literal(`boolean`)
8582
+ ]),
8583
+ required: Type.Optional(Type.Boolean()),
8584
+ description: Type.Optional(Type.String())
8585
+ }, { additionalProperties: false });
8586
+ const slashCommandSchema = Type.Object({
8587
+ name: Type.String(),
8588
+ description: Type.Optional(Type.String()),
8589
+ arguments: Type.Optional(Type.Array(slashCommandArgumentSchema))
8590
+ }, { additionalProperties: false });
8591
+ const typePermissionGrantInputSchema = Type.Object({
8592
+ subject_kind: Type.Union([Type.Literal(`principal`), Type.Literal(`principal_kind`)]),
8593
+ subject_value: Type.String(),
8594
+ permission: Type.Union([Type.Literal(`spawn`), Type.Literal(`manage`)]),
8595
+ expires_at: Type.Optional(Type.String())
8596
+ }, { additionalProperties: false });
7675
8597
  const registerEntityTypeBodySchema = Type.Object({
7676
8598
  name: Type.Optional(Type.String()),
7677
8599
  description: Type.Optional(Type.String()),
7678
8600
  creation_schema: Type.Optional(jsonObjectSchema),
7679
8601
  inbox_schemas: Type.Optional(schemaMapSchema),
7680
8602
  state_schemas: Type.Optional(schemaMapSchema),
8603
+ slash_commands: Type.Optional(Type.Array(slashCommandSchema)),
7681
8604
  serve_endpoint: Type.Optional(Type.String()),
7682
- default_dispatch_policy: Type.Optional(dispatchPolicySchema)
8605
+ default_dispatch_policy: Type.Optional(dispatchPolicySchema),
8606
+ permission_grants: Type.Optional(Type.Array(typePermissionGrantInputSchema))
7683
8607
  }, { additionalProperties: false });
7684
8608
  const amendEntityTypeSchemasBodySchema = Type.Object({
7685
8609
  inbox_schemas: Type.Optional(schemaMapSchema),
@@ -7687,20 +8611,56 @@ const amendEntityTypeSchemasBodySchema = Type.Object({
7687
8611
  }, { additionalProperties: false });
7688
8612
  const entityTypesRouter = Router({ base: `/_electric/entity-types` });
7689
8613
  entityTypesRouter.get(`/`, listEntityTypes);
7690
- entityTypesRouter.post(`/`, withSchema(registerEntityTypeBodySchema), registerEntityType);
7691
- entityTypesRouter.patch(`/:name/schemas`, withSchema(amendEntityTypeSchemasBodySchema), amendSchemas);
7692
- entityTypesRouter.get(`/:name`, getEntityType);
7693
- entityTypesRouter.delete(`/:name`, deleteEntityType);
8614
+ entityTypesRouter.post(`/`, withSchema(registerEntityTypeBodySchema), withEntityTypeRegistrationPermission, registerEntityType);
8615
+ entityTypesRouter.patch(`/:name/schemas`, withExistingEntityType, withEntityTypeManagePermission, withSchema(amendEntityTypeSchemasBodySchema), amendSchemas);
8616
+ entityTypesRouter.get(`/:name`, withExistingEntityType, withEntityTypeSpawnPermission, getEntityType);
8617
+ entityTypesRouter.delete(`/:name`, withExistingEntityType, withEntityTypeManagePermission, deleteEntityType);
8618
+ entityTypesRouter.get(`/:name/grants`, withExistingEntityType, withEntityTypeManagePermission, listTypePermissionGrants);
8619
+ entityTypesRouter.post(`/:name/grants`, withExistingEntityType, withSchema(typePermissionGrantInputSchema), withEntityTypeManagePermission, createTypePermissionGrant);
8620
+ entityTypesRouter.delete(`/:name/grants/:grantId`, withExistingEntityType, withEntityTypeManagePermission, deleteTypePermissionGrant);
7694
8621
  async function registerEntityType(request, ctx) {
7695
8622
  const parsed = routeBody(request);
7696
8623
  const normalized = normalizeEntityTypeRequest(parsed);
7697
8624
  if (normalized.serve_endpoint && !normalized.description && !normalized.creation_schema) return await discoverServeEndpoint(ctx, normalized);
7698
8625
  const entityType = await ctx.entityManager.registerEntityType(normalized);
8626
+ await applyRegistrationPermissionGrants(ctx, entityType.name, normalized);
7699
8627
  return json(toPublicEntityType(entityType), { status: 201 });
7700
8628
  }
7701
8629
  async function listEntityTypes(_request, ctx) {
7702
8630
  const entityTypes$1 = await ctx.entityManager.registry.listEntityTypes();
7703
- return json(entityTypes$1.map((entityType) => toPublicEntityType(entityType)));
8631
+ const visible = [];
8632
+ for (const entityType of entityTypes$1) if (await canAccessEntityType(ctx, entityType, `spawn`)) visible.push(entityType);
8633
+ return json(visible.map((entityType) => toPublicEntityType(entityType)));
8634
+ }
8635
+ async function withExistingEntityType(request, ctx) {
8636
+ const entityType = await ctx.entityManager.registry.getEntityType(request.params.name);
8637
+ if (!entityType) return apiError(404, ErrCodeNotFound, `Entity type not found`);
8638
+ request.entityTypeRoute = { entityType };
8639
+ return void 0;
8640
+ }
8641
+ async function withEntityTypeManagePermission(request, ctx) {
8642
+ const entityType = request.entityTypeRoute?.entityType;
8643
+ if (!entityType) throw new Error(`entity type middleware did not run`);
8644
+ if (await canAccessEntityType(ctx, entityType, `manage`, request)) return void 0;
8645
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to manage ${entityType.name}`);
8646
+ }
8647
+ async function withEntityTypeSpawnPermission(request, ctx) {
8648
+ const entityType = request.entityTypeRoute?.entityType;
8649
+ if (!entityType) throw new Error(`entity type middleware did not run`);
8650
+ if (await canAccessEntityType(ctx, entityType, `spawn`, request)) return void 0;
8651
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn ${entityType.name}`);
8652
+ }
8653
+ async function withEntityTypeRegistrationPermission(request, ctx) {
8654
+ const parsed = normalizeEntityTypeRequest(routeBody(request));
8655
+ if (!parsed.name) return void 0;
8656
+ const existing = await ctx.entityManager.registry.getEntityType(parsed.name);
8657
+ if (existing) {
8658
+ request.entityTypeRoute = { entityType: existing };
8659
+ if (await canAccessEntityType(ctx, existing, `manage`, request)) return void 0;
8660
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to manage ${existing.name}`);
8661
+ }
8662
+ if (await canRegisterEntityType(ctx, parsed, request)) return void 0;
8663
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to register entity types`);
7704
8664
  }
7705
8665
  async function discoverServeEndpoint(ctx, parsed) {
7706
8666
  try {
@@ -7709,17 +8669,17 @@ async function discoverServeEndpoint(ctx, parsed) {
7709
8669
  const manifest = await response.json();
7710
8670
  if (manifest.name !== parsed.name) return apiError(400, ErrCodeServeEndpointNameMismatch, `Serve endpoint returned name "${manifest.name}" but expected "${parsed.name}"`);
7711
8671
  manifest.serve_endpoint = parsed.serve_endpoint;
8672
+ manifest.permission_grants = parsed.permission_grants;
7712
8673
  const entityType = await ctx.entityManager.registerEntityType(normalizeEntityTypeRequest(manifest));
8674
+ await applyRegistrationPermissionGrants(ctx, entityType.name, manifest);
7713
8675
  return json(toPublicEntityType(entityType), { status: 201 });
7714
8676
  } catch (err) {
7715
8677
  if (err instanceof ElectricAgentsError) throw err;
7716
8678
  return apiError(502, ErrCodeServeEndpointUnreachable, `Failed to reach serve endpoint: ${err instanceof Error ? err.message : String(err)}`);
7717
8679
  }
7718
8680
  }
7719
- async function getEntityType(request, ctx) {
7720
- const entityType = await ctx.entityManager.registry.getEntityType(request.params.name);
7721
- if (!entityType) return apiError(404, ErrCodeNotFound, `Entity type not found`);
7722
- return json(toPublicEntityType(entityType));
8681
+ async function getEntityType(request) {
8682
+ return json(toPublicEntityType(request.entityTypeRoute.entityType));
7723
8683
  }
7724
8684
  async function amendSchemas(request, ctx) {
7725
8685
  const parsed = routeBody(request);
@@ -7733,6 +8693,47 @@ async function deleteEntityType(request, ctx) {
7733
8693
  await ctx.entityManager.deleteEntityType(request.params.name);
7734
8694
  return status(204);
7735
8695
  }
8696
+ async function listTypePermissionGrants(request, ctx) {
8697
+ const grants = await ctx.entityManager.registry.listEntityTypePermissionGrants(request.entityTypeRoute.entityType.name);
8698
+ return json({ grants });
8699
+ }
8700
+ async function createTypePermissionGrant(request, ctx) {
8701
+ const parsed = routeBody(request);
8702
+ const grant = await ctx.entityManager.registry.createEntityTypePermissionGrant({
8703
+ entityType: request.entityTypeRoute.entityType.name,
8704
+ permission: parsed.permission,
8705
+ subjectKind: parsed.subject_kind,
8706
+ subjectValue: parsed.subject_value,
8707
+ expiresAt: parseExpiresAt(parsed.expires_at),
8708
+ createdBy: ctx.principal.url
8709
+ });
8710
+ return json(grant, { status: 201 });
8711
+ }
8712
+ async function deleteTypePermissionGrant(request, ctx) {
8713
+ const deleted = await ctx.entityManager.registry.deleteEntityTypePermissionGrant(request.entityTypeRoute.entityType.name, parseGrantId(request));
8714
+ return deleted ? status(204) : apiError(404, ErrCodeNotFound, `Grant not found`);
8715
+ }
8716
+ async function applyRegistrationPermissionGrants(ctx, entityType, request) {
8717
+ for (const grant of request.permission_grants ?? []) await ctx.entityManager.registry.ensureEntityTypePermissionGrant({
8718
+ entityType,
8719
+ permission: grant.permission,
8720
+ subjectKind: grant.subject_kind,
8721
+ subjectValue: grant.subject_value,
8722
+ expiresAt: parseExpiresAt(grant.expires_at),
8723
+ createdBy: ctx.principal.url
8724
+ });
8725
+ }
8726
+ function parseGrantId(request) {
8727
+ const grantId = Number.parseInt(String(request.params.grantId), 10);
8728
+ if (!Number.isSafeInteger(grantId) || grantId <= 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid grant id`, 400);
8729
+ return grantId;
8730
+ }
8731
+ function parseExpiresAt(value) {
8732
+ if (value === void 0) return void 0;
8733
+ const expiresAt = new Date(value);
8734
+ if (Number.isNaN(expiresAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid expires_at timestamp`, 400);
8735
+ return expiresAt;
8736
+ }
7736
8737
  function normalizeEntityTypeRequest(parsed) {
7737
8738
  const serveEndpoint = rewriteLoopbackWebhookUrl(parsed.serve_endpoint);
7738
8739
  return {
@@ -7741,11 +8742,13 @@ function normalizeEntityTypeRequest(parsed) {
7741
8742
  creation_schema: parsed.creation_schema,
7742
8743
  inbox_schemas: parsed.inbox_schemas,
7743
8744
  state_schemas: parsed.state_schemas,
8745
+ slash_commands: parsed.slash_commands,
7744
8746
  serve_endpoint: serveEndpoint,
7745
8747
  default_dispatch_policy: parsed.default_dispatch_policy ?? (serveEndpoint ? { targets: [{
7746
8748
  type: `webhook`,
7747
8749
  url: serveEndpoint
7748
- }] } : void 0)
8750
+ }] } : void 0),
8751
+ permission_grants: parsed.permission_grants
7749
8752
  };
7750
8753
  }
7751
8754
  function toPublicEntityType(entityType) {
@@ -7804,6 +8807,7 @@ function applyCors(response) {
7804
8807
  `content-type`,
7805
8808
  `authorization`,
7806
8809
  `electric-claim-token`,
8810
+ `electric-owner-entity`,
7807
8811
  ELECTRIC_PRINCIPAL_HEADER,
7808
8812
  `ngrok-skip-browser-warning`
7809
8813
  ].join(`, `));
@@ -7854,7 +8858,7 @@ observationsRouter.post(`/entities/ensure-stream`, withSchema(ensureEntitiesMemb
7854
8858
  observationsRouter.post(`/cron/ensure-stream`, withSchema(ensureCronStreamBodySchema), ensureCronStream);
7855
8859
  async function ensureEntitiesMembershipStream(request, ctx) {
7856
8860
  const parsed = routeBody(request);
7857
- const result = await ctx.entityManager.ensureEntitiesMembershipStream(parsed.tags ?? {});
8861
+ const result = await ctx.entityManager.ensureEntitiesMembershipStream(parsed.tags ?? {}, ctx.principal);
7858
8862
  return json(result);
7859
8863
  }
7860
8864
  async function ensureCronStream(request, ctx) {