@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.
@@ -4,13 +4,13 @@ import { DurableStreamTestServer } from "@durable-streams/server";
4
4
  import { createServer } from "node:http";
5
5
  import { createServerAdapter } from "@whatwg-node/server";
6
6
  import { Agent } from "undici";
7
- import { appendPathToUrl, assertTags, buildEventSourceManifestEntry, buildTagsIndex, createEntityRegistry, createRuntimeHandler, entityStateSchema, eventSourceSubscriptionManifestKey, getCronStreamPath, getCronStreamPathFromSpec, getEntitiesStreamPath, getNextCronFireAt, getSharedStateStreamPath, getWebhookStreamPath, manifestChildKey, manifestSharedStateKey, manifestSourceKey, normalizeTags, parseCronStreamPath, resolveCronScheduleSpec, resolveEventSourceSubscription, sourceRefForTags, verifyWebhookSignature } from "@electric-ax/agents-runtime";
7
+ import { COMPOSER_INPUT_MESSAGE_TYPE, appendPathToUrl, assertTags, buildEventSourceManifestEntry, buildTagsIndex, createEntityRegistry, createRuntimeHandler, 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";
8
8
  import fs, { existsSync } from "node:fs";
9
9
  import path, { dirname, resolve } from "node:path";
10
10
  import { drizzle } from "drizzle-orm/postgres-js";
11
11
  import { migrate } from "drizzle-orm/postgres-js/migrator";
12
12
  import postgres from "postgres";
13
- import { and, desc, eq, lt, ne, sql } from "drizzle-orm";
13
+ import { and, desc, eq, inArray, lt, ne, sql } from "drizzle-orm";
14
14
  import { bigint, bigserial, boolean, check, index, integer, jsonb, pgTable, primaryKey, serial, text, timestamp, unique } from "drizzle-orm/pg-core";
15
15
  import { AutoRouter, Router, json, status, withParams } from "itty-router";
16
16
  import { access, readFile } from "node:fs/promises";
@@ -41,11 +41,16 @@ __export(schema_exports, {
41
41
  entities: () => entities,
42
42
  entityBridges: () => entityBridges,
43
43
  entityDispatchState: () => entityDispatchState,
44
+ entityEffectivePermissions: () => entityEffectivePermissions,
45
+ entityLineage: () => entityLineage,
44
46
  entityManifestSources: () => entityManifestSources,
47
+ entityPermissionGrants: () => entityPermissionGrants,
48
+ entityTypePermissionGrants: () => entityTypePermissionGrants,
45
49
  entityTypes: () => entityTypes,
46
50
  runnerRuntimeDiagnostics: () => runnerRuntimeDiagnostics,
47
51
  runners: () => runners,
48
52
  scheduledTasks: () => scheduledTasks,
53
+ sharedStateLinks: () => sharedStateLinks,
49
54
  subscriptionWebhooks: () => subscriptionWebhooks,
50
55
  tagStreamOutbox: () => tagStreamOutbox,
51
56
  users: () => users,
@@ -59,6 +64,7 @@ const entityTypes = pgTable(`entity_types`, {
59
64
  creationSchema: jsonb(`creation_schema`),
60
65
  inboxSchemas: jsonb(`inbox_schemas`),
61
66
  stateSchemas: jsonb(`state_schemas`),
67
+ slashCommands: jsonb(`slash_commands`),
62
68
  serveEndpoint: text(`serve_endpoint`),
63
69
  defaultDispatchPolicy: jsonb(`default_dispatch_policy`),
64
70
  revision: integer(`revision`).notNull().default(1),
@@ -93,6 +99,94 @@ const entities = pgTable(`entities`, {
93
99
  index(`entities_tags_index_gin`).using(`gin`, table.tagsIndex),
94
100
  check(`chk_entities_status`, sql`${table.status} IN ('spawning', 'running', 'idle', 'paused', 'stopping', 'stopped', 'killed')`)
95
101
  ]);
102
+ const entityTypePermissionGrants = pgTable(`entity_type_permission_grants`, {
103
+ id: bigserial(`id`, { mode: `number` }).primaryKey(),
104
+ tenantId: text(`tenant_id`).notNull().default(`default`),
105
+ entityType: text(`entity_type`).notNull(),
106
+ permission: text(`permission`).notNull(),
107
+ subjectKind: text(`subject_kind`).notNull(),
108
+ subjectValue: text(`subject_value`).notNull(),
109
+ createdBy: text(`created_by`),
110
+ expiresAt: timestamp(`expires_at`, { withTimezone: true }),
111
+ createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow(),
112
+ updatedAt: timestamp(`updated_at`, { withTimezone: true }).notNull().defaultNow()
113
+ }, (table) => [
114
+ index(`idx_type_permission_grants_lookup`).on(table.tenantId, table.entityType, table.permission, table.subjectKind, table.subjectValue),
115
+ index(`idx_type_permission_grants_expiry`).on(table.tenantId, table.expiresAt),
116
+ check(`chk_type_permission_grants_permission`, sql`${table.permission} IN ('spawn', 'manage')`),
117
+ check(`chk_type_permission_grants_subject_kind`, sql`${table.subjectKind} IN ('principal', 'principal_kind')`)
118
+ ]);
119
+ const entityLineage = pgTable(`entity_lineage`, {
120
+ tenantId: text(`tenant_id`).notNull().default(`default`),
121
+ ancestorUrl: text(`ancestor_url`).notNull(),
122
+ descendantUrl: text(`descendant_url`).notNull(),
123
+ depth: integer(`depth`).notNull(),
124
+ createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow()
125
+ }, (table) => [
126
+ primaryKey({ columns: [
127
+ table.tenantId,
128
+ table.ancestorUrl,
129
+ table.descendantUrl
130
+ ] }),
131
+ index(`idx_entity_lineage_descendant`).on(table.tenantId, table.descendantUrl),
132
+ check(`chk_entity_lineage_depth`, sql`${table.depth} >= 0`)
133
+ ]);
134
+ const entityPermissionGrants = pgTable(`entity_permission_grants`, {
135
+ id: bigserial(`id`, { mode: `number` }).primaryKey(),
136
+ tenantId: text(`tenant_id`).notNull().default(`default`),
137
+ entityUrl: text(`entity_url`).notNull(),
138
+ permission: text(`permission`).notNull(),
139
+ subjectKind: text(`subject_kind`).notNull(),
140
+ subjectValue: text(`subject_value`).notNull(),
141
+ propagation: text(`propagation`).notNull().default(`self`),
142
+ copyToChildren: boolean(`copy_to_children`).notNull().default(false),
143
+ createdBy: text(`created_by`),
144
+ expiresAt: timestamp(`expires_at`, { withTimezone: true }),
145
+ createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow(),
146
+ updatedAt: timestamp(`updated_at`, { withTimezone: true }).notNull().defaultNow()
147
+ }, (table) => [
148
+ index(`idx_entity_permission_grants_entity`).on(table.tenantId, table.entityUrl),
149
+ index(`idx_entity_permission_grants_subject`).on(table.tenantId, table.permission, table.subjectKind, table.subjectValue),
150
+ index(`idx_entity_permission_grants_expiry`).on(table.tenantId, table.expiresAt),
151
+ check(`chk_entity_permission_grants_permission`, sql`${table.permission} IN ('read', 'write', 'delete', 'signal', 'fork', 'schedule', 'spawn', 'manage')`),
152
+ check(`chk_entity_permission_grants_subject_kind`, sql`${table.subjectKind} IN ('principal', 'principal_kind')`),
153
+ check(`chk_entity_permission_grants_propagation`, sql`${table.propagation} IN ('self', 'descendants')`)
154
+ ]);
155
+ const entityEffectivePermissions = pgTable(`entity_effective_permissions`, {
156
+ id: bigserial(`id`, { mode: `number` }).primaryKey(),
157
+ tenantId: text(`tenant_id`).notNull().default(`default`),
158
+ entityUrl: text(`entity_url`).notNull(),
159
+ sourceEntityUrl: text(`source_entity_url`).notNull(),
160
+ sourceGrantId: bigint(`source_grant_id`, { mode: `number` }).notNull(),
161
+ permission: text(`permission`).notNull(),
162
+ subjectKind: text(`subject_kind`).notNull(),
163
+ subjectValue: text(`subject_value`).notNull(),
164
+ expiresAt: timestamp(`expires_at`, { withTimezone: true }),
165
+ createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow()
166
+ }, (table) => [
167
+ unique(`uq_entity_effective_permission`).on(table.tenantId, table.entityUrl, table.sourceGrantId),
168
+ index(`idx_entity_effective_permissions_lookup`).on(table.tenantId, table.permission, table.subjectKind, table.subjectValue, table.entityUrl),
169
+ index(`idx_entity_effective_permissions_entity`).on(table.tenantId, table.entityUrl),
170
+ index(`idx_entity_effective_permissions_expiry`).on(table.tenantId, table.expiresAt),
171
+ check(`chk_entity_effective_permissions_permission`, sql`${table.permission} IN ('read', 'write', 'delete', 'signal', 'fork', 'schedule', 'spawn', 'manage')`),
172
+ check(`chk_entity_effective_permissions_subject_kind`, sql`${table.subjectKind} IN ('principal', 'principal_kind')`)
173
+ ]);
174
+ const sharedStateLinks = pgTable(`shared_state_links`, {
175
+ tenantId: text(`tenant_id`).notNull().default(`default`),
176
+ sharedStateId: text(`shared_state_id`).notNull(),
177
+ ownerEntityUrl: text(`owner_entity_url`).notNull(),
178
+ manifestKey: text(`manifest_key`).notNull(),
179
+ createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow(),
180
+ updatedAt: timestamp(`updated_at`, { withTimezone: true }).notNull().defaultNow()
181
+ }, (table) => [
182
+ primaryKey({ columns: [
183
+ table.tenantId,
184
+ table.ownerEntityUrl,
185
+ table.manifestKey
186
+ ] }),
187
+ index(`idx_shared_state_links_shared_state`).on(table.tenantId, table.sharedStateId),
188
+ index(`idx_shared_state_links_owner`).on(table.tenantId, table.ownerEntityUrl)
189
+ ]);
96
190
  const users = pgTable(`users`, {
97
191
  tenantId: text(`tenant_id`).notNull().default(`default`),
98
192
  id: text(`id`).notNull(),
@@ -279,12 +373,18 @@ const entityBridges = pgTable(`entity_bridges`, {
279
373
  sourceRef: text(`source_ref`).notNull(),
280
374
  tags: jsonb(`tags`).notNull(),
281
375
  streamUrl: text(`stream_url`).notNull(),
376
+ principalUrl: text(`principal_url`),
377
+ principalKind: text(`principal_kind`),
282
378
  shapeHandle: text(`shape_handle`),
283
379
  shapeOffset: text(`shape_offset`),
284
380
  lastObserverActivityAt: timestamp(`last_observer_activity_at`, { withTimezone: true }).notNull().defaultNow(),
285
381
  createdAt: timestamp(`created_at`, { withTimezone: true }).notNull().defaultNow(),
286
382
  updatedAt: timestamp(`updated_at`, { withTimezone: true }).notNull().defaultNow()
287
- }, (table) => [primaryKey({ columns: [table.tenantId, table.sourceRef] }), unique(`uq_entity_bridges_stream_url`).on(table.tenantId, table.streamUrl)]);
383
+ }, (table) => [
384
+ primaryKey({ columns: [table.tenantId, table.sourceRef] }),
385
+ unique(`uq_entity_bridges_stream_url`).on(table.tenantId, table.streamUrl),
386
+ index(`idx_entity_bridges_principal`).on(table.tenantId, table.principalKind, table.principalUrl)
387
+ ]);
288
388
  const entityManifestSources = pgTable(`entity_manifest_sources`, {
289
389
  tenantId: text(`tenant_id`).notNull().default(`default`),
290
390
  ownerEntityUrl: text(`owner_entity_url`).notNull(),
@@ -1045,29 +1145,136 @@ function buildElectricProxyTarget(options) {
1045
1145
  if (options.electricSecret) target.searchParams.set(`secret`, options.electricSecret);
1046
1146
  const table = options.incomingUrl.searchParams.get(`table`);
1047
1147
  if (table === `entities`) {
1048
- 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"`);
1049
- applyTenantShapeWhere(target, options.tenantId);
1148
+ 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"`);
1149
+ applyShapeWhere(target, buildReadableEntitiesWhere({
1150
+ tenantId: options.tenantId,
1151
+ principalUrl: options.principalUrl ?? ``,
1152
+ principalKind: options.principalKind ?? ``,
1153
+ permissionBypass: options.permissionBypass
1154
+ }));
1050
1155
  } else if (table === `entity_types`) {
1051
- target.searchParams.set(`columns`, `"tenant_id","name","description","creation_schema","inbox_schemas","state_schemas","serve_endpoint","default_dispatch_policy","revision","created_at","updated_at"`);
1052
- applyTenantShapeWhere(target, options.tenantId);
1156
+ 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"`);
1157
+ applyShapeWhere(target, buildSpawnableEntityTypesWhere({
1158
+ tenantId: options.tenantId,
1159
+ principalUrl: options.principalUrl ?? ``,
1160
+ principalKind: options.principalKind ?? ``,
1161
+ permissionBypass: options.permissionBypass
1162
+ }));
1053
1163
  } else if (table === `runners`) {
1054
1164
  target.searchParams.set(`columns`, `"tenant_id","id","owner_principal","label","kind","admin_status","wake_stream","sandbox_profiles","created_at","updated_at"`);
1055
1165
  applyTenantShapeWhere(target, options.tenantId, [`owner_principal = ${sqlStringLiteral$2(options.principalUrl ?? ``)}`]);
1166
+ } else if (table === `users`) {
1167
+ target.searchParams.set(`columns`, `"tenant_id","id","display_name","email","avatar_url","created_at","updated_at"`);
1168
+ applyTenantShapeWhere(target, options.tenantId);
1169
+ } else if (table === `entity_effective_permissions`) {
1170
+ target.searchParams.set(`columns`, `"tenant_id","id","entity_url","source_entity_url","source_grant_id","permission","subject_kind","subject_value","expires_at","created_at"`);
1171
+ applyShapeWhere(target, buildCurrentPrincipalEntityEffectivePermissionsWhere({
1172
+ tenantId: options.tenantId,
1173
+ principalUrl: options.principalUrl ?? ``,
1174
+ principalKind: options.principalKind ?? ``,
1175
+ permissionBypass: options.permissionBypass
1176
+ }));
1056
1177
  } else if (table === `runner_runtime_diagnostics`) {
1057
1178
  target.searchParams.set(`columns`, `"tenant_id","runner_id","owner_principal","wake_stream_offset","last_seen_at","liveness_lease_expires_at","diagnostics","updated_at"`);
1058
1179
  applyTenantShapeWhere(target, options.tenantId, [`owner_principal = ${sqlStringLiteral$2(options.principalUrl ?? ``)}`]);
1059
1180
  } else if (table === `entity_dispatch_state`) {
1060
1181
  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"`);
1061
- applyTenantShapeWhere(target, options.tenantId);
1182
+ applyShapeWhere(target, buildReadableEntityUrlWhere({
1183
+ tenantId: options.tenantId,
1184
+ principalUrl: options.principalUrl ?? ``,
1185
+ principalKind: options.principalKind ?? ``,
1186
+ permissionBypass: options.permissionBypass
1187
+ }));
1062
1188
  } else if (table === `wake_notifications`) {
1063
1189
  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"`);
1064
- applyTenantShapeWhere(target, options.tenantId);
1190
+ applyShapeWhere(target, buildReadableEntityUrlWhere({
1191
+ tenantId: options.tenantId,
1192
+ principalUrl: options.principalUrl ?? ``,
1193
+ principalKind: options.principalKind ?? ``,
1194
+ permissionBypass: options.permissionBypass
1195
+ }));
1065
1196
  } else if (table === `consumer_claims`) {
1066
1197
  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"`);
1067
- applyTenantShapeWhere(target, options.tenantId);
1198
+ applyShapeWhere(target, buildReadableEntityUrlWhere({
1199
+ tenantId: options.tenantId,
1200
+ principalUrl: options.principalUrl ?? ``,
1201
+ principalKind: options.principalKind ?? ``,
1202
+ permissionBypass: options.permissionBypass
1203
+ }));
1068
1204
  }
1069
1205
  return target;
1070
1206
  }
1207
+ function buildReadableEntitiesWhere(options) {
1208
+ const tenant = sqlStringLiteral$2(options.tenantId);
1209
+ if (options.permissionBypass) return `tenant_id = ${tenant}`;
1210
+ const principalUrl$1 = sqlStringLiteral$2(options.principalUrl);
1211
+ const principalKind = sqlStringLiteral$2(options.principalKind);
1212
+ return [
1213
+ `tenant_id = ${tenant}`,
1214
+ `AND (`,
1215
+ ` created_by = ${principalUrl$1}`,
1216
+ ` OR url IN (`,
1217
+ ` SELECT entity_url`,
1218
+ ` FROM entity_effective_permissions`,
1219
+ ` WHERE tenant_id = ${tenant}`,
1220
+ ` AND permission IN ('read', 'manage')`,
1221
+ ` AND (`,
1222
+ ` (subject_kind = 'principal' AND subject_value = ${principalUrl$1})`,
1223
+ ` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
1224
+ ` )`,
1225
+ ` )`,
1226
+ `)`
1227
+ ].join(`\n`);
1228
+ }
1229
+ function buildReadableEntityUrlWhere(options) {
1230
+ const tenant = sqlStringLiteral$2(options.tenantId);
1231
+ if (options.permissionBypass) return `tenant_id = ${tenant}`;
1232
+ return [
1233
+ `tenant_id = ${tenant}`,
1234
+ `AND entity_url IN (`,
1235
+ ` SELECT url`,
1236
+ ` FROM entities`,
1237
+ ` WHERE ${indentWhere(buildReadableEntitiesWhere(options), ` `).trimStart()}`,
1238
+ `)`
1239
+ ].join(`\n`);
1240
+ }
1241
+ function buildCurrentPrincipalEntityEffectivePermissionsWhere(options) {
1242
+ const tenant = sqlStringLiteral$2(options.tenantId);
1243
+ if (options.permissionBypass) return `tenant_id = ${tenant}`;
1244
+ const principalUrl$1 = sqlStringLiteral$2(options.principalUrl);
1245
+ const principalKind = sqlStringLiteral$2(options.principalKind);
1246
+ return [
1247
+ `tenant_id = ${tenant}`,
1248
+ `AND (`,
1249
+ ` (subject_kind = 'principal' AND subject_value = ${principalUrl$1})`,
1250
+ ` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
1251
+ `)`,
1252
+ `AND entity_url IN (`,
1253
+ ` SELECT url`,
1254
+ ` FROM entities`,
1255
+ ` WHERE ${buildReadableEntitiesWhere(options)}`,
1256
+ `)`
1257
+ ].join(`\n`);
1258
+ }
1259
+ function buildSpawnableEntityTypesWhere(options) {
1260
+ const tenant = sqlStringLiteral$2(options.tenantId);
1261
+ if (options.permissionBypass) return `tenant_id = ${tenant}`;
1262
+ const principalUrl$1 = sqlStringLiteral$2(options.principalUrl);
1263
+ const principalKind = sqlStringLiteral$2(options.principalKind);
1264
+ return [
1265
+ `tenant_id = ${tenant}`,
1266
+ `AND name IN (`,
1267
+ ` SELECT entity_type`,
1268
+ ` FROM entity_type_permission_grants`,
1269
+ ` WHERE tenant_id = ${tenant}`,
1270
+ ` AND permission IN ('spawn', 'manage')`,
1271
+ ` AND (`,
1272
+ ` (subject_kind = 'principal' AND subject_value = ${principalUrl$1})`,
1273
+ ` OR (subject_kind = 'principal_kind' AND subject_value = ${principalKind})`,
1274
+ ` )`,
1275
+ `)`
1276
+ ].join(`\n`);
1277
+ }
1071
1278
  async function forwardFetchRequest(options) {
1072
1279
  const routingAdapter = resolveDurableStreamsRoutingAdapter(options.durableStreamsRouting, options.durableStreamsUrl);
1073
1280
  const routingInput = {
@@ -1102,13 +1309,18 @@ function decodeJsonObject(body) {
1102
1309
  return null;
1103
1310
  }
1104
1311
  function applyTenantShapeWhere(target, tenantId, extraConditions = []) {
1105
- const tenantWhere = [`tenant_id = ${sqlStringLiteral$2(tenantId)}`, ...extraConditions].join(` AND `);
1312
+ applyShapeWhere(target, [`tenant_id = ${sqlStringLiteral$2(tenantId)}`, ...extraConditions].join(` AND `));
1313
+ }
1314
+ function applyShapeWhere(target, enforcedWhere) {
1106
1315
  const existingWhere = target.searchParams.get(`where`);
1107
- target.searchParams.set(`where`, existingWhere ? `${tenantWhere} AND (${existingWhere})` : tenantWhere);
1316
+ target.searchParams.set(`where`, existingWhere ? `${enforcedWhere} AND (${existingWhere})` : enforcedWhere);
1108
1317
  }
1109
1318
  function sqlStringLiteral$2(value) {
1110
1319
  return `'${value.replace(/'/g, `''`)}'`;
1111
1320
  }
1321
+ function indentWhere(where, prefix) {
1322
+ return where.split(`\n`).map((line) => `${prefix}${line}`).join(`\n`);
1323
+ }
1112
1324
 
1113
1325
  //#endregion
1114
1326
  //#region src/routing/agent-ui-router.ts
@@ -1402,6 +1614,262 @@ function isLoopbackHostname(hostname) {
1402
1614
  return hostname === `localhost` || hostname === `127.0.0.1` || hostname === `::1`;
1403
1615
  }
1404
1616
 
1617
+ //#endregion
1618
+ //#region src/principal.ts
1619
+ const ELECTRIC_PRINCIPAL_HEADER = `electric-principal`;
1620
+ const PRINCIPAL_KINDS = new Set([
1621
+ `user`,
1622
+ `agent`,
1623
+ `service`,
1624
+ `system`
1625
+ ]);
1626
+ function parsePrincipalKey(input) {
1627
+ const colon = input.indexOf(`:`);
1628
+ if (colon <= 0) throw new Error(`Invalid principal identifier`);
1629
+ const kind = input.slice(0, colon);
1630
+ const id = input.slice(colon + 1);
1631
+ if (!PRINCIPAL_KINDS.has(kind)) throw new Error(`Invalid principal kind`);
1632
+ if (!id || id.includes(`/`)) throw new Error(`Invalid principal id`);
1633
+ const key = `${kind}:${id}`;
1634
+ return {
1635
+ kind,
1636
+ id,
1637
+ key,
1638
+ url: `/principal/${encodeURIComponent(key)}`
1639
+ };
1640
+ }
1641
+ function principalUrl(key) {
1642
+ return parsePrincipalKey(key).url;
1643
+ }
1644
+ function parsePrincipalUrl(url) {
1645
+ if (!url.startsWith(`/principal/`)) return null;
1646
+ const segment = url.slice(`/principal/`.length);
1647
+ if (!segment || segment.includes(`/`)) return null;
1648
+ try {
1649
+ return parsePrincipalKey(decodeURIComponent(segment));
1650
+ } catch {
1651
+ return null;
1652
+ }
1653
+ }
1654
+ function parsePrincipalInput(input) {
1655
+ const urlPrincipal = parsePrincipalUrl(input);
1656
+ if (urlPrincipal) return urlPrincipal;
1657
+ try {
1658
+ return parsePrincipalKey(input);
1659
+ } catch {
1660
+ return null;
1661
+ }
1662
+ }
1663
+ function getPrincipalFromRequest(request) {
1664
+ const value = request.headers.get(ELECTRIC_PRINCIPAL_HEADER);
1665
+ if (!value) return null;
1666
+ return parsePrincipalInput(value);
1667
+ }
1668
+ function getDevPrincipal() {
1669
+ return parsePrincipalKey(`system:dev-local`);
1670
+ }
1671
+ const BUILT_IN_SYSTEM_PRINCIPAL_IDS = new Set([
1672
+ `framework`,
1673
+ `auth-sync`,
1674
+ `dev-local`
1675
+ ]);
1676
+ function isBuiltInSystemPrincipalUrl(url) {
1677
+ if (!url?.startsWith(`/principal/`)) return false;
1678
+ try {
1679
+ const principal = parsePrincipalUrl(url);
1680
+ if (!principal) return false;
1681
+ return principal.kind === `system` && BUILT_IN_SYSTEM_PRINCIPAL_IDS.has(principal.id);
1682
+ } catch {
1683
+ return false;
1684
+ }
1685
+ }
1686
+ function principalFromCreatedBy(createdBy) {
1687
+ if (!createdBy) return void 0;
1688
+ const principal = parsePrincipalUrl(createdBy);
1689
+ if (!principal) return {
1690
+ url: createdBy,
1691
+ key: null
1692
+ };
1693
+ return {
1694
+ url: principal.url,
1695
+ key: principal.key,
1696
+ kind: principal.kind,
1697
+ id: principal.id
1698
+ };
1699
+ }
1700
+ const principalIdentityStateSchema = Type.Object({
1701
+ kind: Type.Union([
1702
+ Type.Literal(`user`),
1703
+ Type.Literal(`agent`),
1704
+ Type.Literal(`service`),
1705
+ Type.Literal(`system`)
1706
+ ]),
1707
+ id: Type.String(),
1708
+ key: Type.String(),
1709
+ url: Type.String(),
1710
+ updated_at: Type.String(),
1711
+ display_name: Type.Optional(Type.String()),
1712
+ email: Type.Optional(Type.String()),
1713
+ avatar_url: Type.Optional(Type.String()),
1714
+ auth_provider: Type.Optional(Type.String()),
1715
+ auth_subject: Type.Optional(Type.String()),
1716
+ claims: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
1717
+ created_at: Type.Optional(Type.String())
1718
+ }, { additionalProperties: false });
1719
+ const principalUpdateIdentityMessageSchema = Type.Object({ identity: principalIdentityStateSchema }, { additionalProperties: false });
1720
+
1721
+ //#endregion
1722
+ //#region src/permissions.ts
1723
+ const authzDecisionCache = new WeakMap();
1724
+ function principalSubject(principal) {
1725
+ return {
1726
+ principalUrl: principal.url,
1727
+ principalKind: principal.kind
1728
+ };
1729
+ }
1730
+ function isPermissionBypassPrincipal(ctx) {
1731
+ return isBuiltInSystemPrincipalUrl(ctx.principal.url);
1732
+ }
1733
+ async function canAccessEntity(ctx, entity, permission, request) {
1734
+ if (isPermissionBypassPrincipal(ctx)) return true;
1735
+ await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
1736
+ const builtInAllowed = entity.created_by === ctx.principal.url || await ctx.entityManager.registry.hasEntityPermission(entity.url, permission, principalSubject(ctx.principal));
1737
+ return await applyAuthorizationHook(ctx, {
1738
+ verb: permission,
1739
+ resourceKey: `entity:${entity.url}`,
1740
+ resource: {
1741
+ kind: `entity`,
1742
+ entity
1743
+ },
1744
+ builtInAllowed,
1745
+ request
1746
+ });
1747
+ }
1748
+ async function canAccessEntityType(ctx, entityType, permission, request) {
1749
+ if (isPermissionBypassPrincipal(ctx)) return true;
1750
+ await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
1751
+ const builtInAllowed = await ctx.entityManager.registry.hasEntityTypePermission(entityType.name, permission, principalSubject(ctx.principal));
1752
+ return await applyAuthorizationHook(ctx, {
1753
+ verb: permission,
1754
+ resourceKey: `entity_type:${entityType.name}`,
1755
+ resource: {
1756
+ kind: `entity_type`,
1757
+ entityType
1758
+ },
1759
+ builtInAllowed,
1760
+ request
1761
+ });
1762
+ }
1763
+ async function canRegisterEntityType(ctx, input, request) {
1764
+ if (isPermissionBypassPrincipal(ctx)) return true;
1765
+ return await applyAuthorizationHook(ctx, {
1766
+ verb: `manage`,
1767
+ resourceKey: `entity_type_registration:${input.name}`,
1768
+ resource: {
1769
+ kind: `entity_type_registration`,
1770
+ entityTypeName: input.name
1771
+ },
1772
+ builtInAllowed: true,
1773
+ request
1774
+ });
1775
+ }
1776
+ async function canAccessSharedState(ctx, sharedStateId, permission, request, ownerEntityUrl) {
1777
+ if (isPermissionBypassPrincipal(ctx)) return true;
1778
+ await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
1779
+ const storedLinkedEntityUrls = await ctx.entityManager.registry.listSharedStateLinkedEntityUrls(sharedStateId);
1780
+ const bootstrapEntityUrls = storedLinkedEntityUrls.length === 0 && ownerEntityUrl ? [ownerEntityUrl] : [];
1781
+ const linkedEntityUrls = [...new Set([...storedLinkedEntityUrls, ...bootstrapEntityUrls])];
1782
+ for (const entityUrl of linkedEntityUrls) {
1783
+ const entity = await ctx.entityManager.registry.getEntity(entityUrl);
1784
+ if (!entity) continue;
1785
+ if (entity.created_by === ctx.principal.url || await ctx.entityManager.registry.hasEntityPermission(entity.url, permission, principalSubject(ctx.principal))) return await applyAuthorizationHook(ctx, {
1786
+ verb: permission,
1787
+ resourceKey: `shared_state:${sharedStateId}`,
1788
+ resource: {
1789
+ kind: `shared_state`,
1790
+ sharedStateId,
1791
+ linkedEntityUrls
1792
+ },
1793
+ builtInAllowed: true,
1794
+ request
1795
+ });
1796
+ }
1797
+ return await applyAuthorizationHook(ctx, {
1798
+ verb: permission,
1799
+ resourceKey: `shared_state:${sharedStateId}`,
1800
+ resource: {
1801
+ kind: `shared_state`,
1802
+ sharedStateId,
1803
+ linkedEntityUrls
1804
+ },
1805
+ builtInAllowed: false,
1806
+ request
1807
+ });
1808
+ }
1809
+ async function applyAuthorizationHook(ctx, input) {
1810
+ const hook = ctx.authorizeRequest;
1811
+ if (!hook) return input.builtInAllowed;
1812
+ const cacheKey = [
1813
+ ctx.service,
1814
+ ctx.principal.url,
1815
+ input.verb,
1816
+ input.resourceKey
1817
+ ].join(`|`);
1818
+ const cached = getCachedDecision(hook, cacheKey);
1819
+ if (cached) return cached.decision === `allow`;
1820
+ let decision;
1821
+ try {
1822
+ decision = await hook({
1823
+ tenant: ctx.service,
1824
+ principal: ctx.principal,
1825
+ verb: input.verb,
1826
+ resource: input.resource,
1827
+ request: input.request ? requestMetadata(input.request) : void 0,
1828
+ builtInAllowed: input.builtInAllowed
1829
+ });
1830
+ } catch (error) {
1831
+ serverLog.warn(`[agent-server] authorization hook failed:`, error);
1832
+ return false;
1833
+ }
1834
+ cacheDecision(hook, cacheKey, decision);
1835
+ return decision.decision === `allow`;
1836
+ }
1837
+ function getCachedDecision(hook, cacheKey) {
1838
+ const cache = authzDecisionCache.get(hook);
1839
+ const entry = cache?.get(cacheKey);
1840
+ if (!entry) return null;
1841
+ if (entry.expiresAt <= Date.now()) {
1842
+ cache?.delete(cacheKey);
1843
+ return null;
1844
+ }
1845
+ return { decision: entry.decision };
1846
+ }
1847
+ function cacheDecision(hook, cacheKey, decision) {
1848
+ if (!decision.expires_at) return;
1849
+ const expiresAt = Date.parse(decision.expires_at);
1850
+ if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) return;
1851
+ let cache = authzDecisionCache.get(hook);
1852
+ if (!cache) {
1853
+ cache = new Map();
1854
+ authzDecisionCache.set(hook, cache);
1855
+ }
1856
+ cache.set(cacheKey, {
1857
+ decision: decision.decision,
1858
+ expiresAt
1859
+ });
1860
+ }
1861
+ function requestMetadata(request) {
1862
+ const headers = {};
1863
+ request.headers.forEach((value, key) => {
1864
+ headers[key] = value;
1865
+ });
1866
+ return {
1867
+ method: request.method,
1868
+ url: request.url,
1869
+ headers
1870
+ };
1871
+ }
1872
+
1405
1873
  //#endregion
1406
1874
  //#region src/webhook-signing.ts
1407
1875
  const encoder = new TextEncoder();
@@ -1492,6 +1960,7 @@ const subscriptionControlActions = [
1492
1960
  `ack`,
1493
1961
  `release`
1494
1962
  ];
1963
+ const SHARED_STATE_OWNER_ENTITY_HEADER = `electric-owner-entity`;
1495
1964
  const durableStreamsRouter = Router();
1496
1965
  durableStreamsRouter.put(`/__ds/subscriptions/:subscriptionId`, putSubscriptionBase);
1497
1966
  durableStreamsRouter.get(`/__ds/subscriptions/:subscriptionId`, getSubscriptionBase);
@@ -1709,6 +2178,8 @@ async function webhookJwks(_request, ctx) {
1709
2178
  });
1710
2179
  }
1711
2180
  async function streamAppend(request, ctx) {
2181
+ const auth = await authorizeDurableStreamAccess(request, ctx);
2182
+ if (auth) return auth;
1712
2183
  return await electricAgentsStreamAppendRouter.fetch(createStreamAppendRouteRequest(request), ctx.runtime, (req, body) => forwardFetchRequest({
1713
2184
  request: {
1714
2185
  method: req.method,
@@ -1725,8 +2196,9 @@ async function streamAppend(request, ctx) {
1725
2196
  }));
1726
2197
  }
1727
2198
  async function proxyPassThrough(request, ctx) {
2199
+ const auth = await authorizeDurableStreamAccess(request, ctx);
2200
+ if (auth) return auth;
1728
2201
  const streamPath = new URL(request.url).pathname;
1729
- if (ctx.entityManager?.isAttachmentStreamPath(streamPath)) return new Response(null, { status: 404 });
1730
2202
  const upstream = await forwardToDurableStreams(ctx, request);
1731
2203
  const method = request.method.toUpperCase();
1732
2204
  const endTrackedRead = method === `GET` ? await ctx.entityBridgeManager.beginClientRead(streamPath) : null;
@@ -1737,6 +2209,51 @@ async function proxyPassThrough(request, ctx) {
1737
2209
  await endTrackedRead?.();
1738
2210
  }
1739
2211
  }
2212
+ async function authorizeDurableStreamAccess(request, ctx) {
2213
+ const method = request.method.toUpperCase();
2214
+ const streamPath = new URL(request.url).pathname;
2215
+ if (method === `GET` || method === `HEAD`) {
2216
+ const registry = ctx.entityManager?.registry;
2217
+ const entity = registry?.getEntityByStream ? await registry.getEntityByStream(streamPath) : null;
2218
+ if (entity) {
2219
+ if (await canAccessEntity(ctx, entity, `read`, request)) return void 0;
2220
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to read ${entity.url}`);
2221
+ }
2222
+ const attachmentEntityUrl = entityUrlFromAttachmentStreamPath(streamPath);
2223
+ if (attachmentEntityUrl) {
2224
+ const attachmentEntity = registry?.getEntity ? await registry.getEntity(attachmentEntityUrl) : null;
2225
+ if (!attachmentEntity) return apiError(404, ErrCodeNotFound, `Entity not found`);
2226
+ if (await canAccessEntity(ctx, attachmentEntity, `read`, request)) return void 0;
2227
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to read ${attachmentEntity.url}`);
2228
+ }
2229
+ }
2230
+ const sharedStateId = sharedStateIdFromPath(streamPath);
2231
+ if (!sharedStateId) return void 0;
2232
+ if (method === `GET` || method === `HEAD`) {
2233
+ if (await canAccessSharedState(ctx, sharedStateId, `read`, request)) return void 0;
2234
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to read shared state`);
2235
+ }
2236
+ if (method === `PUT` || method === `POST`) {
2237
+ const ownerEntityUrl = request.headers.get(SHARED_STATE_OWNER_ENTITY_HEADER)?.trim() || void 0;
2238
+ if (await canAccessSharedState(ctx, sharedStateId, `write`, request, ownerEntityUrl)) return void 0;
2239
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to write shared state`);
2240
+ }
2241
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to access shared state`);
2242
+ }
2243
+ function entityUrlFromAttachmentStreamPath(path$1) {
2244
+ const match = path$1.match(/^\/([^/]+)\/([^/]+)\/attachments\/[^/]+$/);
2245
+ if (!match) return null;
2246
+ return `/${match[1]}/${match[2]}`;
2247
+ }
2248
+ function sharedStateIdFromPath(path$1) {
2249
+ const match = path$1.match(/^\/_electric\/shared-state\/([^/]+)$/);
2250
+ if (!match) return null;
2251
+ try {
2252
+ return decodeURIComponent(match[1]);
2253
+ } catch {
2254
+ return match[1];
2255
+ }
2256
+ }
1740
2257
 
1741
2258
  //#endregion
1742
2259
  //#region src/routing/electric-proxy-router.ts
@@ -1744,12 +2261,15 @@ const electricProxyRouter = Router({ base: `/_electric/electric` });
1744
2261
  electricProxyRouter.get(`/*`, proxyElectric);
1745
2262
  async function proxyElectric(request, ctx) {
1746
2263
  if (!ctx.electricUrl) return apiError(500, `ELECTRIC_PROXY_FAILED`, `Electric URL not configured`);
2264
+ await ctx.entityManager.registry.pruneExpiredPermissionGrants?.();
1747
2265
  const target = buildElectricProxyTarget({
1748
2266
  incomingUrl: new URL(request.url),
1749
2267
  electricUrl: ctx.electricUrl,
1750
2268
  electricSecret: ctx.electricSecret,
1751
2269
  tenantId: ctx.service,
1752
- principalUrl: ctx.principal.url
2270
+ principalUrl: ctx.principal.url,
2271
+ principalKind: ctx.principal.kind,
2272
+ permissionBypass: isPermissionBypassPrincipal(ctx)
1753
2273
  });
1754
2274
  const headers = new Headers(request.headers);
1755
2275
  headers.delete(`host`);
@@ -1769,110 +2289,6 @@ async function proxyElectric(request, ctx) {
1769
2289
  });
1770
2290
  }
1771
2291
 
1772
- //#endregion
1773
- //#region src/principal.ts
1774
- const ELECTRIC_PRINCIPAL_HEADER = `electric-principal`;
1775
- const PRINCIPAL_KINDS = new Set([
1776
- `user`,
1777
- `agent`,
1778
- `service`,
1779
- `system`
1780
- ]);
1781
- function parsePrincipalKey(input) {
1782
- const colon = input.indexOf(`:`);
1783
- if (colon <= 0) throw new Error(`Invalid principal identifier`);
1784
- const kind = input.slice(0, colon);
1785
- const id = input.slice(colon + 1);
1786
- if (!PRINCIPAL_KINDS.has(kind)) throw new Error(`Invalid principal kind`);
1787
- if (!id || id.includes(`/`)) throw new Error(`Invalid principal id`);
1788
- const key = `${kind}:${id}`;
1789
- return {
1790
- kind,
1791
- id,
1792
- key,
1793
- url: `/principal/${encodeURIComponent(key)}`
1794
- };
1795
- }
1796
- function principalUrl(key) {
1797
- return parsePrincipalKey(key).url;
1798
- }
1799
- function parsePrincipalUrl(url) {
1800
- if (!url.startsWith(`/principal/`)) return null;
1801
- const segment = url.slice(`/principal/`.length);
1802
- if (!segment || segment.includes(`/`)) return null;
1803
- try {
1804
- return parsePrincipalKey(decodeURIComponent(segment));
1805
- } catch {
1806
- return null;
1807
- }
1808
- }
1809
- function parsePrincipalInput(input) {
1810
- const urlPrincipal = parsePrincipalUrl(input);
1811
- if (urlPrincipal) return urlPrincipal;
1812
- try {
1813
- return parsePrincipalKey(input);
1814
- } catch {
1815
- return null;
1816
- }
1817
- }
1818
- function getPrincipalFromRequest(request) {
1819
- const value = request.headers.get(ELECTRIC_PRINCIPAL_HEADER);
1820
- if (!value) return null;
1821
- return parsePrincipalInput(value);
1822
- }
1823
- function getDevPrincipal() {
1824
- return parsePrincipalKey(`system:dev-local`);
1825
- }
1826
- const BUILT_IN_SYSTEM_PRINCIPAL_IDS = new Set([
1827
- `framework`,
1828
- `auth-sync`,
1829
- `dev-local`
1830
- ]);
1831
- function isBuiltInSystemPrincipalUrl(url) {
1832
- if (!url?.startsWith(`/principal/`)) return false;
1833
- try {
1834
- const principal = parsePrincipalUrl(url);
1835
- if (!principal) return false;
1836
- return principal.kind === `system` && BUILT_IN_SYSTEM_PRINCIPAL_IDS.has(principal.id);
1837
- } catch {
1838
- return false;
1839
- }
1840
- }
1841
- function principalFromCreatedBy(createdBy) {
1842
- if (!createdBy) return void 0;
1843
- const principal = parsePrincipalUrl(createdBy);
1844
- if (!principal) return {
1845
- url: createdBy,
1846
- key: null
1847
- };
1848
- return {
1849
- url: principal.url,
1850
- key: principal.key,
1851
- kind: principal.kind,
1852
- id: principal.id
1853
- };
1854
- }
1855
- const principalIdentityStateSchema = Type.Object({
1856
- kind: Type.Union([
1857
- Type.Literal(`user`),
1858
- Type.Literal(`agent`),
1859
- Type.Literal(`service`),
1860
- Type.Literal(`system`)
1861
- ]),
1862
- id: Type.String(),
1863
- key: Type.String(),
1864
- url: Type.String(),
1865
- updated_at: Type.String(),
1866
- display_name: Type.Optional(Type.String()),
1867
- email: Type.Optional(Type.String()),
1868
- avatar_url: Type.Optional(Type.String()),
1869
- auth_provider: Type.Optional(Type.String()),
1870
- auth_subject: Type.Optional(Type.String()),
1871
- claims: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
1872
- created_at: Type.Optional(Type.String())
1873
- }, { additionalProperties: false });
1874
- const principalUpdateIdentityMessageSchema = Type.Object({ identity: principalIdentityStateSchema }, { additionalProperties: false });
1875
-
1876
2292
  //#endregion
1877
2293
  //#region src/dispatch-policy-schema.ts
1878
2294
  const nonEmptyStringSchema = Type.String({ minLength: 1 });
@@ -2046,16 +2462,26 @@ function isDuplicateUrlError(err) {
2046
2462
  return e.code === `23505`;
2047
2463
  }
2048
2464
  const DEFAULT_RUNNER_LEASE_MS = 3e4;
2465
+ const PERMISSION_PRUNE_INTERVAL_MS = 3e4;
2049
2466
  function runnerWakeStream(runnerId) {
2050
2467
  return `/runners/${runnerId}/wake`;
2051
2468
  }
2052
2469
  var PostgresRegistry = class {
2470
+ lastPermissionPruneStartedAt = 0;
2471
+ permissionPrunePromise = null;
2053
2472
  constructor(db, tenantId = DEFAULT_TENANT_ID) {
2054
2473
  this.db = db;
2055
2474
  this.tenantId = tenantId;
2056
2475
  }
2057
2476
  async initialize() {}
2058
2477
  close() {}
2478
+ async ensureUserForPrincipal(principal) {
2479
+ if (principal.kind !== `user`) return;
2480
+ await this.db.insert(users).values({
2481
+ tenantId: this.tenantId,
2482
+ id: principal.id
2483
+ }).onConflictDoNothing();
2484
+ }
2059
2485
  async createRunner(input) {
2060
2486
  const now = new Date();
2061
2487
  const wakeStream = input.wakeStream ?? runnerWakeStream(input.id);
@@ -2300,6 +2726,7 @@ var PostgresRegistry = class {
2300
2726
  creationSchema: et.creation_schema ?? null,
2301
2727
  inboxSchemas: et.inbox_schemas ?? null,
2302
2728
  stateSchemas: et.state_schemas ?? null,
2729
+ slashCommands: et.slash_commands ?? null,
2303
2730
  serveEndpoint: et.serve_endpoint ?? null,
2304
2731
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
2305
2732
  revision: et.revision,
@@ -2312,6 +2739,7 @@ var PostgresRegistry = class {
2312
2739
  creationSchema: et.creation_schema ?? null,
2313
2740
  inboxSchemas: et.inbox_schemas ?? null,
2314
2741
  stateSchemas: et.state_schemas ?? null,
2742
+ slashCommands: et.slash_commands ?? null,
2315
2743
  serveEndpoint: et.serve_endpoint ?? null,
2316
2744
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
2317
2745
  revision: et.revision,
@@ -2329,6 +2757,7 @@ var PostgresRegistry = class {
2329
2757
  creationSchema: et.creation_schema ?? null,
2330
2758
  inboxSchemas: et.inbox_schemas ?? null,
2331
2759
  stateSchemas: et.state_schemas ?? null,
2760
+ slashCommands: et.slash_commands ?? null,
2332
2761
  serveEndpoint: et.serve_endpoint ?? null,
2333
2762
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
2334
2763
  revision: et.revision,
@@ -2355,6 +2784,7 @@ var PostgresRegistry = class {
2355
2784
  creationSchema: et.creation_schema ?? null,
2356
2785
  inboxSchemas: et.inbox_schemas ?? null,
2357
2786
  stateSchemas: et.state_schemas ?? null,
2787
+ slashCommands: et.slash_commands ?? null,
2358
2788
  serveEndpoint: et.serve_endpoint ?? null,
2359
2789
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
2360
2790
  revision: et.revision,
@@ -2390,6 +2820,59 @@ var PostgresRegistry = class {
2390
2820
  pendingSourceStreams: [],
2391
2821
  updatedAt: new Date()
2392
2822
  }).onConflictDoNothing();
2823
+ await tx.insert(entityLineage).values({
2824
+ tenantId: this.tenantId,
2825
+ ancestorUrl: entity.url,
2826
+ descendantUrl: entity.url,
2827
+ depth: 0
2828
+ }).onConflictDoNothing();
2829
+ if (entity.parent) await tx.execute(sql`
2830
+ INSERT INTO ${entityLineage} (
2831
+ tenant_id,
2832
+ ancestor_url,
2833
+ descendant_url,
2834
+ depth
2835
+ )
2836
+ SELECT
2837
+ ${this.tenantId},
2838
+ ancestor_url,
2839
+ ${entity.url},
2840
+ depth + 1
2841
+ FROM ${entityLineage}
2842
+ WHERE tenant_id = ${this.tenantId}
2843
+ AND descendant_url = ${entity.parent}
2844
+ ON CONFLICT DO NOTHING
2845
+ `);
2846
+ await tx.execute(sql`
2847
+ INSERT INTO ${entityEffectivePermissions} (
2848
+ tenant_id,
2849
+ entity_url,
2850
+ source_entity_url,
2851
+ source_grant_id,
2852
+ permission,
2853
+ subject_kind,
2854
+ subject_value,
2855
+ expires_at
2856
+ )
2857
+ SELECT
2858
+ ${this.tenantId},
2859
+ ${entity.url},
2860
+ grants.entity_url,
2861
+ grants.id,
2862
+ grants.permission,
2863
+ grants.subject_kind,
2864
+ grants.subject_value,
2865
+ grants.expires_at
2866
+ FROM ${entityPermissionGrants} grants
2867
+ JOIN ${entityLineage} lineage
2868
+ ON lineage.tenant_id = grants.tenant_id
2869
+ AND lineage.ancestor_url = grants.entity_url
2870
+ AND lineage.descendant_url = ${entity.url}
2871
+ WHERE grants.tenant_id = ${this.tenantId}
2872
+ AND grants.propagation = 'descendants'
2873
+ AND (grants.expires_at IS NULL OR grants.expires_at > now())
2874
+ ON CONFLICT DO NOTHING
2875
+ `);
2393
2876
  return parseInt(result[0].txid);
2394
2877
  });
2395
2878
  } catch (err) {
@@ -2411,10 +2894,8 @@ var PostgresRegistry = class {
2411
2894
  }
2412
2895
  async getEntityByStream(streamPath) {
2413
2896
  const mainSuffix = `/main`;
2414
- const errorSuffix = `/error`;
2415
2897
  let entityUrl = null;
2416
2898
  if (streamPath.endsWith(mainSuffix)) entityUrl = streamPath.slice(0, -mainSuffix.length);
2417
- else if (streamPath.endsWith(errorSuffix)) entityUrl = streamPath.slice(0, -errorSuffix.length);
2418
2899
  if (!entityUrl) return null;
2419
2900
  return this.getEntity(entityUrl);
2420
2901
  }
@@ -2424,6 +2905,23 @@ var PostgresRegistry = class {
2424
2905
  if (filter?.status) conditions.push(eq(entities.status, filter.status));
2425
2906
  if (filter?.parent) conditions.push(eq(entities.parent, filter.parent));
2426
2907
  if (filter?.created_by) conditions.push(eq(entities.createdBy, filter.created_by));
2908
+ if (filter?.readableBy && !filter.readableBy.bypass) conditions.push(sql`(
2909
+ ${entities.createdBy} = ${filter.readableBy.principalUrl}
2910
+ OR ${entities.url} IN (
2911
+ SELECT ${entityEffectivePermissions.entityUrl}
2912
+ FROM ${entityEffectivePermissions}
2913
+ WHERE ${entityEffectivePermissions.tenantId} = ${this.tenantId}
2914
+ AND ${entityEffectivePermissions.permission} IN ('read', 'manage')
2915
+ AND (${entityEffectivePermissions.expiresAt} IS NULL OR ${entityEffectivePermissions.expiresAt} > now())
2916
+ AND (
2917
+ (${entityEffectivePermissions.subjectKind} = 'principal'
2918
+ AND ${entityEffectivePermissions.subjectValue} = ${filter.readableBy.principalUrl})
2919
+ OR
2920
+ (${entityEffectivePermissions.subjectKind} = 'principal_kind'
2921
+ AND ${entityEffectivePermissions.subjectValue} = ${filter.readableBy.principalKind})
2922
+ )
2923
+ )
2924
+ )`);
2427
2925
  const whereClause = and(...conditions);
2428
2926
  const countResult = await this.db.select({ count: sql`count(*)` }).from(entities).where(whereClause);
2429
2927
  const total = Number(countResult[0].count);
@@ -2436,6 +2934,189 @@ var PostgresRegistry = class {
2436
2934
  total
2437
2935
  };
2438
2936
  }
2937
+ async createEntityTypePermissionGrant(input) {
2938
+ const [row] = await this.db.insert(entityTypePermissionGrants).values({
2939
+ tenantId: this.tenantId,
2940
+ entityType: input.entityType,
2941
+ permission: input.permission,
2942
+ subjectKind: input.subjectKind,
2943
+ subjectValue: input.subjectValue,
2944
+ createdBy: input.createdBy ?? null,
2945
+ expiresAt: input.expiresAt ?? null
2946
+ }).returning();
2947
+ return this.rowToEntityTypePermissionGrant(row);
2948
+ }
2949
+ async ensureEntityTypePermissionGrant(input) {
2950
+ 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);
2951
+ if (existing) return this.rowToEntityTypePermissionGrant(existing);
2952
+ return await this.createEntityTypePermissionGrant(input);
2953
+ }
2954
+ async listEntityTypePermissionGrants(entityType) {
2955
+ const rows = await this.db.select().from(entityTypePermissionGrants).where(and(eq(entityTypePermissionGrants.tenantId, this.tenantId), eq(entityTypePermissionGrants.entityType, entityType))).orderBy(entityTypePermissionGrants.id);
2956
+ return rows.map((row) => this.rowToEntityTypePermissionGrant(row));
2957
+ }
2958
+ async deleteEntityTypePermissionGrant(entityType, grantId) {
2959
+ 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 });
2960
+ return rows.length > 0;
2961
+ }
2962
+ async hasEntityTypePermission(entityType, permission, subject) {
2963
+ const permissions = [permission, `manage`];
2964
+ 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`(
2965
+ (${entityTypePermissionGrants.subjectKind} = 'principal'
2966
+ AND ${entityTypePermissionGrants.subjectValue} = ${subject.principalUrl})
2967
+ OR
2968
+ (${entityTypePermissionGrants.subjectKind} = 'principal_kind'
2969
+ AND ${entityTypePermissionGrants.subjectValue} = ${subject.principalKind})
2970
+ )`)).limit(1);
2971
+ return rows.length > 0;
2972
+ }
2973
+ async createEntityPermissionGrant(input) {
2974
+ return await this.db.transaction(async (tx) => {
2975
+ const [row] = await tx.insert(entityPermissionGrants).values({
2976
+ tenantId: this.tenantId,
2977
+ entityUrl: input.entityUrl,
2978
+ permission: input.permission,
2979
+ subjectKind: input.subjectKind,
2980
+ subjectValue: input.subjectValue,
2981
+ propagation: input.propagation ?? `self`,
2982
+ copyToChildren: input.copyToChildren ?? false,
2983
+ createdBy: input.createdBy ?? null,
2984
+ expiresAt: input.expiresAt ?? null
2985
+ }).returning();
2986
+ await this.materializeEntityPermissionGrant(tx, row);
2987
+ return this.rowToEntityPermissionGrant(row);
2988
+ });
2989
+ }
2990
+ async listEntityPermissionGrants(entityUrl) {
2991
+ const rows = await this.db.select().from(entityPermissionGrants).where(and(eq(entityPermissionGrants.tenantId, this.tenantId), eq(entityPermissionGrants.entityUrl, entityUrl))).orderBy(entityPermissionGrants.id);
2992
+ return rows.map((row) => this.rowToEntityPermissionGrant(row));
2993
+ }
2994
+ async deleteEntityPermissionGrant(entityUrl, grantId) {
2995
+ return await this.db.transaction(async (tx) => {
2996
+ await tx.delete(entityEffectivePermissions).where(and(eq(entityEffectivePermissions.tenantId, this.tenantId), eq(entityEffectivePermissions.sourceGrantId, grantId)));
2997
+ 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 });
2998
+ return rows.length > 0;
2999
+ });
3000
+ }
3001
+ async copyEntityPermissionGrantsForSpawn(parentEntityUrl, childEntityUrl, createdBy) {
3002
+ 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())`));
3003
+ const copied = [];
3004
+ for (const grant of parentGrants) copied.push(await this.createEntityPermissionGrant({
3005
+ entityUrl: childEntityUrl,
3006
+ permission: grant.permission,
3007
+ subjectKind: grant.subjectKind,
3008
+ subjectValue: grant.subjectValue,
3009
+ propagation: `self`,
3010
+ copyToChildren: grant.copyToChildren,
3011
+ createdBy,
3012
+ expiresAt: grant.expiresAt ?? void 0
3013
+ }));
3014
+ return copied;
3015
+ }
3016
+ async hasEntityPermission(entityUrl, permission, subject) {
3017
+ const permissions = [permission, `manage`];
3018
+ 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`(
3019
+ (${entityEffectivePermissions.subjectKind} = 'principal'
3020
+ AND ${entityEffectivePermissions.subjectValue} = ${subject.principalUrl})
3021
+ OR
3022
+ (${entityEffectivePermissions.subjectKind} = 'principal_kind'
3023
+ AND ${entityEffectivePermissions.subjectValue} = ${subject.principalKind})
3024
+ )`)).limit(1);
3025
+ return rows.length > 0;
3026
+ }
3027
+ async replaceSharedStateLink(ownerEntityUrl, manifestKey, sharedStateId) {
3028
+ await this.db.delete(sharedStateLinks).where(and(eq(sharedStateLinks.tenantId, this.tenantId), eq(sharedStateLinks.ownerEntityUrl, ownerEntityUrl), eq(sharedStateLinks.manifestKey, manifestKey)));
3029
+ if (!sharedStateId) return;
3030
+ await this.db.insert(sharedStateLinks).values({
3031
+ tenantId: this.tenantId,
3032
+ ownerEntityUrl,
3033
+ manifestKey,
3034
+ sharedStateId
3035
+ }).onConflictDoUpdate({
3036
+ target: [
3037
+ sharedStateLinks.tenantId,
3038
+ sharedStateLinks.ownerEntityUrl,
3039
+ sharedStateLinks.manifestKey
3040
+ ],
3041
+ set: {
3042
+ sharedStateId,
3043
+ updatedAt: new Date()
3044
+ }
3045
+ });
3046
+ }
3047
+ async listSharedStateLinkedEntityUrls(sharedStateId) {
3048
+ const rows = await this.db.selectDistinct({ ownerEntityUrl: sharedStateLinks.ownerEntityUrl }).from(sharedStateLinks).where(and(eq(sharedStateLinks.tenantId, this.tenantId), eq(sharedStateLinks.sharedStateId, sharedStateId)));
3049
+ return rows.map((row) => row.ownerEntityUrl);
3050
+ }
3051
+ async pruneExpiredPermissionGrants(now = new Date(), options = {}) {
3052
+ if (this.permissionPrunePromise) return await this.permissionPrunePromise;
3053
+ const startedAt = Date.now();
3054
+ if (!options.force && startedAt - this.lastPermissionPruneStartedAt < PERMISSION_PRUNE_INTERVAL_MS) return;
3055
+ this.lastPermissionPruneStartedAt = startedAt;
3056
+ const promise = this.pruneExpiredPermissionGrantsNow(now);
3057
+ this.permissionPrunePromise = promise;
3058
+ try {
3059
+ await promise;
3060
+ } catch (error) {
3061
+ this.lastPermissionPruneStartedAt = 0;
3062
+ throw error;
3063
+ } finally {
3064
+ if (this.permissionPrunePromise === promise) this.permissionPrunePromise = null;
3065
+ }
3066
+ }
3067
+ async pruneExpiredPermissionGrantsNow(now) {
3068
+ await this.db.transaction(async (tx) => {
3069
+ 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)));
3070
+ const ids = expiredEntityGrantIds.map((row) => row.id);
3071
+ if (ids.length > 0) {
3072
+ await tx.delete(entityEffectivePermissions).where(and(eq(entityEffectivePermissions.tenantId, this.tenantId), inArray(entityEffectivePermissions.sourceGrantId, ids)));
3073
+ await tx.delete(entityPermissionGrants).where(and(eq(entityPermissionGrants.tenantId, this.tenantId), inArray(entityPermissionGrants.id, ids)));
3074
+ }
3075
+ await tx.delete(entityEffectivePermissions).where(and(eq(entityEffectivePermissions.tenantId, this.tenantId), sql`${entityEffectivePermissions.expiresAt} IS NOT NULL`, lt(entityEffectivePermissions.expiresAt, now)));
3076
+ await tx.delete(entityTypePermissionGrants).where(and(eq(entityTypePermissionGrants.tenantId, this.tenantId), sql`${entityTypePermissionGrants.expiresAt} IS NOT NULL`, lt(entityTypePermissionGrants.expiresAt, now)));
3077
+ });
3078
+ }
3079
+ async materializeEntityPermissionGrant(tx, grant) {
3080
+ await tx.delete(entityEffectivePermissions).where(and(eq(entityEffectivePermissions.tenantId, this.tenantId), eq(entityEffectivePermissions.sourceGrantId, grant.id)));
3081
+ if (grant.propagation === `descendants`) {
3082
+ await tx.execute(sql`
3083
+ INSERT INTO ${entityEffectivePermissions} (
3084
+ tenant_id,
3085
+ entity_url,
3086
+ source_entity_url,
3087
+ source_grant_id,
3088
+ permission,
3089
+ subject_kind,
3090
+ subject_value,
3091
+ expires_at
3092
+ )
3093
+ SELECT
3094
+ ${this.tenantId},
3095
+ descendant_url,
3096
+ ${grant.entityUrl},
3097
+ ${grant.id},
3098
+ ${grant.permission},
3099
+ ${grant.subjectKind},
3100
+ ${grant.subjectValue},
3101
+ ${grant.expiresAt}
3102
+ FROM ${entityLineage}
3103
+ WHERE tenant_id = ${this.tenantId}
3104
+ AND ancestor_url = ${grant.entityUrl}
3105
+ ON CONFLICT DO NOTHING
3106
+ `);
3107
+ return;
3108
+ }
3109
+ await tx.insert(entityEffectivePermissions).values({
3110
+ tenantId: this.tenantId,
3111
+ entityUrl: grant.entityUrl,
3112
+ sourceEntityUrl: grant.entityUrl,
3113
+ sourceGrantId: grant.id,
3114
+ permission: grant.permission,
3115
+ subjectKind: grant.subjectKind,
3116
+ subjectValue: grant.subjectValue,
3117
+ expiresAt: grant.expiresAt
3118
+ }).onConflictDoNothing();
3119
+ }
2439
3120
  async updateStatus(entityUrl, status$1) {
2440
3121
  const whereClause = isTerminalEntityStatus(status$1) ? this.entityWhere(entityUrl) : and(this.entityWhere(entityUrl), ne(entities.status, `stopped`), ne(entities.status, `killed`));
2441
3122
  await this.db.update(entities).set({
@@ -2537,7 +3218,9 @@ var PostgresRegistry = class {
2537
3218
  tenantId: this.tenantId,
2538
3219
  sourceRef: row.sourceRef,
2539
3220
  tags: normalizeTags(row.tags),
2540
- streamUrl: row.streamUrl
3221
+ streamUrl: row.streamUrl,
3222
+ principalUrl: row.principalUrl,
3223
+ principalKind: row.principalKind
2541
3224
  }).onConflictDoNothing();
2542
3225
  const existing = await this.getEntityBridge(row.sourceRef);
2543
3226
  if (!existing) throw new Error(`Failed to load entity bridge ${row.sourceRef}`);
@@ -2692,6 +3375,7 @@ var PostgresRegistry = class {
2692
3375
  creation_schema: row.creationSchema,
2693
3376
  inbox_schemas: row.inboxSchemas,
2694
3377
  state_schemas: row.stateSchemas,
3378
+ slash_commands: row.slashCommands ?? void 0,
2695
3379
  serve_endpoint: row.serveEndpoint ?? void 0,
2696
3380
  default_dispatch_policy: row.defaultDispatchPolicy ?? void 0,
2697
3381
  revision: row.revision,
@@ -2699,15 +3383,40 @@ var PostgresRegistry = class {
2699
3383
  updated_at: row.updatedAt
2700
3384
  };
2701
3385
  }
3386
+ rowToEntityTypePermissionGrant(row) {
3387
+ return {
3388
+ id: row.id,
3389
+ entity_type: row.entityType,
3390
+ permission: row.permission,
3391
+ subject_kind: row.subjectKind,
3392
+ subject_value: row.subjectValue,
3393
+ created_by: row.createdBy ?? void 0,
3394
+ expires_at: row.expiresAt?.toISOString(),
3395
+ created_at: row.createdAt.toISOString(),
3396
+ updated_at: row.updatedAt.toISOString()
3397
+ };
3398
+ }
3399
+ rowToEntityPermissionGrant(row) {
3400
+ return {
3401
+ id: row.id,
3402
+ entity_url: row.entityUrl,
3403
+ permission: row.permission,
3404
+ subject_kind: row.subjectKind,
3405
+ subject_value: row.subjectValue,
3406
+ propagation: row.propagation,
3407
+ copy_to_children: row.copyToChildren,
3408
+ created_by: row.createdBy ?? void 0,
3409
+ expires_at: row.expiresAt?.toISOString(),
3410
+ created_at: row.createdAt.toISOString(),
3411
+ updated_at: row.updatedAt.toISOString()
3412
+ };
3413
+ }
2702
3414
  rowToEntity(row) {
2703
3415
  return {
2704
3416
  url: row.url,
2705
3417
  type: row.type,
2706
3418
  status: assertEntityStatus(row.status),
2707
- streams: {
2708
- main: `${row.url}/main`,
2709
- error: `${row.url}/error`
2710
- },
3419
+ streams: { main: `${row.url}/main` },
2711
3420
  subscription_id: row.subscriptionId,
2712
3421
  dispatch_policy: row.dispatchPolicy ?? void 0,
2713
3422
  write_token: row.writeToken,
@@ -2729,6 +3438,8 @@ var PostgresRegistry = class {
2729
3438
  sourceRef: row.sourceRef,
2730
3439
  tags: row.tags ?? {},
2731
3440
  streamUrl: row.streamUrl,
3441
+ principalUrl: row.principalUrl ?? void 0,
3442
+ principalKind: row.principalKind ?? void 0,
2732
3443
  shapeHandle: row.shapeHandle ?? void 0,
2733
3444
  shapeOffset: row.shapeOffset ?? void 0,
2734
3445
  lastObserverActivityAt: row.lastObserverActivityAt,
@@ -2991,6 +3702,7 @@ var EntityManager = class {
2991
3702
  this.validateSchema(req.creation_schema);
2992
3703
  this.validateSchemaMap(req.inbox_schemas);
2993
3704
  this.validateSchemaMap(req.state_schemas);
3705
+ this.validateSlashCommands(req.slash_commands);
2994
3706
  const defaultDispatchPolicy = req.default_dispatch_policy ? this.validateDispatchPolicy(req.default_dispatch_policy, { label: `default_dispatch_policy` }) : void 0;
2995
3707
  const existing = await this.registry.getEntityType(req.name);
2996
3708
  const now = new Date().toISOString();
@@ -3000,6 +3712,7 @@ var EntityManager = class {
3000
3712
  creation_schema: req.creation_schema,
3001
3713
  inbox_schemas: req.inbox_schemas,
3002
3714
  state_schemas: req.state_schemas,
3715
+ slash_commands: req.slash_commands,
3003
3716
  serve_endpoint: req.serve_endpoint,
3004
3717
  default_dispatch_policy: defaultDispatchPolicy,
3005
3718
  revision: existing ? existing.revision + 1 : 1,
@@ -3031,7 +3744,10 @@ var EntityManager = class {
3031
3744
  }
3032
3745
  async ensurePrincipal(principal) {
3033
3746
  const existing = await this.registry.getEntity(principal.url);
3034
- if (existing) return existing;
3747
+ if (existing) {
3748
+ await this.ensureUserPrincipal(principal);
3749
+ return existing;
3750
+ }
3035
3751
  await this.ensurePrincipalEntityType();
3036
3752
  try {
3037
3753
  const entity = await this.spawn(`principal`, {
@@ -3060,15 +3776,22 @@ var EntityManager = class {
3060
3776
  updated_at: now
3061
3777
  }
3062
3778
  }));
3779
+ await this.ensureUserPrincipal(principal);
3063
3780
  return entity;
3064
3781
  } catch (error) {
3065
3782
  if (error instanceof ElectricAgentsError && error.code === ErrCodeDuplicateURL) {
3066
3783
  const raced = await this.registry.getEntity(principal.url);
3067
- if (raced) return raced;
3784
+ if (raced) {
3785
+ await this.ensureUserPrincipal(principal);
3786
+ return raced;
3787
+ }
3068
3788
  }
3069
3789
  throw error;
3070
3790
  }
3071
3791
  }
3792
+ async ensureUserPrincipal(principal) {
3793
+ if (principal.kind === `user`) await this.registry.ensureUserForPrincipal(principal);
3794
+ }
3072
3795
  /**
3073
3796
  * Spawn a new entity of the given type with durable streams.
3074
3797
  */
@@ -3098,7 +3821,6 @@ var EntityManager = class {
3098
3821
  const writeToken = randomUUID();
3099
3822
  const entityURL = typeName === `principal` ? principalUrl(instanceId) : `/${typeName}/${instanceId}`;
3100
3823
  const mainPath = `${entityURL}/main`;
3101
- const errorPath = `${entityURL}/error`;
3102
3824
  const subscriptionId = `${typeName}-handler`;
3103
3825
  const spawnT0 = performance.now();
3104
3826
  const existingByURL = await this.registry.getEntity(entityURL);
@@ -3115,10 +3837,7 @@ var EntityManager = class {
3115
3837
  type: typeName,
3116
3838
  status: `idle`,
3117
3839
  url: entityURL,
3118
- streams: {
3119
- main: mainPath,
3120
- error: errorPath
3121
- },
3840
+ streams: { main: mainPath },
3122
3841
  subscription_id: subscriptionId,
3123
3842
  dispatch_policy: dispatchPolicy,
3124
3843
  write_token: writeToken,
@@ -3155,6 +3874,18 @@ var EntityManager = class {
3155
3874
  }
3156
3875
  });
3157
3876
  const initialEvents = [createdEvent];
3877
+ const slashCommandTimestamp = new Date().toISOString();
3878
+ for (const command of entityType.slash_commands ?? []) {
3879
+ const slashCommandEvent = entityStateSchema.slashCommands.insert({
3880
+ key: command.name,
3881
+ value: {
3882
+ ...command,
3883
+ source: `static`,
3884
+ updated_at: slashCommandTimestamp
3885
+ }
3886
+ });
3887
+ initialEvents.push(slashCommandEvent);
3888
+ }
3158
3889
  if (req.initialMessage !== void 0) {
3159
3890
  const msgNow = new Date().toISOString();
3160
3891
  const inboxEvent = entityStateSchema.inbox.insert({
@@ -3162,6 +3893,7 @@ var EntityManager = class {
3162
3893
  value: {
3163
3894
  from: req.created_by ?? req.parent ?? `spawn`,
3164
3895
  payload: req.initialMessage,
3896
+ message_type: req.initialMessageType,
3165
3897
  timestamp: msgNow
3166
3898
  }
3167
3899
  });
@@ -3171,55 +3903,43 @@ var EntityManager = class {
3171
3903
  const queueEnterT0 = performance.now();
3172
3904
  const queueWaiting = this.spawnPersistQueue.length();
3173
3905
  const queueRunning = this.spawnPersistQueue.running();
3174
- const [mainStreamResult, errorStreamResult, entityResult] = await this.spawnPersistQueue.push(async () => {
3906
+ const [mainStreamResult, entityResult] = await this.spawnPersistQueue.push(async () => {
3175
3907
  let entityTxid;
3176
3908
  try {
3177
3909
  entityTxid = await withSpan(`db.createEntity`, () => this.registry.createEntity(entityData));
3178
3910
  } catch (err) {
3179
- return [
3180
- {
3181
- status: `fulfilled`,
3182
- value: void 0
3183
- },
3184
- {
3185
- status: `fulfilled`,
3186
- value: void 0
3187
- },
3188
- {
3189
- status: `rejected`,
3190
- reason: err
3191
- }
3192
- ];
3911
+ return [{
3912
+ status: `fulfilled`,
3913
+ value: void 0
3914
+ }, {
3915
+ status: `rejected`,
3916
+ reason: err
3917
+ }];
3193
3918
  }
3194
- const [mainStreamResult$1, errorStreamResult$1] = await Promise.allSettled([this.streamClient.create(mainPath, {
3919
+ const [mainStreamResult$1] = await Promise.allSettled([this.streamClient.create(mainPath, {
3195
3920
  contentType,
3196
3921
  body: initialBody
3197
- }), this.streamClient.create(errorPath, { contentType })]);
3198
- return [
3199
- mainStreamResult$1,
3200
- errorStreamResult$1,
3201
- {
3202
- status: `fulfilled`,
3203
- value: entityTxid
3204
- }
3205
- ];
3922
+ })]);
3923
+ return [mainStreamResult$1, {
3924
+ status: `fulfilled`,
3925
+ value: entityTxid
3926
+ }];
3206
3927
  });
3207
3928
  const parallelMs = +(performance.now() - queueEnterT0).toFixed(2);
3208
- if (mainStreamResult.status === `rejected` || errorStreamResult.status === `rejected` || entityResult.status === `rejected`) {
3929
+ if (mainStreamResult.status === `rejected` || entityResult.status === `rejected`) {
3209
3930
  const entityReason = entityResult.status === `rejected` ? entityResult.reason : null;
3210
- const streamReason = mainStreamResult.status === `rejected` ? mainStreamResult.reason : errorStreamResult.status === `rejected` ? errorStreamResult.reason : null;
3931
+ const streamReason = mainStreamResult.status === `rejected` ? mainStreamResult.reason : null;
3211
3932
  const isDuplicate = entityReason instanceof EntityAlreadyExistsError;
3212
3933
  const isStreamConflict = !!streamReason && typeof streamReason === `object` && (`status` in streamReason && streamReason.status === 409 || `code` in streamReason && streamReason.code === `CONFLICT_SEQ`);
3213
3934
  const rollbacks = [];
3214
3935
  if (!isDuplicate && !isStreamConflict) {
3215
3936
  if (mainStreamResult.status === `fulfilled`) rollbacks.push(this.streamClient.delete(mainPath));
3216
- if (errorStreamResult.status === `fulfilled`) rollbacks.push(this.streamClient.delete(errorPath));
3217
3937
  if (entityResult.status === `fulfilled`) rollbacks.push(this.registry.deleteEntity(entityURL));
3218
3938
  if (req.wake) rollbacks.push(this.wakeRegistry.unregisterBySubscriberAndSource(req.wake.subscriberUrl, entityURL, this.tenantId));
3219
3939
  await Promise.allSettled(rollbacks);
3220
3940
  }
3221
3941
  if (isDuplicate || isStreamConflict) throw new ElectricAgentsError(ErrCodeDuplicateURL, `Entity already exists at URL "${entityURL}"`, 409);
3222
- const failure = mainStreamResult.status === `rejected` ? mainStreamResult.reason : errorStreamResult.status === `rejected` ? errorStreamResult.reason : entityResult.reason;
3942
+ const failure = mainStreamResult.status === `rejected` ? mainStreamResult.reason : entityResult.reason;
3223
3943
  if (failure instanceof Error) throw failure;
3224
3944
  throw new ElectricAgentsError(`SPAWN_FAILED`, `Spawn failed: ${String(failure)}`, 500);
3225
3945
  }
@@ -3304,7 +4024,7 @@ var EntityManager = class {
3304
4024
  });
3305
4025
  const sharedStateIdMap = await this.buildForkSharedStateIdMap(snapshot.sharedStateIds, suffix);
3306
4026
  const stringMap = this.buildForkStringMap(entityUrlMap, sharedStateIdMap);
3307
- const entityPlans = this.buildForkEntityPlans(effectiveSubtree, entityUrlMap, stringMap);
4027
+ const entityPlans = this.buildForkEntityPlans(effectiveSubtree, entityUrlMap, stringMap, opts.createdBy);
3308
4028
  this.addForkLocks(this.forkWriteLockedEntities, effectiveSubtree.map((entity) => entity.url), writeEntityLocks);
3309
4029
  this.addForkLocks(this.forkWriteLockedStreams, [...snapshot.sharedStateIds].map((id) => getSharedStateStreamPath(id)), writeStreamLocks);
3310
4030
  const createdStreams = [];
@@ -3315,8 +4035,6 @@ var EntityManager = class {
3315
4035
  const isRoot = plan.source.url === rootUrl;
3316
4036
  await this.streamClient.fork(plan.fork.streams.main, plan.source.streams.main, isRoot && opts.forkPointer ? { forkPointer: opts.forkPointer } : void 0);
3317
4037
  createdStreams.push(plan.fork.streams.main);
3318
- await this.streamClient.fork(plan.fork.streams.error, plan.source.streams.error);
3319
- createdStreams.push(plan.fork.streams.error);
3320
4038
  }
3321
4039
  for (const [sourceId, forkId] of sharedStateIdMap) {
3322
4040
  const sourcePath = getSharedStateStreamPath(sourceId);
@@ -3650,7 +4368,6 @@ var EntityManager = class {
3650
4368
  for (const [sourceUrl, forkUrl] of entityUrlMap) {
3651
4369
  stringMap.set(sourceUrl, forkUrl);
3652
4370
  stringMap.set(`${sourceUrl}/main`, `${forkUrl}/main`);
3653
- stringMap.set(`${sourceUrl}/error`, `${forkUrl}/error`);
3654
4371
  }
3655
4372
  for (const [sourceId, forkId] of sharedStateIdMap) {
3656
4373
  stringMap.set(sourceId, forkId);
@@ -3658,7 +4375,7 @@ var EntityManager = class {
3658
4375
  }
3659
4376
  return stringMap;
3660
4377
  }
3661
- buildForkEntityPlans(entitiesToFork, entityUrlMap, stringMap) {
4378
+ buildForkEntityPlans(entitiesToFork, entityUrlMap, stringMap, createdBy) {
3662
4379
  const now = Date.now();
3663
4380
  return entitiesToFork.map((source) => {
3664
4381
  const forkUrl = entityUrlMap.get(source.url);
@@ -3671,14 +4388,12 @@ var EntityManager = class {
3671
4388
  url: forkUrl,
3672
4389
  type,
3673
4390
  status: `idle`,
3674
- streams: {
3675
- main: `${forkUrl}/main`,
3676
- error: `${forkUrl}/error`
3677
- },
4391
+ streams: { main: `${forkUrl}/main` },
3678
4392
  subscription_id: `${type}-handler`,
3679
4393
  write_token: randomUUID(),
3680
4394
  spawn_args: spawnArgs,
3681
4395
  parent,
4396
+ created_by: createdBy ?? source.created_by,
3682
4397
  created_at: now,
3683
4398
  updated_at: now
3684
4399
  };
@@ -3912,7 +4627,7 @@ var EntityManager = class {
3912
4627
  }
3913
4628
  async materializeForkManifestSideEffects(entityUrl, manifests) {
3914
4629
  for (const [manifestKey, manifest] of manifests) {
3915
- await this.syncEntitiesManifestSource(entityUrl, manifestKey, `upsert`, manifest);
4630
+ await this.syncManifestLinks(entityUrl, manifestKey, `upsert`, manifest);
3916
4631
  const wake = buildManifestWakeRegistration(entityUrl, manifest, manifestKey);
3917
4632
  if (wake) await this.wakeRegistry.register({
3918
4633
  ...wake,
@@ -3942,6 +4657,7 @@ var EntityManager = class {
3942
4657
  await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
3943
4658
  entityUrl: targetUrl,
3944
4659
  from: senderUrl,
4660
+ from_agent: senderUrl,
3945
4661
  payload: manifest.payload,
3946
4662
  key: `scheduled-${producerId}`,
3947
4663
  type: typeof manifest.messageType === `string` ? manifest.messageType : void 0,
@@ -3981,12 +4697,14 @@ var EntityManager = class {
3981
4697
  const now = new Date().toISOString();
3982
4698
  const key = req.key ?? `msg-in-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
3983
4699
  const value = {
3984
- from: req.from,
4700
+ from: req.from_principal ?? req.from,
3985
4701
  payload: req.payload,
3986
4702
  timestamp: now,
3987
4703
  mode: req.mode ?? `immediate`,
3988
4704
  status: req.mode === `queued` || req.mode === `paused` ? `pending` : `processed`
3989
4705
  };
4706
+ if (req.from_principal) value.from_principal = req.from_principal;
4707
+ if (req.from_agent) value.from_agent = req.from_agent;
3990
4708
  if (req.type) value.message_type = req.type;
3991
4709
  if (req.position) value.position = req.position;
3992
4710
  else if (value.mode === `queued` || value.mode === `paused`) value.position = createInitialQueuePosition(new Date(now));
@@ -4158,9 +4876,9 @@ var EntityManager = class {
4158
4876
  if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
4159
4877
  return updated;
4160
4878
  }
4161
- async ensureEntitiesMembershipStream(tags) {
4879
+ async ensureEntitiesMembershipStream(tags, principal) {
4162
4880
  if (!this.entityBridgeManager) throw new Error(`Entity bridge manager not configured`);
4163
- return this.entityBridgeManager.register(this.validateTags(tags));
4881
+ return this.entityBridgeManager.register(this.validateTags(tags), principal.url, principal.kind);
4164
4882
  }
4165
4883
  async writeManifestEntry(entityUrl, key, operation, value, opts) {
4166
4884
  const entity = await this.registry.getEntity(entityUrl);
@@ -4178,11 +4896,11 @@ var EntityManager = class {
4178
4896
  const encoded = this.encodeChangeEvent(event);
4179
4897
  if (opts?.producerId) {
4180
4898
  await this.streamClient.appendIdempotent(entity.streams.main, encoded, { producerId: opts.producerId });
4181
- await this.syncEntitiesManifestSource(entityUrl, key, operation, value);
4899
+ await this.syncManifestLinks(entityUrl, key, operation, value);
4182
4900
  return;
4183
4901
  }
4184
4902
  await this.streamClient.append(entity.streams.main, encoded);
4185
- await this.syncEntitiesManifestSource(entityUrl, key, operation, value);
4903
+ await this.syncManifestLinks(entityUrl, key, operation, value);
4186
4904
  }
4187
4905
  async upsertCronSchedule(entityUrl, req) {
4188
4906
  if (req.payload === void 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: payload`, 400);
@@ -4331,6 +5049,8 @@ var EntityManager = class {
4331
5049
  await this.scheduler.enqueueDelayedSend({
4332
5050
  entityUrl,
4333
5051
  from: req.from,
5052
+ from_principal: req.from_principal,
5053
+ from_agent: req.from_agent,
4334
5054
  payload: req.payload,
4335
5055
  key: req.key,
4336
5056
  type: req.type,
@@ -4373,14 +5093,23 @@ var EntityManager = class {
4373
5093
  await this.streamClient.appendIdempotent(subscriber.streams.main, this.encodeChangeEvent(wakeEvent), { producerId: `wake-reg-${result.registrationDbId}-${result.sourceEventKey}` });
4374
5094
  });
4375
5095
  }
4376
- async syncEntitiesManifestSource(entityUrl, manifestKey, operation, value) {
5096
+ async syncManifestLinks(entityUrl, manifestKey, operation, value) {
4377
5097
  const sourceRef = operation === `delete` ? void 0 : this.extractEntitiesSourceRef(value);
4378
5098
  await this.registry.replaceEntityManifestSource(entityUrl, manifestKey, sourceRef);
5099
+ const sharedStateId = operation === `delete` ? void 0 : this.extractSharedStateId(value);
5100
+ await this.registry.replaceSharedStateLink(entityUrl, manifestKey, sharedStateId);
4379
5101
  }
4380
5102
  extractEntitiesSourceRef(manifest) {
4381
5103
  if (manifest?.kind === `source` && manifest.sourceType === `entities` && typeof manifest.sourceRef === `string`) return manifest.sourceRef;
4382
5104
  return void 0;
4383
5105
  }
5106
+ extractSharedStateId(manifest) {
5107
+ if (manifest?.kind === `shared-state` && typeof manifest.id === `string`) return manifest.id;
5108
+ if (manifest?.kind !== `source` || manifest.sourceType !== `db`) return void 0;
5109
+ if (typeof manifest.sourceRef === `string`) return manifest.sourceRef;
5110
+ const config = isRecord(manifest.config) ? manifest.config : void 0;
5111
+ return typeof config?.id === `string` ? config.id : void 0;
5112
+ }
4384
5113
  /**
4385
5114
  * Read a child entity's stream and extract concatenated text deltas
4386
5115
  * for a specific run, plus any error messages for that run.
@@ -4544,14 +5273,7 @@ var EntityManager = class {
4544
5273
  await this.streamClient.append(entity.streams.main, signalData);
4545
5274
  return;
4546
5275
  }
4547
- const errorCloseEvent = {
4548
- type: `signal`,
4549
- key: signalEvent.key,
4550
- value: signalEvent.value,
4551
- headers: signalEvent.headers
4552
- };
4553
- const errorSignalData = this.encodeChangeEvent(errorCloseEvent);
4554
- for (const [streamPath, data] of [[entity.streams.main, signalData], [entity.streams.error, errorSignalData]]) try {
5276
+ for (const [streamPath, data] of [[entity.streams.main, signalData]]) try {
4555
5277
  await this.streamClient.append(streamPath, data, { close: true });
4556
5278
  } catch (err) {
4557
5279
  const message = err instanceof Error ? err.message : String(err);
@@ -4617,7 +5339,9 @@ var EntityManager = class {
4617
5339
  creation_schema: existing.creation_schema,
4618
5340
  inbox_schemas: mergedInbox,
4619
5341
  state_schemas: mergedState,
5342
+ slash_commands: existing.slash_commands,
4620
5343
  serve_endpoint: existing.serve_endpoint,
5344
+ default_dispatch_policy: existing.default_dispatch_policy,
4621
5345
  revision: nextRevision,
4622
5346
  created_at: existing.created_at,
4623
5347
  updated_at: now
@@ -4671,11 +5395,19 @@ var EntityManager = class {
4671
5395
  throw new ElectricAgentsError(ErrCodeInvalidRequest, error instanceof Error ? error.message : `Invalid tags`, 400);
4672
5396
  }
4673
5397
  }
5398
+ validateSlashCommands(input) {
5399
+ const validationError = validateSlashCommandDefinitions(input);
5400
+ if (!validationError) return;
5401
+ throw new ElectricAgentsError(ErrCodeSchemaValidationFailed, validationError.message, 422, validationError.details);
5402
+ }
4674
5403
  async validateSendRequest(entityUrl, req) {
4675
5404
  const entity = await this.registry.getEntity(entityUrl);
4676
5405
  if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
4677
5406
  if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
4678
- if (req.type && entity.type) {
5407
+ if (req.type === COMPOSER_INPUT_MESSAGE_TYPE) {
5408
+ const valErr = validateComposerInputPayload(req.payload);
5409
+ if (valErr) throw new ElectricAgentsError(ErrCodeSchemaValidationFailed, valErr.message, 422, valErr.details);
5410
+ } else if (req.type && entity.type) {
4679
5411
  const { inboxSchemas } = await this.getEffectiveSchemas(entity);
4680
5412
  if (inboxSchemas) {
4681
5413
  const schema = inboxSchemas[req.type];
@@ -4927,6 +5659,27 @@ const wakeConditionSchema = Type.Union([Type.Literal(`runFinished`), Type.Object
4927
5659
  Type.Literal(`delete`)
4928
5660
  ])))
4929
5661
  })]);
5662
+ const permissionSubjectSchema = Type.Object({
5663
+ subject_kind: Type.Union([Type.Literal(`principal`), Type.Literal(`principal_kind`)]),
5664
+ subject_value: Type.String()
5665
+ }, { additionalProperties: false });
5666
+ const entityPermissionSchema = Type.Union([
5667
+ Type.Literal(`read`),
5668
+ Type.Literal(`write`),
5669
+ Type.Literal(`delete`),
5670
+ Type.Literal(`signal`),
5671
+ Type.Literal(`fork`),
5672
+ Type.Literal(`schedule`),
5673
+ Type.Literal(`spawn`),
5674
+ Type.Literal(`manage`)
5675
+ ]);
5676
+ const entityPermissionGrantInputSchema = Type.Object({
5677
+ ...permissionSubjectSchema.properties,
5678
+ permission: entityPermissionSchema,
5679
+ propagation: Type.Optional(Type.Union([Type.Literal(`self`), Type.Literal(`descendants`)])),
5680
+ copy_to_children: Type.Optional(Type.Boolean()),
5681
+ expires_at: Type.Optional(Type.String())
5682
+ }, { additionalProperties: false });
4930
5683
  const spawnBodySchema = Type.Object({
4931
5684
  args: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
4932
5685
  tags: Type.Optional(stringRecordSchema$1),
@@ -4934,6 +5687,8 @@ const spawnBodySchema = Type.Object({
4934
5687
  dispatch_policy: Type.Optional(dispatchPolicySchema),
4935
5688
  sandbox: Type.Optional(sandboxChoiceSchema),
4936
5689
  initialMessage: Type.Optional(Type.Unknown()),
5690
+ grants: Type.Optional(Type.Array(entityPermissionGrantInputSchema)),
5691
+ initialMessageType: Type.Optional(Type.String()),
4937
5692
  wake: Type.Optional(Type.Object({
4938
5693
  subscriberUrl: Type.String(),
4939
5694
  condition: wakeConditionSchema,
@@ -4955,8 +5710,22 @@ const sendBodySchema = Type.Object({
4955
5710
  ])),
4956
5711
  position: Type.Optional(Type.String()),
4957
5712
  afterMs: Type.Optional(Type.Number()),
4958
- from: Type.Optional(Type.String())
5713
+ from: Type.Optional(Type.String()),
5714
+ from_principal: Type.Optional(Type.String()),
5715
+ from_agent: Type.Optional(Type.String())
4959
5716
  });
5717
+ function agentUrlForPrincipal(principal) {
5718
+ if (principal.kind === `agent`) return `/${principal.id}`;
5719
+ if (principal.key.startsWith(`entity:`)) return `/${principal.key.slice(`entity:`.length)}`;
5720
+ return null;
5721
+ }
5722
+ function agentUrlPath(value) {
5723
+ try {
5724
+ return new URL(value).pathname;
5725
+ } catch {
5726
+ return value;
5727
+ }
5728
+ }
4960
5729
  const inboxMessageBodySchema = Type.Object({
4961
5730
  payload: Type.Optional(Type.Unknown()),
4962
5731
  position: Type.Optional(Type.String()),
@@ -5035,24 +5804,27 @@ const attachmentSubjectTypes = new Set([
5035
5804
  ]);
5036
5805
  const entitiesRouter = Router({ base: `/_electric/entities` });
5037
5806
  entitiesRouter.get(`/`, listEntities);
5038
- entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spawnBodySchema), spawnEntity);
5039
- entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, getEntity);
5040
- entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity);
5041
- entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, killEntity);
5042
- entitiesRouter.post(`/:type/:instanceId/signal`, withExistingEntity, withSchema(signalBodySchema), signalEntity);
5043
- entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), sendEntity);
5044
- entitiesRouter.post(`/:type/:instanceId/attachments`, withExistingEntity, createAttachment);
5045
- entitiesRouter.get(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, readAttachment);
5046
- entitiesRouter.delete(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, deleteAttachment);
5047
- entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), updateInboxMessage);
5048
- entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, deleteInboxMessage);
5049
- entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), forkEntity);
5050
- entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), setTag);
5051
- entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, deleteTag);
5052
- entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), upsertSchedule);
5053
- entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, deleteSchedule);
5054
- entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), upsertEventSourceSubscription);
5055
- entitiesRouter.delete(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, deleteEventSourceSubscription);
5807
+ entitiesRouter.put(`/:type/:instanceId`, withSpawnableEntityType, withSchema(spawnBodySchema), withSpawnPermission, spawnEntity);
5808
+ entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`read`), getEntity);
5809
+ entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`read`), headEntity);
5810
+ entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`delete`), killEntity);
5811
+ entitiesRouter.post(`/:type/:instanceId/signal`, withExistingEntity, withSchema(signalBodySchema), withEntityPermission(`signal`), signalEntity);
5812
+ entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), withEntityPermission(`write`), sendEntity);
5813
+ entitiesRouter.post(`/:type/:instanceId/attachments`, withExistingEntity, withEntityPermission(`write`), createAttachment);
5814
+ entitiesRouter.get(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, withEntityPermission(`read`), readAttachment);
5815
+ entitiesRouter.delete(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, withEntityPermission(`write`), deleteAttachment);
5816
+ entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), withEntityPermission(`write`), updateInboxMessage);
5817
+ entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withEntityPermission(`write`), deleteInboxMessage);
5818
+ entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), withEntityPermission(`fork`), forkEntity);
5819
+ entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), withEntityPermission(`write`), setTag);
5820
+ entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withEntityPermission(`write`), deleteTag);
5821
+ entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), withEntityPermission(`schedule`), upsertSchedule);
5822
+ entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withEntityPermission(`schedule`), deleteSchedule);
5823
+ entitiesRouter.put(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(eventSourceSubscriptionBodySchema), withEntityPermission(`write`), upsertEventSourceSubscription);
5824
+ entitiesRouter.delete(`/:type/:instanceId/event-source-subscriptions/:subscriptionId`, withExistingEntity, withEntityPermission(`write`), deleteEventSourceSubscription);
5825
+ entitiesRouter.get(`/:type/:instanceId/grants`, withExistingEntity, withEntityPermission(`manage`), listEntityPermissionGrants);
5826
+ entitiesRouter.post(`/:type/:instanceId/grants`, withExistingEntity, withSchema(entityPermissionGrantInputSchema), withEntityPermission(`manage`), createEntityPermissionGrant);
5827
+ entitiesRouter.delete(`/:type/:instanceId/grants/:grantId`, withExistingEntity, withEntityPermission(`manage`), deleteEntityPermissionGrant);
5056
5828
  function entityUrlFromSegments(type, instanceId) {
5057
5829
  if (!type || !instanceId) return null;
5058
5830
  if (type.startsWith(`_`) || type.includes(`*`) || instanceId.includes(`*`)) return null;
@@ -5151,6 +5923,17 @@ function rejectPrincipalEntityMutation(request, action) {
5151
5923
  if (entity.type !== `principal`) return void 0;
5152
5924
  return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be ${action}`);
5153
5925
  }
5926
+ function parseExpiresAt$1(value) {
5927
+ if (value === void 0) return void 0;
5928
+ const expiresAt = new Date(value);
5929
+ if (Number.isNaN(expiresAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid expires_at timestamp`, 400);
5930
+ return expiresAt;
5931
+ }
5932
+ function parseGrantId$1(request) {
5933
+ const grantId = Number.parseInt(String(request.params.grantId), 10);
5934
+ if (!Number.isSafeInteger(grantId) || grantId <= 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid grant id`, 400);
5935
+ return grantId;
5936
+ }
5154
5937
  async function withExistingEntity(request, ctx) {
5155
5938
  const entityUrl = entityUrlFromSegments(request.params.type, request.params.instanceId);
5156
5939
  if (!entityUrl) return void 0;
@@ -5181,17 +5964,76 @@ async function withSpawnableEntityType(request, ctx) {
5181
5964
  if (request.params.type === `principal`) return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be spawned directly`);
5182
5965
  const entityType = await ctx.entityManager.registry.getEntityType(request.params.type);
5183
5966
  if (!entityType) return apiError(404, ErrCodeUnknownEntityType, `Entity type "${request.params.type}" not found`);
5967
+ request.spawnRoute = { entityType };
5184
5968
  return void 0;
5185
5969
  }
5970
+ function withEntityPermission(permission) {
5971
+ return async (request, ctx) => {
5972
+ const { entity } = requireExistingEntityRoute(request);
5973
+ if (await canAccessEntity(ctx, entity, permission, request)) return void 0;
5974
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to ${permission} ${entity.url}`);
5975
+ };
5976
+ }
5977
+ async function withSpawnPermission(request, ctx) {
5978
+ const parsed = routeBody(request);
5979
+ const entityType = request.spawnRoute?.entityType;
5980
+ if (!entityType) throw new Error(`spawnable entity type middleware did not run`);
5981
+ if (!await canAccessEntityType(ctx, entityType, `spawn`, request)) return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn ${entityType.name}`);
5982
+ if (!parsed.parent) return void 0;
5983
+ const parent = await ctx.entityManager.registry.getEntity(parsed.parent);
5984
+ if (!parent) return apiError(404, ErrCodeNotFound, `Parent entity not found`);
5985
+ if (await canAccessEntity(ctx, parent, `spawn`, request)) return await validateParentedSpawnGrants(request, ctx, parent, parsed);
5986
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn children from ${parent.url}`);
5987
+ }
5988
+ async function validateParentedSpawnGrants(request, ctx, parent, parsed) {
5989
+ const needsParentManage = (parsed.grants ?? []).some(requiresParentManageForInitialGrant);
5990
+ if (!needsParentManage) return void 0;
5991
+ if (await canAccessEntity(ctx, parent, `manage`, request)) return void 0;
5992
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to delegate broad grants from ${parent.url}`);
5993
+ }
5994
+ function requiresParentManageForInitialGrant(grant) {
5995
+ return grant.permission === `manage` || grant.subject_kind === `principal_kind` || grant.propagation === `descendants` || grant.copy_to_children === true;
5996
+ }
5186
5997
  async function listEntities({ query }, ctx) {
5187
5998
  const { entities: entities$1 } = await ctx.entityManager.registry.listEntities({
5188
5999
  type: firstQueryValue$1(query.type),
5189
6000
  status: firstQueryValue$1(query.status),
5190
6001
  parent: firstQueryValue$1(query.parent),
5191
- created_by: firstQueryValue$1(query.created_by)
6002
+ created_by: firstQueryValue$1(query.created_by),
6003
+ readableBy: {
6004
+ ...principalSubject(ctx.principal),
6005
+ bypass: isPermissionBypassPrincipal(ctx)
6006
+ }
5192
6007
  });
5193
6008
  return json(entities$1.map((entity) => toPublicEntity(entity)));
5194
6009
  }
6010
+ async function listEntityPermissionGrants(request, ctx) {
6011
+ const { entityUrl } = requireExistingEntityRoute(request);
6012
+ const grants = await ctx.entityManager.registry.listEntityPermissionGrants(entityUrl);
6013
+ return json({ grants });
6014
+ }
6015
+ async function createEntityPermissionGrant(request, ctx) {
6016
+ const { entityUrl } = requireExistingEntityRoute(request);
6017
+ const parsed = routeBody(request);
6018
+ const grant = await ctx.entityManager.registry.createEntityPermissionGrant({
6019
+ entityUrl,
6020
+ permission: parsed.permission,
6021
+ subjectKind: parsed.subject_kind,
6022
+ subjectValue: parsed.subject_value,
6023
+ propagation: parsed.propagation,
6024
+ copyToChildren: parsed.copy_to_children,
6025
+ expiresAt: parseExpiresAt$1(parsed.expires_at),
6026
+ createdBy: ctx.principal.url
6027
+ });
6028
+ await ctx.entityBridgeManager.onEntityChanged(entityUrl);
6029
+ return json(grant, { status: 201 });
6030
+ }
6031
+ async function deleteEntityPermissionGrant(request, ctx) {
6032
+ const { entityUrl } = requireExistingEntityRoute(request);
6033
+ const deleted = await ctx.entityManager.registry.deleteEntityPermissionGrant(entityUrl, parseGrantId$1(request));
6034
+ if (deleted) await ctx.entityBridgeManager.onEntityChanged(entityUrl);
6035
+ return deleted ? status(204) : apiError(404, ErrCodeNotFound, `Grant not found`);
6036
+ }
5195
6037
  async function upsertSchedule(request, ctx) {
5196
6038
  const principalMutationError = rejectPrincipalEntityMutation(request, `scheduled`);
5197
6039
  if (principalMutationError) return principalMutationError;
@@ -5297,6 +6139,7 @@ async function forkEntity(request, ctx) {
5297
6139
  const result = await ctx.entityManager.forkSubtree(entityUrl, {
5298
6140
  rootInstanceId: parsed.instance_id,
5299
6141
  waitTimeoutMs: parsed.waitTimeoutMs,
6142
+ createdBy: ctx.principal.url,
5300
6143
  ...parsed.fork_pointer && { forkPointer: {
5301
6144
  offset: parsed.fork_pointer.offset,
5302
6145
  subOffset: parsed.fork_pointer.sub_offset
@@ -5312,26 +6155,27 @@ async function sendEntity(request, ctx) {
5312
6155
  const parsed = routeBody(request);
5313
6156
  const principal = ctx.principal;
5314
6157
  if (parsed.from !== void 0 && parsed.from !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from must match Electric-Principal`);
6158
+ if (parsed.from_principal !== void 0 && parsed.from_principal !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from_principal must match Electric-Principal`);
6159
+ if (parsed.from_agent !== void 0) {
6160
+ const principalAgentUrl = agentUrlForPrincipal(principal);
6161
+ if (agentUrlPath(parsed.from_agent) !== principalAgentUrl) return apiError(400, ErrCodeInvalidRequest, `Request from_agent must match authenticated agent principal`);
6162
+ }
5315
6163
  await ctx.entityManager.ensurePrincipal(principal);
5316
6164
  const { entityUrl, entity } = requireExistingEntityRoute(request);
5317
6165
  const dispatchEntity = entity.dispatch_policy ? entity : await backfillEntityDispatchPolicy(ctx, entity);
5318
6166
  await linkEntityDispatchSubscription(ctx, dispatchEntity);
5319
- if (parsed.afterMs && parsed.afterMs > 0) await ctx.entityManager.enqueueDelayedSend(entityUrl, {
5320
- from: principal.url,
5321
- payload: parsed.payload,
5322
- key: parsed.key,
5323
- type: parsed.type,
5324
- mode: parsed.mode,
5325
- position: parsed.position
5326
- }, new Date(Date.now() + parsed.afterMs));
5327
- else await ctx.entityManager.send(entityUrl, {
6167
+ const sendReq = {
5328
6168
  from: principal.url,
6169
+ from_principal: principal.url,
6170
+ from_agent: parsed.from_agent,
5329
6171
  payload: parsed.payload,
5330
6172
  key: parsed.key,
5331
6173
  type: parsed.type,
5332
6174
  mode: parsed.mode,
5333
6175
  position: parsed.position
5334
- });
6176
+ };
6177
+ if (parsed.afterMs && parsed.afterMs > 0) await ctx.entityManager.enqueueDelayedSend(entityUrl, sendReq, new Date(Date.now() + parsed.afterMs));
6178
+ else await ctx.entityManager.send(entityUrl, sendReq);
5335
6179
  return status(204);
5336
6180
  }
5337
6181
  async function createAttachment(request, ctx) {
@@ -5400,14 +6244,27 @@ async function spawnEntity(request, ctx) {
5400
6244
  dispatch_policy: dispatchPolicy,
5401
6245
  sandbox: parsed.sandbox,
5402
6246
  initialMessage: void 0,
6247
+ initialMessageType: void 0,
5403
6248
  wake: parsed.wake,
5404
6249
  created_by: principal.url
5405
6250
  });
6251
+ if (parsed.parent) await ctx.entityManager.registry.copyEntityPermissionGrantsForSpawn(parsed.parent, entity.url, principal.url);
6252
+ for (const grant of parsed.grants ?? []) await ctx.entityManager.registry.createEntityPermissionGrant({
6253
+ entityUrl: entity.url,
6254
+ permission: grant.permission,
6255
+ subjectKind: grant.subject_kind,
6256
+ subjectValue: grant.subject_value,
6257
+ propagation: grant.propagation,
6258
+ copyToChildren: grant.copy_to_children,
6259
+ expiresAt: parseExpiresAt$1(grant.expires_at),
6260
+ createdBy: principal.url
6261
+ });
5406
6262
  const linkBeforeInitialMessage = parsed.initialMessage !== void 0 && shouldLinkDispatchBeforeInitialMessage(dispatchPolicy);
5407
6263
  if (linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
5408
6264
  if (parsed.initialMessage !== void 0) await ctx.entityManager.send(entity.url, {
5409
6265
  from: principal.url,
5410
- payload: parsed.initialMessage
6266
+ payload: parsed.initialMessage,
6267
+ type: parsed.initialMessageType
5411
6268
  });
5412
6269
  if (!linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
5413
6270
  return json({
@@ -5454,14 +6311,37 @@ async function signalEntity(request, ctx) {
5454
6311
  //#region src/routing/entity-types-router.ts
5455
6312
  const jsonObjectSchema = Type.Record(Type.String(), Type.Unknown());
5456
6313
  const schemaMapSchema = Type.Record(Type.String(), jsonObjectSchema);
6314
+ const slashCommandArgumentSchema = Type.Object({
6315
+ name: Type.String(),
6316
+ type: Type.Union([
6317
+ Type.Literal(`string`),
6318
+ Type.Literal(`number`),
6319
+ Type.Literal(`boolean`)
6320
+ ]),
6321
+ required: Type.Optional(Type.Boolean()),
6322
+ description: Type.Optional(Type.String())
6323
+ }, { additionalProperties: false });
6324
+ const slashCommandSchema = Type.Object({
6325
+ name: Type.String(),
6326
+ description: Type.Optional(Type.String()),
6327
+ arguments: Type.Optional(Type.Array(slashCommandArgumentSchema))
6328
+ }, { additionalProperties: false });
6329
+ const typePermissionGrantInputSchema = Type.Object({
6330
+ subject_kind: Type.Union([Type.Literal(`principal`), Type.Literal(`principal_kind`)]),
6331
+ subject_value: Type.String(),
6332
+ permission: Type.Union([Type.Literal(`spawn`), Type.Literal(`manage`)]),
6333
+ expires_at: Type.Optional(Type.String())
6334
+ }, { additionalProperties: false });
5457
6335
  const registerEntityTypeBodySchema = Type.Object({
5458
6336
  name: Type.Optional(Type.String()),
5459
6337
  description: Type.Optional(Type.String()),
5460
6338
  creation_schema: Type.Optional(jsonObjectSchema),
5461
6339
  inbox_schemas: Type.Optional(schemaMapSchema),
5462
6340
  state_schemas: Type.Optional(schemaMapSchema),
6341
+ slash_commands: Type.Optional(Type.Array(slashCommandSchema)),
5463
6342
  serve_endpoint: Type.Optional(Type.String()),
5464
- default_dispatch_policy: Type.Optional(dispatchPolicySchema)
6343
+ default_dispatch_policy: Type.Optional(dispatchPolicySchema),
6344
+ permission_grants: Type.Optional(Type.Array(typePermissionGrantInputSchema))
5465
6345
  }, { additionalProperties: false });
5466
6346
  const amendEntityTypeSchemasBodySchema = Type.Object({
5467
6347
  inbox_schemas: Type.Optional(schemaMapSchema),
@@ -5469,20 +6349,56 @@ const amendEntityTypeSchemasBodySchema = Type.Object({
5469
6349
  }, { additionalProperties: false });
5470
6350
  const entityTypesRouter = Router({ base: `/_electric/entity-types` });
5471
6351
  entityTypesRouter.get(`/`, listEntityTypes);
5472
- entityTypesRouter.post(`/`, withSchema(registerEntityTypeBodySchema), registerEntityType);
5473
- entityTypesRouter.patch(`/:name/schemas`, withSchema(amendEntityTypeSchemasBodySchema), amendSchemas);
5474
- entityTypesRouter.get(`/:name`, getEntityType);
5475
- entityTypesRouter.delete(`/:name`, deleteEntityType);
6352
+ entityTypesRouter.post(`/`, withSchema(registerEntityTypeBodySchema), withEntityTypeRegistrationPermission, registerEntityType);
6353
+ entityTypesRouter.patch(`/:name/schemas`, withExistingEntityType, withEntityTypeManagePermission, withSchema(amendEntityTypeSchemasBodySchema), amendSchemas);
6354
+ entityTypesRouter.get(`/:name`, withExistingEntityType, withEntityTypeSpawnPermission, getEntityType);
6355
+ entityTypesRouter.delete(`/:name`, withExistingEntityType, withEntityTypeManagePermission, deleteEntityType);
6356
+ entityTypesRouter.get(`/:name/grants`, withExistingEntityType, withEntityTypeManagePermission, listTypePermissionGrants);
6357
+ entityTypesRouter.post(`/:name/grants`, withExistingEntityType, withSchema(typePermissionGrantInputSchema), withEntityTypeManagePermission, createTypePermissionGrant);
6358
+ entityTypesRouter.delete(`/:name/grants/:grantId`, withExistingEntityType, withEntityTypeManagePermission, deleteTypePermissionGrant);
5476
6359
  async function registerEntityType(request, ctx) {
5477
6360
  const parsed = routeBody(request);
5478
6361
  const normalized = normalizeEntityTypeRequest(parsed);
5479
6362
  if (normalized.serve_endpoint && !normalized.description && !normalized.creation_schema) return await discoverServeEndpoint(ctx, normalized);
5480
6363
  const entityType = await ctx.entityManager.registerEntityType(normalized);
6364
+ await applyRegistrationPermissionGrants(ctx, entityType.name, normalized);
5481
6365
  return json(toPublicEntityType(entityType), { status: 201 });
5482
6366
  }
5483
6367
  async function listEntityTypes(_request, ctx) {
5484
6368
  const entityTypes$1 = await ctx.entityManager.registry.listEntityTypes();
5485
- return json(entityTypes$1.map((entityType) => toPublicEntityType(entityType)));
6369
+ const visible = [];
6370
+ for (const entityType of entityTypes$1) if (await canAccessEntityType(ctx, entityType, `spawn`)) visible.push(entityType);
6371
+ return json(visible.map((entityType) => toPublicEntityType(entityType)));
6372
+ }
6373
+ async function withExistingEntityType(request, ctx) {
6374
+ const entityType = await ctx.entityManager.registry.getEntityType(request.params.name);
6375
+ if (!entityType) return apiError(404, ErrCodeNotFound, `Entity type not found`);
6376
+ request.entityTypeRoute = { entityType };
6377
+ return void 0;
6378
+ }
6379
+ async function withEntityTypeManagePermission(request, ctx) {
6380
+ const entityType = request.entityTypeRoute?.entityType;
6381
+ if (!entityType) throw new Error(`entity type middleware did not run`);
6382
+ if (await canAccessEntityType(ctx, entityType, `manage`, request)) return void 0;
6383
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to manage ${entityType.name}`);
6384
+ }
6385
+ async function withEntityTypeSpawnPermission(request, ctx) {
6386
+ const entityType = request.entityTypeRoute?.entityType;
6387
+ if (!entityType) throw new Error(`entity type middleware did not run`);
6388
+ if (await canAccessEntityType(ctx, entityType, `spawn`, request)) return void 0;
6389
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn ${entityType.name}`);
6390
+ }
6391
+ async function withEntityTypeRegistrationPermission(request, ctx) {
6392
+ const parsed = normalizeEntityTypeRequest(routeBody(request));
6393
+ if (!parsed.name) return void 0;
6394
+ const existing = await ctx.entityManager.registry.getEntityType(parsed.name);
6395
+ if (existing) {
6396
+ request.entityTypeRoute = { entityType: existing };
6397
+ if (await canAccessEntityType(ctx, existing, `manage`, request)) return void 0;
6398
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to manage ${existing.name}`);
6399
+ }
6400
+ if (await canRegisterEntityType(ctx, parsed, request)) return void 0;
6401
+ return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to register entity types`);
5486
6402
  }
5487
6403
  async function discoverServeEndpoint(ctx, parsed) {
5488
6404
  try {
@@ -5491,17 +6407,17 @@ async function discoverServeEndpoint(ctx, parsed) {
5491
6407
  const manifest = await response.json();
5492
6408
  if (manifest.name !== parsed.name) return apiError(400, ErrCodeServeEndpointNameMismatch, `Serve endpoint returned name "${manifest.name}" but expected "${parsed.name}"`);
5493
6409
  manifest.serve_endpoint = parsed.serve_endpoint;
6410
+ manifest.permission_grants = parsed.permission_grants;
5494
6411
  const entityType = await ctx.entityManager.registerEntityType(normalizeEntityTypeRequest(manifest));
6412
+ await applyRegistrationPermissionGrants(ctx, entityType.name, manifest);
5495
6413
  return json(toPublicEntityType(entityType), { status: 201 });
5496
6414
  } catch (err) {
5497
6415
  if (err instanceof ElectricAgentsError) throw err;
5498
6416
  return apiError(502, ErrCodeServeEndpointUnreachable, `Failed to reach serve endpoint: ${err instanceof Error ? err.message : String(err)}`);
5499
6417
  }
5500
6418
  }
5501
- async function getEntityType(request, ctx) {
5502
- const entityType = await ctx.entityManager.registry.getEntityType(request.params.name);
5503
- if (!entityType) return apiError(404, ErrCodeNotFound, `Entity type not found`);
5504
- return json(toPublicEntityType(entityType));
6419
+ async function getEntityType(request) {
6420
+ return json(toPublicEntityType(request.entityTypeRoute.entityType));
5505
6421
  }
5506
6422
  async function amendSchemas(request, ctx) {
5507
6423
  const parsed = routeBody(request);
@@ -5515,6 +6431,47 @@ async function deleteEntityType(request, ctx) {
5515
6431
  await ctx.entityManager.deleteEntityType(request.params.name);
5516
6432
  return status(204);
5517
6433
  }
6434
+ async function listTypePermissionGrants(request, ctx) {
6435
+ const grants = await ctx.entityManager.registry.listEntityTypePermissionGrants(request.entityTypeRoute.entityType.name);
6436
+ return json({ grants });
6437
+ }
6438
+ async function createTypePermissionGrant(request, ctx) {
6439
+ const parsed = routeBody(request);
6440
+ const grant = await ctx.entityManager.registry.createEntityTypePermissionGrant({
6441
+ entityType: request.entityTypeRoute.entityType.name,
6442
+ permission: parsed.permission,
6443
+ subjectKind: parsed.subject_kind,
6444
+ subjectValue: parsed.subject_value,
6445
+ expiresAt: parseExpiresAt(parsed.expires_at),
6446
+ createdBy: ctx.principal.url
6447
+ });
6448
+ return json(grant, { status: 201 });
6449
+ }
6450
+ async function deleteTypePermissionGrant(request, ctx) {
6451
+ const deleted = await ctx.entityManager.registry.deleteEntityTypePermissionGrant(request.entityTypeRoute.entityType.name, parseGrantId(request));
6452
+ return deleted ? status(204) : apiError(404, ErrCodeNotFound, `Grant not found`);
6453
+ }
6454
+ async function applyRegistrationPermissionGrants(ctx, entityType, request) {
6455
+ for (const grant of request.permission_grants ?? []) await ctx.entityManager.registry.ensureEntityTypePermissionGrant({
6456
+ entityType,
6457
+ permission: grant.permission,
6458
+ subjectKind: grant.subject_kind,
6459
+ subjectValue: grant.subject_value,
6460
+ expiresAt: parseExpiresAt(grant.expires_at),
6461
+ createdBy: ctx.principal.url
6462
+ });
6463
+ }
6464
+ function parseGrantId(request) {
6465
+ const grantId = Number.parseInt(String(request.params.grantId), 10);
6466
+ if (!Number.isSafeInteger(grantId) || grantId <= 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid grant id`, 400);
6467
+ return grantId;
6468
+ }
6469
+ function parseExpiresAt(value) {
6470
+ if (value === void 0) return void 0;
6471
+ const expiresAt = new Date(value);
6472
+ if (Number.isNaN(expiresAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid expires_at timestamp`, 400);
6473
+ return expiresAt;
6474
+ }
5518
6475
  function normalizeEntityTypeRequest(parsed) {
5519
6476
  const serveEndpoint = rewriteLoopbackWebhookUrl(parsed.serve_endpoint);
5520
6477
  return {
@@ -5523,11 +6480,13 @@ function normalizeEntityTypeRequest(parsed) {
5523
6480
  creation_schema: parsed.creation_schema,
5524
6481
  inbox_schemas: parsed.inbox_schemas,
5525
6482
  state_schemas: parsed.state_schemas,
6483
+ slash_commands: parsed.slash_commands,
5526
6484
  serve_endpoint: serveEndpoint,
5527
6485
  default_dispatch_policy: parsed.default_dispatch_policy ?? (serveEndpoint ? { targets: [{
5528
6486
  type: `webhook`,
5529
6487
  url: serveEndpoint
5530
- }] } : void 0)
6488
+ }] } : void 0),
6489
+ permission_grants: parsed.permission_grants
5531
6490
  };
5532
6491
  }
5533
6492
  function toPublicEntityType(entityType) {
@@ -5586,6 +6545,7 @@ function applyCors(response) {
5586
6545
  `content-type`,
5587
6546
  `authorization`,
5588
6547
  `electric-claim-token`,
6548
+ `electric-owner-entity`,
5589
6549
  ELECTRIC_PRINCIPAL_HEADER,
5590
6550
  `ngrok-skip-browser-warning`
5591
6551
  ].join(`, `));
@@ -5636,7 +6596,7 @@ observationsRouter.post(`/entities/ensure-stream`, withSchema(ensureEntitiesMemb
5636
6596
  observationsRouter.post(`/cron/ensure-stream`, withSchema(ensureCronStreamBodySchema), ensureCronStream);
5637
6597
  async function ensureEntitiesMembershipStream(request, ctx) {
5638
6598
  const parsed = routeBody(request);
5639
- const result = await ctx.entityManager.ensureEntitiesMembershipStream(parsed.tags ?? {});
6599
+ const result = await ctx.entityManager.ensureEntitiesMembershipStream(parsed.tags ?? {}, ctx.principal);
5640
6600
  return json(result);
5641
6601
  }
5642
6602
  async function ensureCronStream(request, ctx) {
@@ -6515,16 +7475,31 @@ function buildTagsWhereClause(tags) {
6515
7475
  function sqlStringLiteral$1(value) {
6516
7476
  return `'${value.replace(/'/g, `''`)}'`;
6517
7477
  }
6518
- function buildTenantTagsWhereClause(tenantId, tags) {
6519
- return `tenant_id = ${sqlStringLiteral$1(tenantId)} AND (${buildTagsWhereClause(tags)})`;
7478
+ function buildTenantTagsWhereClause(tenantId, tags, principalUrl$1, principalKind, permissionBypass) {
7479
+ const readableWhere = principalUrl$1 && principalKind ? buildReadableEntitiesWhere({
7480
+ tenantId,
7481
+ principalUrl: principalUrl$1,
7482
+ principalKind,
7483
+ permissionBypass
7484
+ }) : `tenant_id = ${sqlStringLiteral$1(tenantId)} AND FALSE`;
7485
+ return `${readableWhere} AND (${buildTagsWhereClause(tags)})`;
6520
7486
  }
6521
7487
  function shapeEntityKey(message) {
6522
7488
  return message.value.url;
6523
7489
  }
7490
+ function principalScopedSourceRef(tagSourceRef, principalUrl$1, principalKind) {
7491
+ return `${tagSourceRef}-${hashString(JSON.stringify({
7492
+ principalKind,
7493
+ principalUrl: principalUrl$1
7494
+ }))}`;
7495
+ }
6524
7496
  var EntityBridge = class {
6525
7497
  sourceRef;
6526
7498
  tags;
6527
7499
  streamUrl;
7500
+ principalUrl;
7501
+ principalKind;
7502
+ permissionBypass;
6528
7503
  currentMembers = new Map();
6529
7504
  producer = null;
6530
7505
  liveAbortController = null;
@@ -6541,6 +7516,9 @@ var EntityBridge = class {
6541
7516
  this.sourceRef = row.sourceRef;
6542
7517
  this.tags = normalizeTags(row.tags);
6543
7518
  this.streamUrl = row.streamUrl;
7519
+ this.principalUrl = row.principalUrl;
7520
+ this.principalKind = row.principalKind;
7521
+ this.permissionBypass = isBuiltInSystemPrincipalUrl(row.principalUrl);
6544
7522
  this.initialShapeHandle = row.shapeHandle;
6545
7523
  this.initialShapeOffset = row.shapeOffset;
6546
7524
  }
@@ -6648,7 +7626,7 @@ var EntityBridge = class {
6648
7626
  url: electricUrlWithPath(this.electricUrl, `/v1/shape`).toString(),
6649
7627
  params: {
6650
7628
  table: `entities`,
6651
- where: buildTenantTagsWhereClause(this.tenantId, this.tags),
7629
+ where: buildTenantTagsWhereClause(this.tenantId, this.tags, this.principalUrl, this.principalKind, this.permissionBypass),
6652
7630
  ...this.electricSecret ? { secret: this.electricSecret } : {},
6653
7631
  columns: [...ENTITY_SHAPE_COLUMNS],
6654
7632
  replica: `full`
@@ -6811,15 +7789,17 @@ var EntityBridgeManager = class {
6811
7789
  await bridge.stop();
6812
7790
  }));
6813
7791
  }
6814
- async register(tagsInput) {
7792
+ async register(tagsInput, principalUrl$1, principalKind) {
6815
7793
  if (!this.electricUrl) throw new Error(`[entity-bridge] Electric URL is required for entities()`);
6816
7794
  const tags = normalizeTags(assertTags(tagsInput));
6817
- const sourceRef = sourceRefForTags(tags);
7795
+ const sourceRef = principalScopedSourceRef(sourceRefForTags(tags), principalUrl$1, principalKind);
6818
7796
  const streamUrl = getEntitiesStreamPath(sourceRef);
6819
7797
  const row = await this.registry.upsertEntityBridge({
6820
7798
  sourceRef,
6821
7799
  tags,
6822
- streamUrl
7800
+ streamUrl,
7801
+ principalUrl: principalUrl$1,
7802
+ principalKind
6823
7803
  });
6824
7804
  await this.registry.touchEntityBridge(sourceRef);
6825
7805
  await this.ensureBridge(row);
@@ -7639,6 +8619,8 @@ var ElectricAgentsTenantRuntime = class {
7639
8619
  try {
7640
8620
  await this.manager.send(payload.entityUrl, {
7641
8621
  from: payload.from,
8622
+ from_principal: payload.from_principal,
8623
+ from_agent: payload.from_agent,
7642
8624
  payload: payload.payload,
7643
8625
  key: payload.key ?? `scheduled-task-${taskId}`,
7644
8626
  type: payload.type
@@ -7711,6 +8693,7 @@ var ElectricAgentsTenantRuntime = class {
7711
8693
  await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
7712
8694
  entityUrl: targetUrl,
7713
8695
  from: senderUrl,
8696
+ from_agent: senderUrl,
7714
8697
  payload: value.payload,
7715
8698
  key: `scheduled-${producerId}`,
7716
8699
  type: typeof value.messageType === `string` ? value.messageType : void 0,
@@ -7735,11 +8718,20 @@ var ElectricAgentsTenantRuntime = class {
7735
8718
  async applyManifestEntitySource(ownerEntityUrl, manifestKey, operation, value) {
7736
8719
  const sourceRef = operation === `delete` ? void 0 : this.extractEntitiesSourceRef(value);
7737
8720
  await this.manager.registry.replaceEntityManifestSource(ownerEntityUrl, manifestKey, sourceRef);
8721
+ const sharedStateId = operation === `delete` ? void 0 : this.extractSharedStateId(value);
8722
+ await this.manager.registry.replaceSharedStateLink(ownerEntityUrl, manifestKey, sharedStateId);
7738
8723
  }
7739
8724
  extractEntitiesSourceRef(manifest) {
7740
8725
  if (manifest?.kind === `source` && manifest.sourceType === `entities` && typeof manifest.sourceRef === `string`) return manifest.sourceRef;
7741
8726
  return void 0;
7742
8727
  }
8728
+ extractSharedStateId(manifest) {
8729
+ if (manifest?.kind === `shared-state` && typeof manifest.id === `string`) return manifest.id;
8730
+ if (manifest?.kind !== `source` || manifest.sourceType !== `db`) return void 0;
8731
+ if (typeof manifest.sourceRef === `string`) return manifest.sourceRef;
8732
+ const config = typeof manifest.config === `object` && manifest.config !== null && !Array.isArray(manifest.config) ? manifest.config : void 0;
8733
+ return typeof config?.id === `string` ? config.id : void 0;
8734
+ }
7743
8735
  async maybeMarkEntityIdleAfterRunFinished(entityUrl) {
7744
8736
  const primaryStream = `${entityUrl}/main`;
7745
8737
  const callbacks = await this.db.select().from(consumerCallbacks).where(and(eq(consumerCallbacks.tenantId, this.serviceId), eq(consumerCallbacks.primaryStream, primaryStream))).limit(1);
@@ -8420,6 +9412,8 @@ var WakeRegistry = class {
8420
9412
  if (eventType === `inbox`) {
8421
9413
  const value = event.value;
8422
9414
  if (typeof value?.from === `string`) change.from = value.from;
9415
+ if (typeof value?.from_principal === `string`) change.from_principal = value.from_principal;
9416
+ if (typeof value?.from_agent === `string`) change.from_agent = value.from_agent;
8423
9417
  if (`payload` in (value ?? {})) change.payload = value?.payload;
8424
9418
  if (typeof value?.timestamp === `string`) change.timestamp = value.timestamp;
8425
9419
  if (typeof value?.message_type === `string`) change.message_type = value.message_type;
@@ -8770,6 +9764,7 @@ var ElectricAgentsServer = class {
8770
9764
  entityBridgeManager: this.entityBridgeManager,
8771
9765
  ...this.options.eventSources ? { eventSources: this.options.eventSources } : {},
8772
9766
  ...this.options.ensureEventSourceWakeSource ? { ensureEventSourceWakeSource: this.options.ensureEventSourceWakeSource } : {},
9767
+ ...this.options.authorizeRequest ? { authorizeRequest: this.options.authorizeRequest } : {},
8773
9768
  isShuttingDown: () => this.shuttingDown,
8774
9769
  mockAgent: this.mockAgentBootstrap ? { runtime: this.mockAgentBootstrap.runtime } : void 0
8775
9770
  };