@electric-ax/agents-server 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -61,6 +61,7 @@ const entities = pgTable(`entities`, {
61
61
  tagsIndex: text(`tags_index`).array().notNull().default(sql`'{}'::text[]`),
62
62
  spawnArgs: jsonb(`spawn_args`).default({}),
63
63
  parent: text(`parent`),
64
+ createdBy: text(`created_by`),
64
65
  typeRevision: integer(`type_revision`),
65
66
  inboxSchemas: jsonb(`inbox_schemas`),
66
67
  stateSchemas: jsonb(`state_schemas`),
@@ -71,6 +72,7 @@ const entities = pgTable(`entities`, {
71
72
  index(`idx_entities_type`).on(table.tenantId, table.type),
72
73
  index(`idx_entities_status`).on(table.tenantId, table.status),
73
74
  index(`idx_entities_parent`).on(table.tenantId, table.parent),
75
+ index(`idx_entities_created_by`).on(table.tenantId, table.createdBy),
74
76
  index(`entities_tags_index_gin`).using(`gin`, table.tagsIndex),
75
77
  check(`chk_entities_status`, sql`${table.status} IN ('spawning', 'running', 'idle', 'stopped')`)
76
78
  ]);
@@ -285,7 +287,7 @@ function createDb(postgresUrl) {
285
287
  const poolMax = Number(process.env.ELECTRIC_AGENTS_PG_POOL_MAX ?? `100`);
286
288
  const client = postgres(postgresUrl, {
287
289
  max: poolMax,
288
- fetch_types: false
290
+ fetch_types: true
289
291
  });
290
292
  const db = drizzle(client, { schema: schema_exports });
291
293
  return {
@@ -353,6 +355,7 @@ function toPublicEntity(entity) {
353
355
  tags: entity.tags,
354
356
  spawn_args: entity.spawn_args,
355
357
  parent: entity.parent,
358
+ created_by: entity.created_by,
356
359
  created_at: entity.created_at,
357
360
  updated_at: entity.updated_at
358
361
  };
@@ -592,6 +595,24 @@ var PostgresRegistry = class {
592
595
  }
593
596
  });
594
597
  }
598
+ async ensureEntityType(et) {
599
+ const existing = await this.getEntityType(et.name);
600
+ if (existing) return existing;
601
+ await this.db.insert(entityTypes).values({
602
+ tenantId: this.tenantId,
603
+ name: et.name,
604
+ description: et.description,
605
+ creationSchema: et.creation_schema ?? null,
606
+ inboxSchemas: et.inbox_schemas ?? null,
607
+ stateSchemas: et.state_schemas ?? null,
608
+ serveEndpoint: et.serve_endpoint ?? null,
609
+ defaultDispatchPolicy: et.default_dispatch_policy ?? null,
610
+ revision: et.revision,
611
+ createdAt: et.created_at,
612
+ updatedAt: et.updated_at
613
+ }).onConflictDoNothing();
614
+ return await this.getEntityType(et.name);
615
+ }
595
616
  async getEntityType(name) {
596
617
  const rows = await this.db.select().from(entityTypes).where(this.entityTypeWhere(name)).limit(1);
597
618
  if (rows.length === 0) return null;
@@ -631,6 +652,7 @@ var PostgresRegistry = class {
631
652
  tagsIndex: buildTagsIndex(entity.tags),
632
653
  spawnArgs: entity.spawn_args ?? {},
633
654
  parent: entity.parent ?? null,
655
+ createdBy: entity.created_by ?? null,
634
656
  typeRevision: entity.type_revision ?? null,
635
657
  inboxSchemas: entity.inbox_schemas ?? null,
636
658
  stateSchemas: entity.state_schemas ?? null,
@@ -676,6 +698,7 @@ var PostgresRegistry = class {
676
698
  if (filter?.type) conditions.push(eq(entities.type, filter.type));
677
699
  if (filter?.status) conditions.push(eq(entities.status, filter.status));
678
700
  if (filter?.parent) conditions.push(eq(entities.parent, filter.parent));
701
+ if (filter?.created_by) conditions.push(eq(entities.createdBy, filter.created_by));
679
702
  const whereClause = and(...conditions);
680
703
  const countResult = await this.db.select({ count: sql`count(*)` }).from(entities).where(whereClause);
681
704
  const total = Number(countResult[0].count);
@@ -962,6 +985,7 @@ var PostgresRegistry = class {
962
985
  tags: row.tags ?? {},
963
986
  spawn_args: row.spawnArgs,
964
987
  parent: row.parent ?? void 0,
988
+ created_by: row.createdBy ?? void 0,
965
989
  type_revision: row.typeRevision ?? void 0,
966
990
  inbox_schemas: row.inboxSchemas,
967
991
  state_schemas: row.stateSchemas,
@@ -1687,6 +1711,239 @@ function parseDispatchPolicy(value, label = `dispatch_policy`) {
1687
1711
  throw new Error(details ? `${label} does not match dispatch policy schema: ${details}` : `${label} does not match dispatch policy schema`);
1688
1712
  }
1689
1713
 
1714
+ //#endregion
1715
+ //#region src/utils/webhook-url.ts
1716
+ function rewriteLoopbackWebhookUrl(value) {
1717
+ if (!value) return void 0;
1718
+ const rewriteTarget = process.env.ELECTRIC_AGENTS_REWRITE_LOOPBACK_WEBHOOKS_TO?.trim();
1719
+ if (!rewriteTarget) return value;
1720
+ const url = new URL(value);
1721
+ if (!isLoopbackHostname(url.hostname)) return value;
1722
+ if (rewriteTarget.includes(`://`)) {
1723
+ const target = new URL(rewriteTarget);
1724
+ url.protocol = target.protocol;
1725
+ url.username = target.username;
1726
+ url.password = target.password;
1727
+ url.hostname = target.hostname;
1728
+ url.port = target.port;
1729
+ return url.toString();
1730
+ }
1731
+ url.host = rewriteTarget;
1732
+ return url.toString();
1733
+ }
1734
+ function isLoopbackHostname(hostname) {
1735
+ return hostname === `localhost` || hostname === `127.0.0.1` || hostname === `::1`;
1736
+ }
1737
+
1738
+ //#endregion
1739
+ //#region src/routing/dispatch-policy.ts
1740
+ function subscriptionIdForDispatchTarget(target) {
1741
+ if (target.subscription_id) return target.subscription_id;
1742
+ if (target.type === `runner`) return `runner:${target.runnerId}`;
1743
+ const digest = createHash(`sha256`).update(target.url).digest(`hex`);
1744
+ return `webhook:${digest.slice(0, 16)}`;
1745
+ }
1746
+ function subscriptionIdForEntityDispatchTarget(target, entityUrl) {
1747
+ const base = subscriptionIdForDispatchTarget(target);
1748
+ if (!target.subscription_id) return base;
1749
+ const digest = createHash(`sha256`).update(entityUrl).digest(`hex`);
1750
+ return `${base}:${digest.slice(0, 16)}`;
1751
+ }
1752
+ async function resolveEffectiveDispatchPolicyForSpawn(ctx, typeName, opts) {
1753
+ if (opts.dispatchPolicy) return opts.dispatchPolicy;
1754
+ const entityType = await ctx.entityManager.registry.getEntityType(typeName);
1755
+ if (opts.parent) {
1756
+ const parent = await ctx.entityManager.registry.getEntity(opts.parent);
1757
+ if (parent?.dispatch_policy) return applyTypeDefaultSubscriptionScope(parent.dispatch_policy, entityType?.default_dispatch_policy);
1758
+ }
1759
+ return entityType?.default_dispatch_policy;
1760
+ }
1761
+ async function resolveEffectiveDispatchPolicyForEntity(ctx, entity) {
1762
+ if (entity.dispatch_policy) return entity.dispatch_policy;
1763
+ const entityType = await ctx.entityManager.registry.getEntityType(entity.type);
1764
+ return entityType?.default_dispatch_policy;
1765
+ }
1766
+ async function backfillEntityDispatchPolicy(ctx, entity) {
1767
+ if (entity.dispatch_policy) return entity;
1768
+ const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
1769
+ if (!dispatchPolicy) return entity;
1770
+ return await ctx.entityManager.registry.updateEntityDispatchPolicy(entity.url, dispatchPolicy) ?? {
1771
+ ...entity,
1772
+ dispatch_policy: dispatchPolicy
1773
+ };
1774
+ }
1775
+ function applyTypeDefaultSubscriptionScope(policy, typeDefault) {
1776
+ const target = policy.targets[0];
1777
+ const defaultTarget = typeDefault?.targets[0];
1778
+ if (!target || !defaultTarget?.subscription_id) return policy;
1779
+ if (!sameDispatchDestination(target, defaultTarget)) return policy;
1780
+ if (target.subscription_id === defaultTarget.subscription_id) return policy;
1781
+ return { targets: [{
1782
+ ...target,
1783
+ subscription_id: defaultTarget.subscription_id
1784
+ }] };
1785
+ }
1786
+ function sameDispatchDestination(a, b) {
1787
+ if (a.type !== b.type) return false;
1788
+ if (a.type === `runner` && b.type === `runner`) return a.runnerId === b.runnerId;
1789
+ if (a.type === `webhook` && b.type === `webhook`) return a.url === b.url;
1790
+ return false;
1791
+ }
1792
+ async function assertDispatchPolicyAllowed(ctx, policy) {
1793
+ const target = policy?.targets[0];
1794
+ if (!target || target.type !== `runner`) return;
1795
+ const runner = await ctx.entityManager.registry.getRunner(target.runnerId);
1796
+ if (!runner) throw new ElectricAgentsError(ErrCodeNotFound, `Runner "${target.runnerId}" not found`, 404);
1797
+ if (ctx.principal && runner.owner_user_id !== ctx.principal.key) throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner dispatch requires the authenticated owner`, 403);
1798
+ }
1799
+ async function linkEntityDispatchSubscription(ctx, entity) {
1800
+ const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
1801
+ const target = dispatchPolicy?.targets[0];
1802
+ if (!target) return;
1803
+ await linkStreamToTargetSubscription(ctx, target, entity);
1804
+ }
1805
+ async function unlinkEntityDispatchSubscription(ctx, entity) {
1806
+ const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
1807
+ const target = dispatchPolicy?.targets[0];
1808
+ if (!target) return;
1809
+ const subscriptionId = subscriptionIdForEntityDispatchTarget(target, entity.url);
1810
+ await ctx.streamClient.removeSubscriptionStream(subscriptionId, entity.streams.main).catch((err) => {
1811
+ serverLog.warn(`[dispatch-policy] failed to remove stream from subscription`, {
1812
+ subscriptionId,
1813
+ stream: entity.streams.main
1814
+ }, err);
1815
+ });
1816
+ }
1817
+ async function linkStreamToTargetSubscription(ctx, target, entity) {
1818
+ const streamPath = entity.streams.main;
1819
+ const subscriptionId = subscriptionIdForEntityDispatchTarget(target, entity.url);
1820
+ const existing = await ctx.streamClient.getSubscription(subscriptionId);
1821
+ if (target.type === `runner`) {
1822
+ const runner = await ctx.entityManager.registry.getRunner(target.runnerId);
1823
+ if (!runner) throw new ElectricAgentsError(ErrCodeNotFound, `Runner "${target.runnerId}" not found`, 404);
1824
+ const wakeStream = runner.wake_stream || runnerWakeStream(target.runnerId);
1825
+ await ctx.streamClient.ensure(wakeStream, { contentType: `application/json` });
1826
+ if (!existing) {
1827
+ await ctx.streamClient.putSubscription(subscriptionId, {
1828
+ type: `pull-wake`,
1829
+ streams: [streamPath],
1830
+ wake_stream: wakeStream,
1831
+ description: `Electric Agents runner ${target.runnerId}`
1832
+ });
1833
+ return;
1834
+ }
1835
+ await ctx.streamClient.addSubscriptionStreams(subscriptionId, [streamPath]);
1836
+ return;
1837
+ }
1838
+ const webhookUrl = rewriteLoopbackWebhookUrl(target.url);
1839
+ if (!webhookUrl) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Webhook dispatch target must include a valid URL`, 400);
1840
+ const forwardUrl = appendPathToUrl(ctx.publicUrl, `/_electric/webhook-forward/${encodeURIComponent(subscriptionId)}`);
1841
+ if (!existing) await ctx.streamClient.putSubscription(subscriptionId, {
1842
+ type: `webhook`,
1843
+ streams: [streamPath],
1844
+ webhook: { url: forwardUrl },
1845
+ description: `Electric Agents webhook ${subscriptionId}`
1846
+ });
1847
+ else await ctx.streamClient.addSubscriptionStreams(subscriptionId, [streamPath]);
1848
+ await ctx.pgDb.insert(subscriptionWebhooks).values({
1849
+ tenantId: ctx.service,
1850
+ subscriptionId,
1851
+ webhookUrl
1852
+ }).onConflictDoUpdate({
1853
+ target: [subscriptionWebhooks.tenantId, subscriptionWebhooks.subscriptionId],
1854
+ set: { webhookUrl }
1855
+ });
1856
+ }
1857
+
1858
+ //#endregion
1859
+ //#region src/principal.ts
1860
+ const PRINCIPAL_KINDS = new Set([
1861
+ `user`,
1862
+ `agent`,
1863
+ `service`,
1864
+ `system`
1865
+ ]);
1866
+ function parsePrincipalKey(input) {
1867
+ const colon = input.indexOf(`:`);
1868
+ if (colon <= 0) throw new Error(`Invalid principal key`);
1869
+ const kind = input.slice(0, colon);
1870
+ const id = input.slice(colon + 1);
1871
+ if (!PRINCIPAL_KINDS.has(kind)) throw new Error(`Invalid principal kind`);
1872
+ if (!id || id.includes(`/`)) throw new Error(`Invalid principal id`);
1873
+ const key = `${kind}:${id}`;
1874
+ return {
1875
+ kind,
1876
+ id,
1877
+ key,
1878
+ url: `/principal/${encodeURIComponent(key)}`
1879
+ };
1880
+ }
1881
+ function principalUrl(key) {
1882
+ return parsePrincipalKey(key).url;
1883
+ }
1884
+ function principalKeyFromUrl(url) {
1885
+ if (!url.startsWith(`/principal/`)) return null;
1886
+ const segment = url.slice(`/principal/`.length);
1887
+ if (!segment || segment.includes(`/`)) return null;
1888
+ try {
1889
+ const key = decodeURIComponent(segment);
1890
+ return parsePrincipalKey(key).key;
1891
+ } catch {
1892
+ return null;
1893
+ }
1894
+ }
1895
+ const BUILT_IN_SYSTEM_PRINCIPAL_IDS = new Set([
1896
+ `framework`,
1897
+ `auth-sync`,
1898
+ `dev-local`
1899
+ ]);
1900
+ function isBuiltInSystemPrincipalUrl(url) {
1901
+ if (!url?.startsWith(`/principal/`)) return false;
1902
+ try {
1903
+ const key = principalKeyFromUrl(url);
1904
+ if (!key) return false;
1905
+ const principal = parsePrincipalKey(key);
1906
+ return principal.kind === `system` && BUILT_IN_SYSTEM_PRINCIPAL_IDS.has(principal.id);
1907
+ } catch {
1908
+ return false;
1909
+ }
1910
+ }
1911
+ function principalFromCreatedBy(createdBy) {
1912
+ if (!createdBy) return void 0;
1913
+ const key = principalKeyFromUrl(createdBy);
1914
+ if (!key) return {
1915
+ url: createdBy,
1916
+ key: null
1917
+ };
1918
+ const principal = parsePrincipalKey(key);
1919
+ return {
1920
+ url: principal.url,
1921
+ key: principal.key,
1922
+ kind: principal.kind,
1923
+ id: principal.id
1924
+ };
1925
+ }
1926
+ const principalIdentityStateSchema = Type.Object({
1927
+ kind: Type.Union([
1928
+ Type.Literal(`user`),
1929
+ Type.Literal(`agent`),
1930
+ Type.Literal(`service`),
1931
+ Type.Literal(`system`)
1932
+ ]),
1933
+ id: Type.String(),
1934
+ key: Type.String(),
1935
+ url: Type.String(),
1936
+ updated_at: Type.String(),
1937
+ display_name: Type.Optional(Type.String()),
1938
+ email: Type.Optional(Type.String()),
1939
+ avatar_url: Type.Optional(Type.String()),
1940
+ auth_provider: Type.Optional(Type.String()),
1941
+ auth_subject: Type.Optional(Type.String()),
1942
+ claims: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
1943
+ created_at: Type.Optional(Type.String())
1944
+ }, { additionalProperties: false });
1945
+ const principalUpdateIdentityMessageSchema = Type.Object({ identity: principalIdentityStateSchema }, { additionalProperties: false });
1946
+
1690
1947
  //#endregion
1691
1948
  //#region src/manifest-side-effects.ts
1692
1949
  function isRecord$1(value) {
@@ -1804,25 +2061,11 @@ function extractTraceContext(headers) {
1804
2061
 
1805
2062
  //#endregion
1806
2063
  //#region src/entity-manager.ts
2064
+ function createInitialQueuePosition(date) {
2065
+ return `${String(date.getTime()).padStart(16, `0`)}:a0`;
2066
+ }
1807
2067
  const DEFAULT_FORK_WAIT_TIMEOUT_MS = 12e4;
1808
2068
  const DEFAULT_FORK_WAIT_POLL_MS = 250;
1809
- function applyTypeDefaultSubscriptionScope$1(policy, typeDefault) {
1810
- const target = policy.targets[0];
1811
- const defaultTarget = typeDefault?.targets[0];
1812
- if (!target || !defaultTarget?.subscription_id) return policy;
1813
- if (!sameDispatchDestination$1(target, defaultTarget)) return policy;
1814
- if (target.subscription_id === defaultTarget.subscription_id) return policy;
1815
- return { targets: [{
1816
- ...target,
1817
- subscription_id: defaultTarget.subscription_id
1818
- }] };
1819
- }
1820
- function sameDispatchDestination$1(a, b) {
1821
- if (a.type !== b.type) return false;
1822
- if (a.type === `runner` && b.type === `runner`) return a.runnerId === b.runnerId;
1823
- if (a.type === `webhook` && b.type === `webhook`) return a.url === b.url;
1824
- return false;
1825
- }
1826
2069
  function sleep(ms) {
1827
2070
  return new Promise((resolve$1) => setTimeout(resolve$1, ms));
1828
2071
  }
@@ -1893,6 +2136,7 @@ var EntityManager = class {
1893
2136
  }
1894
2137
  async registerEntityType(req) {
1895
2138
  if (!req.name) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: name`, 400);
2139
+ if (req.name === `principal`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type "principal" is built in and cannot be registered or updated`, 400);
1896
2140
  if (req.name.startsWith(`_`)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type names starting with "_" are reserved`, 400);
1897
2141
  if (!req.description) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: description`, 400);
1898
2142
  this.validateSchema(req.creation_schema);
@@ -1919,10 +2163,63 @@ var EntityManager = class {
1919
2163
  return stored;
1920
2164
  }
1921
2165
  async deleteEntityType(name) {
2166
+ if (name === `principal`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type "principal" is built in and cannot be deleted`, 400);
1922
2167
  const existing = await this.registry.getEntityType(name);
1923
2168
  if (!existing) throw new ElectricAgentsError(ErrCodeNotFound, `Entity type "${name}" not found`, 404);
1924
2169
  await this.registry.deleteEntityType(name);
1925
2170
  }
2171
+ async ensurePrincipalEntityType() {
2172
+ const now = new Date().toISOString();
2173
+ return await this.registry.ensureEntityType({
2174
+ name: `principal`,
2175
+ description: `built-in principal entity`,
2176
+ inbox_schemas: { update_identity: principalUpdateIdentityMessageSchema },
2177
+ state_schemas: { identity: principalIdentityStateSchema },
2178
+ revision: 1,
2179
+ created_at: now,
2180
+ updated_at: now
2181
+ });
2182
+ }
2183
+ async ensurePrincipal(principal) {
2184
+ const existing = await this.registry.getEntity(principal.url);
2185
+ if (existing) return existing;
2186
+ await this.ensurePrincipalEntityType();
2187
+ try {
2188
+ const entity = await this.spawn(`principal`, {
2189
+ instance_id: principal.key,
2190
+ args: {
2191
+ kind: principal.kind,
2192
+ id: principal.id,
2193
+ key: principal.key
2194
+ },
2195
+ tags: {
2196
+ principal_kind: principal.kind,
2197
+ principal_id: principal.id
2198
+ },
2199
+ created_by: principal.url
2200
+ });
2201
+ const now = new Date().toISOString();
2202
+ await this.streamClient.append(entity.streams.main, this.encodeChangeEvent({
2203
+ type: `identity`,
2204
+ key: `self`,
2205
+ value: {
2206
+ kind: principal.kind,
2207
+ id: principal.id,
2208
+ key: principal.key,
2209
+ url: principal.url,
2210
+ created_at: now,
2211
+ updated_at: now
2212
+ }
2213
+ }));
2214
+ return entity;
2215
+ } catch (error) {
2216
+ if (error instanceof ElectricAgentsError && error.code === ErrCodeDuplicateURL) {
2217
+ const raced = await this.registry.getEntity(principal.url);
2218
+ if (raced) return raced;
2219
+ }
2220
+ throw error;
2221
+ }
2222
+ }
1926
2223
  /**
1927
2224
  * Spawn a new entity of the given type with durable streams.
1928
2225
  */
@@ -1938,6 +2235,7 @@ var EntityManager = class {
1938
2235
  });
1939
2236
  }
1940
2237
  async spawnInner(typeName, req) {
2238
+ if (typeName === `principal` && req.created_by !== principalUrl(req.instance_id)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Principal entities are built in and can only be materialized by the system`, 400);
1941
2239
  if (typeName.startsWith(`_`)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type names starting with "_" are reserved`, 400);
1942
2240
  const entityType = await this.registry.getEntityType(typeName);
1943
2241
  if (!entityType) throw new ElectricAgentsError(ErrCodeUnknownEntityType, `Entity type "${typeName}" not found`, 404);
@@ -1949,7 +2247,7 @@ var EntityManager = class {
1949
2247
  const instanceId = req.instance_id || randomUUID();
1950
2248
  if (instanceId.includes(`/`)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `instance_id must not contain forward slashes`, 400);
1951
2249
  const writeToken = randomUUID();
1952
- const entityURL = `/${typeName}/${instanceId}`;
2250
+ const entityURL = typeName === `principal` ? principalUrl(instanceId) : `/${typeName}/${instanceId}`;
1953
2251
  const mainPath = `${entityURL}/main`;
1954
2252
  const errorPath = `${entityURL}/error`;
1955
2253
  const subscriptionId = `${typeName}-handler`;
@@ -1961,7 +2259,7 @@ var EntityManager = class {
1961
2259
  parentEntity = await this.registry.getEntity(req.parent);
1962
2260
  if (!parentEntity) throw new ElectricAgentsError(ErrCodeNotFound, `Parent entity "${req.parent}" not found`, 404);
1963
2261
  }
1964
- const dispatchPolicy = req.dispatch_policy ? this.validateDispatchPolicy(req.dispatch_policy, { label: `dispatch_policy` }) : parentEntity?.dispatch_policy ? applyTypeDefaultSubscriptionScope$1(parentEntity.dispatch_policy, entityType.default_dispatch_policy) : entityType.default_dispatch_policy;
2262
+ const dispatchPolicy = req.dispatch_policy ? this.validateDispatchPolicy(req.dispatch_policy, { label: `dispatch_policy` }) : parentEntity?.dispatch_policy ? applyTypeDefaultSubscriptionScope(parentEntity.dispatch_policy, entityType.default_dispatch_policy) : entityType.default_dispatch_policy;
1965
2263
  const now = Date.now();
1966
2264
  const entityData = {
1967
2265
  type: typeName,
@@ -1980,6 +2278,7 @@ var EntityManager = class {
1980
2278
  inbox_schemas: entityType.inbox_schemas,
1981
2279
  state_schemas: entityType.state_schemas,
1982
2280
  created_at: now,
2281
+ created_by: req.created_by ?? parentEntity?.created_by,
1983
2282
  updated_at: now
1984
2283
  };
1985
2284
  if (req.parent) entityData.parent = req.parent;
@@ -2009,7 +2308,7 @@ var EntityManager = class {
2009
2308
  const inboxEvent = entityStateSchema.inbox.insert({
2010
2309
  key: `msg-in-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
2011
2310
  value: {
2012
- from: req.parent ?? `spawn`,
2311
+ from: req.created_by ?? req.parent ?? `spawn`,
2013
2312
  payload: req.initialMessage,
2014
2313
  timestamp: msgNow
2015
2314
  }
@@ -2552,10 +2851,10 @@ var EntityManager = class {
2552
2851
  changed = true;
2553
2852
  }
2554
2853
  }
2555
- if (typeof next.from === `string`) {
2556
- const forkFrom = entityUrlMap.get(next.from);
2557
- if (forkFrom) {
2558
- next.from = forkFrom;
2854
+ if (typeof next.senderUrl === `string`) {
2855
+ const forkSender = entityUrlMap.get(next.senderUrl);
2856
+ if (forkSender) {
2857
+ next.senderUrl = forkSender;
2559
2858
  changed = true;
2560
2859
  }
2561
2860
  }
@@ -2621,6 +2920,7 @@ var EntityManager = class {
2621
2920
  const fireAtRaw = manifest.fireAt;
2622
2921
  const producerId = manifest.producerId;
2623
2922
  const targetUrl = manifest.targetUrl;
2923
+ const senderUrl = typeof manifest.senderUrl === `string` ? manifest.senderUrl : ownerEntityUrl;
2624
2924
  if (typeof fireAtRaw !== `string` || typeof producerId !== `string` || typeof targetUrl !== `string`) {
2625
2925
  serverLog.warn(`[agent-server] invalid forked future_send manifest entry for ${ownerEntityUrl}/${manifestKey}`);
2626
2926
  return;
@@ -2632,7 +2932,7 @@ var EntityManager = class {
2632
2932
  }
2633
2933
  await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
2634
2934
  entityUrl: targetUrl,
2635
- from: typeof manifest.from === `string` ? manifest.from : ownerEntityUrl,
2935
+ from: senderUrl,
2636
2936
  payload: manifest.payload,
2637
2937
  key: `scheduled-${producerId}`,
2638
2938
  type: typeof manifest.messageType === `string` ? manifest.messageType : void 0,
@@ -2646,6 +2946,7 @@ var EntityManager = class {
2646
2946
  kind: `schedule`,
2647
2947
  scheduleType: `future_send`,
2648
2948
  targetUrl,
2949
+ senderUrl,
2649
2950
  fireAt: fireAt.toISOString(),
2650
2951
  producerId,
2651
2952
  status: `pending`
@@ -2673,9 +2974,14 @@ var EntityManager = class {
2673
2974
  const value = {
2674
2975
  from: req.from,
2675
2976
  payload: req.payload,
2676
- timestamp: now
2977
+ timestamp: now,
2978
+ mode: req.mode ?? `immediate`,
2979
+ status: req.mode === `queued` || req.mode === `paused` ? `pending` : `processed`
2677
2980
  };
2678
2981
  if (req.type) value.message_type = req.type;
2982
+ if (req.position) value.position = req.position;
2983
+ else if (value.mode === `queued` || value.mode === `paused`) value.position = createInitialQueuePosition(new Date(now));
2984
+ if (value.status === `processed`) value.processed_at = now;
2679
2985
  const envelope = entityStateSchema.inbox.insert({
2680
2986
  key,
2681
2987
  value
@@ -2687,11 +2993,47 @@ var EntityManager = class {
2687
2993
  return;
2688
2994
  }
2689
2995
  await this.streamClient.append(entity.streams.main, encoded);
2996
+ if (entity.type === `principal` && req.type === `update_identity`) {
2997
+ const identity = req.payload?.identity;
2998
+ await this.streamClient.append(entity.streams.main, this.encodeChangeEvent({
2999
+ type: `identity`,
3000
+ key: `self`,
3001
+ value: identity
3002
+ }));
3003
+ }
2690
3004
  } catch (err) {
2691
3005
  if (this.isClosedStreamError(err)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409);
2692
3006
  throw err;
2693
3007
  }
2694
3008
  }
3009
+ async updateInboxMessage(entityUrl, key, req) {
3010
+ const entity = await this.registry.getEntity(entityUrl);
3011
+ if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
3012
+ if (entity.status === `stopped`) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409);
3013
+ const now = new Date().toISOString();
3014
+ const value = {};
3015
+ if (`payload` in req) value.payload = req.payload;
3016
+ if (req.position !== void 0) value.position = req.position;
3017
+ if (req.mode !== void 0) value.mode = req.mode;
3018
+ if (req.status !== void 0) {
3019
+ value.status = req.status;
3020
+ if (req.status === `processed`) value.processed_at = now;
3021
+ if (req.status === `cancelled`) value.cancelled_at = now;
3022
+ }
3023
+ if (Object.keys(value).length === 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `No inbox fields to update`, 400);
3024
+ const envelope = entityStateSchema.inbox.update({
3025
+ key,
3026
+ value
3027
+ });
3028
+ await this.streamClient.append(entity.streams.main, this.encodeChangeEvent(envelope));
3029
+ }
3030
+ async deleteInboxMessage(entityUrl, key) {
3031
+ const entity = await this.registry.getEntity(entityUrl);
3032
+ if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
3033
+ if (entity.status === `stopped`) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409);
3034
+ const envelope = entityStateSchema.inbox.delete({ key });
3035
+ await this.streamClient.append(entity.streams.main, this.encodeChangeEvent(envelope));
3036
+ }
2695
3037
  async setTag(entityUrl, key, req, token) {
2696
3038
  const entity = await this.registry.getEntity(entityUrl);
2697
3039
  if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
@@ -2777,7 +3119,7 @@ var EntityManager = class {
2777
3119
  async upsertFutureSendSchedule(ownerEntityUrl, req) {
2778
3120
  if (!this.scheduler) throw new Error(`Scheduler not configured`);
2779
3121
  const targetUrl = req.targetUrl ?? ownerEntityUrl;
2780
- const from = req.from ?? ownerEntityUrl;
3122
+ const from = req.senderUrl ?? ownerEntityUrl;
2781
3123
  const fireAt = new Date(req.fireAt);
2782
3124
  if (Number.isNaN(fireAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid fireAt timestamp: ${req.fireAt}`, 400);
2783
3125
  await this.validateSendRequest(targetUrl, {
@@ -2805,9 +3147,9 @@ var EntityManager = class {
2805
3147
  scheduleType: `future_send`,
2806
3148
  fireAt: fireAt.toISOString(),
2807
3149
  targetUrl,
3150
+ senderUrl: from,
2808
3151
  payload: req.payload,
2809
3152
  producerId,
2810
- ...req.from ? { from: req.from } : {},
2811
3153
  ...req.messageType ? { messageType: req.messageType } : {},
2812
3154
  status: `pending`
2813
3155
  }
@@ -2821,9 +3163,9 @@ var EntityManager = class {
2821
3163
  scheduleType: `future_send`,
2822
3164
  fireAt: fireAt.toISOString(),
2823
3165
  targetUrl,
3166
+ senderUrl: from,
2824
3167
  payload: req.payload,
2825
3168
  producerId,
2826
- ...req.from ? { from: req.from } : {},
2827
3169
  ...req.messageType ? { messageType: req.messageType } : {},
2828
3170
  status: `pending`
2829
3171
  }, { txid });
@@ -2861,7 +3203,9 @@ var EntityManager = class {
2861
3203
  from: req.from,
2862
3204
  payload: req.payload,
2863
3205
  key: req.key,
2864
- type: req.type
3206
+ type: req.type,
3207
+ mode: req.mode,
3208
+ position: req.position
2865
3209
  }, fireAt);
2866
3210
  }
2867
3211
  /**
@@ -3011,6 +3355,7 @@ var EntityManager = class {
3011
3355
  * Add new input/output schema keys to an entity type directly in Postgres.
3012
3356
  */
3013
3357
  async amendSchemas(typeName, schemas) {
3358
+ if (typeName === `principal`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type "principal" is built in and cannot be amended`, 400);
3014
3359
  this.validateSchemaMap(schemas.inbox_schemas);
3015
3360
  this.validateSchemaMap(schemas.state_schemas);
3016
3361
  const existing = await this.registry.getEntityType(typeName);
@@ -3060,9 +3405,11 @@ var EntityManager = class {
3060
3405
  url: entity.url,
3061
3406
  streams: entity.streams,
3062
3407
  tags: entity.tags,
3063
- spawnArgs: entity.spawn_args
3408
+ spawnArgs: entity.spawn_args,
3409
+ createdBy: entity.created_by
3064
3410
  },
3065
- triggerEvent: `message_received`
3411
+ principal: principalFromCreatedBy(entity.created_by),
3412
+ triggerEvent: `inbox`
3066
3413
  };
3067
3414
  }
3068
3415
  validateSchema(schema) {
@@ -3102,6 +3449,7 @@ var EntityManager = class {
3102
3449
  }
3103
3450
  }
3104
3451
  if (!req.from) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: from`, 400);
3452
+ if (entity.type === `principal` && req.type === `update_identity` && !isBuiltInSystemPrincipalUrl(req.from)) throw new ElectricAgentsError(ErrCodeUnauthorized, `Only built-in system principals can update principal identity`, 403);
3105
3453
  if (req.payload === void 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: payload`, 400);
3106
3454
  return entity;
3107
3455
  }
@@ -4501,6 +4849,7 @@ var ElectricAgentsTenantRuntime = class {
4501
4849
  const fireAtRaw = value.fireAt;
4502
4850
  const producerId = value.producerId;
4503
4851
  const targetUrl = value.targetUrl;
4852
+ const senderUrl = typeof value.senderUrl === `string` ? value.senderUrl : ownerEntityUrl;
4504
4853
  if (typeof fireAtRaw !== `string` || typeof producerId !== `string` || typeof targetUrl !== `string`) {
4505
4854
  serverLog.warn(`[agent-server] invalid future_send manifest entry for ${ownerEntityUrl}/${manifestKey}`);
4506
4855
  return;
@@ -4512,7 +4861,7 @@ var ElectricAgentsTenantRuntime = class {
4512
4861
  }
4513
4862
  await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
4514
4863
  entityUrl: targetUrl,
4515
- from: typeof value.from === `string` ? value.from : ownerEntityUrl,
4864
+ from: senderUrl,
4516
4865
  payload: value.payload,
4517
4866
  key: `scheduled-${producerId}`,
4518
4867
  type: typeof value.messageType === `string` ? value.messageType : void 0,
@@ -4526,6 +4875,7 @@ var ElectricAgentsTenantRuntime = class {
4526
4875
  kind: `schedule`,
4527
4876
  scheduleType: `future_send`,
4528
4877
  targetUrl,
4878
+ senderUrl,
4529
4879
  fireAt: fireAt.toISOString(),
4530
4880
  producerId,
4531
4881
  status: `pending`
@@ -5374,6 +5724,7 @@ var AgentsHost = class {
5374
5724
  for (const runtime of runtimes) await this.startTenantRuntime(runtime);
5375
5725
  }
5376
5726
  async startTenantRuntime(runtime) {
5727
+ await runtime.manager.ensurePrincipalEntityType();
5377
5728
  if (this.rehydrateTenantOnStart) await runtime.rehydrateCronSchedules();
5378
5729
  if (this.startEntityBridgeManager) await this.entityProjector.loadTenantBridges(runtime.serviceId, runtime.registry);
5379
5730
  }
@@ -5391,6 +5742,7 @@ var AgentsHost = class {
5391
5742
  scheduler,
5392
5743
  entityBridgeManager: this.entityProjector.forTenant(serviceId, registry)
5393
5744
  });
5745
+ await runtime.manager.ensurePrincipalEntityType();
5394
5746
  return runtime;
5395
5747
  }
5396
5748
  createStreamClient(config) {
@@ -5573,30 +5925,6 @@ function validateParsedBody(schema, parsed) {
5573
5925
  };
5574
5926
  }
5575
5927
 
5576
- //#endregion
5577
- //#region src/utils/webhook-url.ts
5578
- function rewriteLoopbackWebhookUrl(value) {
5579
- if (!value) return void 0;
5580
- const rewriteTarget = process.env.ELECTRIC_AGENTS_REWRITE_LOOPBACK_WEBHOOKS_TO?.trim();
5581
- if (!rewriteTarget) return value;
5582
- const url = new URL(value);
5583
- if (!isLoopbackHostname(url.hostname)) return value;
5584
- if (rewriteTarget.includes(`://`)) {
5585
- const target = new URL(rewriteTarget);
5586
- url.protocol = target.protocol;
5587
- url.username = target.username;
5588
- url.password = target.password;
5589
- url.hostname = target.hostname;
5590
- url.port = target.port;
5591
- return url.toString();
5592
- }
5593
- url.host = rewriteTarget;
5594
- return url.toString();
5595
- }
5596
- function isLoopbackHostname(hostname) {
5597
- return hostname === `localhost` || hostname === `127.0.0.1` || hostname === `::1`;
5598
- }
5599
-
5600
5928
  //#endregion
5601
5929
  //#region src/routing/tenant-stream-paths.ts
5602
5930
  function withoutLeadingSlash(path$1) {
@@ -5956,122 +6284,6 @@ async function proxyElectric(request, ctx) {
5956
6284
  });
5957
6285
  }
5958
6286
 
5959
- //#endregion
5960
- //#region src/routing/dispatch-policy.ts
5961
- function subscriptionIdForDispatchTarget(target) {
5962
- if (target.subscription_id) return target.subscription_id;
5963
- if (target.type === `runner`) return `runner:${target.runnerId}`;
5964
- const digest = createHash(`sha256`).update(target.url).digest(`hex`);
5965
- return `webhook:${digest.slice(0, 16)}`;
5966
- }
5967
- function subscriptionIdForEntityDispatchTarget(target, entityUrl) {
5968
- const base = subscriptionIdForDispatchTarget(target);
5969
- if (!target.subscription_id) return base;
5970
- const digest = createHash(`sha256`).update(entityUrl).digest(`hex`);
5971
- return `${base}:${digest.slice(0, 16)}`;
5972
- }
5973
- async function resolveEffectiveDispatchPolicyForSpawn(ctx, typeName, opts) {
5974
- if (opts.dispatchPolicy) return opts.dispatchPolicy;
5975
- const entityType = await ctx.entityManager.registry.getEntityType(typeName);
5976
- if (opts.parent) {
5977
- const parent = await ctx.entityManager.registry.getEntity(opts.parent);
5978
- if (parent?.dispatch_policy) return applyTypeDefaultSubscriptionScope(parent.dispatch_policy, entityType?.default_dispatch_policy);
5979
- }
5980
- return entityType?.default_dispatch_policy;
5981
- }
5982
- async function resolveEffectiveDispatchPolicyForEntity(ctx, entity) {
5983
- if (entity.dispatch_policy) return entity.dispatch_policy;
5984
- const entityType = await ctx.entityManager.registry.getEntityType(entity.type);
5985
- return entityType?.default_dispatch_policy;
5986
- }
5987
- async function backfillEntityDispatchPolicy(ctx, entity) {
5988
- if (entity.dispatch_policy) return entity;
5989
- const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
5990
- if (!dispatchPolicy) return entity;
5991
- return await ctx.entityManager.registry.updateEntityDispatchPolicy(entity.url, dispatchPolicy) ?? {
5992
- ...entity,
5993
- dispatch_policy: dispatchPolicy
5994
- };
5995
- }
5996
- function applyTypeDefaultSubscriptionScope(policy, typeDefault) {
5997
- const target = policy.targets[0];
5998
- const defaultTarget = typeDefault?.targets[0];
5999
- if (!target || !defaultTarget?.subscription_id) return policy;
6000
- if (!sameDispatchDestination(target, defaultTarget)) return policy;
6001
- if (target.subscription_id === defaultTarget.subscription_id) return policy;
6002
- return { targets: [{
6003
- ...target,
6004
- subscription_id: defaultTarget.subscription_id
6005
- }] };
6006
- }
6007
- function sameDispatchDestination(a, b) {
6008
- if (a.type !== b.type) return false;
6009
- if (a.type === `runner` && b.type === `runner`) return a.runnerId === b.runnerId;
6010
- if (a.type === `webhook` && b.type === `webhook`) return a.url === b.url;
6011
- return false;
6012
- }
6013
- async function assertDispatchPolicyAllowed(ctx, policy) {
6014
- const target = policy?.targets[0];
6015
- if (!target || target.type !== `runner`) return;
6016
- const runner = await ctx.entityManager.registry.getRunner(target.runnerId);
6017
- if (!runner) throw new ElectricAgentsError(ErrCodeNotFound, `Runner "${target.runnerId}" not found`, 404);
6018
- if (!ctx.authenticatedUser) throw new ElectricAgentsError(ErrCodeUnauthorized, `Authentication is required for runner-targeted dispatch`, 401);
6019
- if (runner.owner_user_id !== ctx.authenticatedUser.userId) throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner dispatch requires the authenticated owner`, 403);
6020
- }
6021
- async function linkEntityDispatchSubscription(ctx, entity) {
6022
- const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
6023
- const target = dispatchPolicy?.targets[0];
6024
- if (!target) return;
6025
- await linkStreamToTargetSubscription(ctx, target, entity);
6026
- }
6027
- async function unlinkEntityDispatchSubscription(ctx, entity) {
6028
- const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
6029
- const target = dispatchPolicy?.targets[0];
6030
- if (!target) return;
6031
- const subscriptionId = subscriptionIdForEntityDispatchTarget(target, entity.url);
6032
- await ctx.streamClient.removeSubscriptionStream(subscriptionId, entity.streams.main).catch(() => {});
6033
- }
6034
- async function linkStreamToTargetSubscription(ctx, target, entity) {
6035
- const streamPath = entity.streams.main;
6036
- const subscriptionId = subscriptionIdForEntityDispatchTarget(target, entity.url);
6037
- const existing = await ctx.streamClient.getSubscription(subscriptionId);
6038
- if (target.type === `runner`) {
6039
- const runner = await ctx.entityManager.registry.getRunner(target.runnerId);
6040
- if (!runner) throw new ElectricAgentsError(ErrCodeNotFound, `Runner "${target.runnerId}" not found`, 404);
6041
- const wakeStream = runner.wake_stream || runnerWakeStream(target.runnerId);
6042
- await ctx.streamClient.ensure(wakeStream, { contentType: `application/json` });
6043
- if (!existing) {
6044
- await ctx.streamClient.putSubscription(subscriptionId, {
6045
- type: `pull-wake`,
6046
- streams: [streamPath],
6047
- wake_stream: wakeStream,
6048
- description: `Electric Agents runner ${target.runnerId}`
6049
- });
6050
- return;
6051
- }
6052
- await ctx.streamClient.addSubscriptionStreams(subscriptionId, [streamPath]);
6053
- return;
6054
- }
6055
- const webhookUrl = rewriteLoopbackWebhookUrl(target.url);
6056
- if (!webhookUrl) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Webhook dispatch target must include a valid URL`, 400);
6057
- const forwardUrl = appendPathToUrl(ctx.publicUrl, `/_electric/webhook-forward/${encodeURIComponent(subscriptionId)}`);
6058
- if (!existing) await ctx.streamClient.putSubscription(subscriptionId, {
6059
- type: `webhook`,
6060
- streams: [streamPath],
6061
- webhook: { url: forwardUrl },
6062
- description: `Electric Agents webhook ${subscriptionId}`
6063
- });
6064
- else await ctx.streamClient.addSubscriptionStreams(subscriptionId, [streamPath]);
6065
- await ctx.pgDb.insert(subscriptionWebhooks).values({
6066
- tenantId: ctx.service,
6067
- subscriptionId,
6068
- webhookUrl
6069
- }).onConflictDoUpdate({
6070
- target: [subscriptionWebhooks.tenantId, subscriptionWebhooks.subscriptionId],
6071
- set: { webhookUrl }
6072
- });
6073
- }
6074
-
6075
6287
  //#endregion
6076
6288
  //#region src/routing/entities-router.ts
6077
6289
  const stringRecordSchema = Type.Record(Type.String(), Type.String());
@@ -6104,11 +6316,33 @@ const spawnBodySchema = Type.Object({
6104
6316
  }))
6105
6317
  });
6106
6318
  const sendBodySchema = Type.Object({
6107
- from: Type.Optional(Type.String()),
6108
6319
  payload: Type.Optional(Type.Unknown()),
6109
6320
  key: Type.Optional(Type.String()),
6110
6321
  type: Type.Optional(Type.String()),
6111
- afterMs: Type.Optional(Type.Number())
6322
+ mode: Type.Optional(Type.Union([
6323
+ Type.Literal(`immediate`),
6324
+ Type.Literal(`queued`),
6325
+ Type.Literal(`paused`),
6326
+ Type.Literal(`steer`)
6327
+ ])),
6328
+ position: Type.Optional(Type.String()),
6329
+ afterMs: Type.Optional(Type.Number()),
6330
+ from: Type.Optional(Type.String())
6331
+ });
6332
+ const inboxMessageBodySchema = Type.Object({
6333
+ payload: Type.Optional(Type.Unknown()),
6334
+ position: Type.Optional(Type.String()),
6335
+ mode: Type.Optional(Type.Union([
6336
+ Type.Literal(`immediate`),
6337
+ Type.Literal(`queued`),
6338
+ Type.Literal(`paused`),
6339
+ Type.Literal(`steer`)
6340
+ ])),
6341
+ status: Type.Optional(Type.Union([
6342
+ Type.Literal(`pending`),
6343
+ Type.Literal(`processed`),
6344
+ Type.Literal(`cancelled`)
6345
+ ]))
6112
6346
  });
6113
6347
  const forkBodySchema = Type.Object({
6114
6348
  instance_id: Type.Optional(Type.String()),
@@ -6127,8 +6361,8 @@ const scheduleBodySchema = Type.Union([Type.Object({
6127
6361
  payload: Type.Unknown(),
6128
6362
  targetUrl: Type.Optional(Type.String()),
6129
6363
  fireAt: Type.String(),
6130
- from: Type.Optional(Type.String()),
6131
- messageType: Type.Optional(Type.String())
6364
+ messageType: Type.Optional(Type.String()),
6365
+ from: Type.Optional(Type.String())
6132
6366
  })]);
6133
6367
  const entitiesRegisterBodySchema = Type.Object({ tags: Type.Optional(stringRecordSchema) });
6134
6368
  const entitiesRouter = Router({ base: `/_electric/entities` });
@@ -6139,6 +6373,8 @@ entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, getEntity);
6139
6373
  entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity);
6140
6374
  entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, killEntity);
6141
6375
  entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), sendEntity);
6376
+ entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), updateInboxMessage);
6377
+ entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, deleteInboxMessage);
6142
6378
  entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), forkEntity);
6143
6379
  entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), setTag);
6144
6380
  entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, removeTag);
@@ -6147,6 +6383,11 @@ entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEn
6147
6383
  function entityUrlFromSegments(type, instanceId) {
6148
6384
  if (!type || !instanceId) return null;
6149
6385
  if (type.startsWith(`_`) || type.includes(`*`) || instanceId.includes(`*`)) return null;
6386
+ if (type === `principal`) try {
6387
+ return principalUrl(decodeURIComponent(instanceId));
6388
+ } catch {
6389
+ return null;
6390
+ }
6150
6391
  return `/${type}/${instanceId}`;
6151
6392
  }
6152
6393
  function firstQueryValue$1(value) {
@@ -6156,12 +6397,27 @@ function requireExistingEntityRoute(request) {
6156
6397
  if (!request.entityRoute) throw new Error(`existing entity middleware did not run`);
6157
6398
  return request.entityRoute;
6158
6399
  }
6400
+ function rejectPrincipalEntityMutation(request, action) {
6401
+ const { entity } = requireExistingEntityRoute(request);
6402
+ if (entity.type !== `principal`) return void 0;
6403
+ return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be ${action}`);
6404
+ }
6159
6405
  async function withExistingEntity(request, ctx) {
6160
6406
  const entityUrl = entityUrlFromSegments(request.params.type, request.params.instanceId);
6161
6407
  if (!entityUrl) return void 0;
6162
6408
  const entity = await ctx.entityManager.registry.getEntity(entityUrl);
6163
6409
  if (!entity) {
6164
6410
  const entityType = await ctx.entityManager.registry.getEntityType(request.params.type);
6411
+ if (request.params.type === `principal`) try {
6412
+ const materialized = await ctx.entityManager.ensurePrincipal(parsePrincipalKey(decodeURIComponent(request.params.instanceId)));
6413
+ request.entityRoute = {
6414
+ entityUrl,
6415
+ entity: materialized
6416
+ };
6417
+ return void 0;
6418
+ } catch (error) {
6419
+ return apiError(400, ErrCodeInvalidRequest, error instanceof Error ? error.message : `Invalid principal`);
6420
+ }
6165
6421
  if (entityType) return apiError(404, ErrCodeNotFound, `Entity not found at ${entityUrl}`);
6166
6422
  return apiError(404, ErrCodeUnknownEntityType, `Entity type "${request.params.type}" not found`);
6167
6423
  }
@@ -6173,6 +6429,7 @@ async function withExistingEntity(request, ctx) {
6173
6429
  }
6174
6430
  async function withSpawnableEntityType(request, ctx) {
6175
6431
  if (!entityUrlFromSegments(request.params.type, request.params.instanceId)) return void 0;
6432
+ if (request.params.type === `principal`) return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be spawned directly`);
6176
6433
  const entityType = await ctx.entityManager.registry.getEntityType(request.params.type);
6177
6434
  if (!entityType) return apiError(404, ErrCodeUnknownEntityType, `Entity type "${request.params.type}" not found`);
6178
6435
  return void 0;
@@ -6181,7 +6438,8 @@ async function listEntities({ query }, ctx) {
6181
6438
  const { entities: entities$1 } = await ctx.entityManager.registry.listEntities({
6182
6439
  type: firstQueryValue$1(query.type),
6183
6440
  status: firstQueryValue$1(query.status),
6184
- parent: firstQueryValue$1(query.parent)
6441
+ parent: firstQueryValue$1(query.parent),
6442
+ created_by: firstQueryValue$1(query.created_by)
6185
6443
  });
6186
6444
  return json(entities$1.map((entity) => toPublicEntity(entity)));
6187
6445
  }
@@ -6191,6 +6449,8 @@ async function registerEntitiesSource(request, ctx) {
6191
6449
  return json(result);
6192
6450
  }
6193
6451
  async function upsertSchedule(request, ctx) {
6452
+ const principalMutationError = rejectPrincipalEntityMutation(request, `scheduled`);
6453
+ if (principalMutationError) return principalMutationError;
6194
6454
  const parsed = routeBody(request);
6195
6455
  const { entityUrl } = requireExistingEntityRoute(request);
6196
6456
  const scheduleId = decodeURIComponent(request.params.scheduleId);
@@ -6206,12 +6466,13 @@ async function upsertSchedule(request, ctx) {
6206
6466
  return json(result);
6207
6467
  }
6208
6468
  if (parsed.scheduleType === `future_send`) {
6469
+ if (parsed.from !== void 0 && parsed.from !== ctx.principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from must match Electric-Principal`);
6209
6470
  const result = await ctx.entityManager.upsertFutureSendSchedule(entityUrl, {
6210
6471
  id: scheduleId,
6211
6472
  payload: parsed.payload,
6212
6473
  targetUrl: parsed.targetUrl,
6213
6474
  fireAt: parsed.fireAt,
6214
- from: parsed.from,
6475
+ senderUrl: ctx.principal.url,
6215
6476
  messageType: parsed.messageType
6216
6477
  });
6217
6478
  return json(result);
@@ -6219,11 +6480,15 @@ async function upsertSchedule(request, ctx) {
6219
6480
  throw new Error(`schedule schema accepted an unknown scheduleType`);
6220
6481
  }
6221
6482
  async function deleteSchedule(request, ctx) {
6483
+ const principalMutationError = rejectPrincipalEntityMutation(request, `unscheduled`);
6484
+ if (principalMutationError) return principalMutationError;
6222
6485
  const { entityUrl } = requireExistingEntityRoute(request);
6223
6486
  const result = await ctx.entityManager.deleteSchedule(entityUrl, { id: decodeURIComponent(request.params.scheduleId) });
6224
6487
  return json(result);
6225
6488
  }
6226
6489
  async function setTag(request, ctx) {
6490
+ const principalMutationError = rejectPrincipalEntityMutation(request, `tagged`);
6491
+ if (principalMutationError) return principalMutationError;
6227
6492
  const parsed = routeBody(request);
6228
6493
  const { entityUrl } = requireExistingEntityRoute(request);
6229
6494
  const token = writeTokenFromRequest(request);
@@ -6231,12 +6496,16 @@ async function setTag(request, ctx) {
6231
6496
  return json(toPublicEntity(updated));
6232
6497
  }
6233
6498
  async function removeTag(request, ctx) {
6499
+ const principalMutationError = rejectPrincipalEntityMutation(request, `untagged`);
6500
+ if (principalMutationError) return principalMutationError;
6234
6501
  const { entityUrl } = requireExistingEntityRoute(request);
6235
6502
  const token = writeTokenFromRequest(request);
6236
6503
  const updated = await ctx.entityManager.removeTag(entityUrl, decodeURIComponent(request.params.tagKey), token);
6237
6504
  return json(toPublicEntity(updated));
6238
6505
  }
6239
6506
  async function forkEntity(request, ctx) {
6507
+ const principalMutationError = rejectPrincipalEntityMutation(request, `forked`);
6508
+ if (principalMutationError) return principalMutationError;
6240
6509
  const parsed = routeBody(request);
6241
6510
  const { entityUrl, entity } = requireExistingEntityRoute(request);
6242
6511
  await assertDispatchPolicyAllowed(ctx, entity.dispatch_policy);
@@ -6252,27 +6521,47 @@ async function forkEntity(request, ctx) {
6252
6521
  }
6253
6522
  async function sendEntity(request, ctx) {
6254
6523
  const parsed = routeBody(request);
6524
+ const principal = ctx.principal;
6525
+ if (parsed.from !== void 0 && parsed.from !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from must match Electric-Principal`);
6526
+ await ctx.entityManager.ensurePrincipal(principal);
6255
6527
  const { entityUrl, entity } = requireExistingEntityRoute(request);
6256
6528
  if (!entity.dispatch_policy) {
6257
6529
  const updatedEntity = await backfillEntityDispatchPolicy(ctx, entity);
6258
6530
  await linkEntityDispatchSubscription(ctx, updatedEntity);
6259
6531
  }
6260
6532
  if (parsed.afterMs && parsed.afterMs > 0) await ctx.entityManager.enqueueDelayedSend(entityUrl, {
6261
- from: parsed.from,
6533
+ from: principal.url,
6262
6534
  payload: parsed.payload,
6263
6535
  key: parsed.key,
6264
- type: parsed.type
6536
+ type: parsed.type,
6537
+ mode: parsed.mode,
6538
+ position: parsed.position
6265
6539
  }, new Date(Date.now() + parsed.afterMs));
6266
6540
  else await ctx.entityManager.send(entityUrl, {
6267
- from: parsed.from,
6541
+ from: principal.url,
6268
6542
  payload: parsed.payload,
6269
6543
  key: parsed.key,
6270
- type: parsed.type
6544
+ type: parsed.type,
6545
+ mode: parsed.mode,
6546
+ position: parsed.position
6271
6547
  });
6272
6548
  return status(204);
6273
6549
  }
6550
+ async function updateInboxMessage(request, ctx) {
6551
+ const parsed = routeBody(request);
6552
+ const { entityUrl } = requireExistingEntityRoute(request);
6553
+ await ctx.entityManager.updateInboxMessage(entityUrl, decodeURIComponent(request.params.messageKey), parsed);
6554
+ return status(204);
6555
+ }
6556
+ async function deleteInboxMessage(request, ctx) {
6557
+ const { entityUrl } = requireExistingEntityRoute(request);
6558
+ await ctx.entityManager.deleteInboxMessage(entityUrl, decodeURIComponent(request.params.messageKey));
6559
+ return status(204);
6560
+ }
6274
6561
  async function spawnEntity(request, ctx) {
6275
6562
  const parsed = routeBody(request);
6563
+ const principal = ctx.principal;
6564
+ await ctx.entityManager.ensurePrincipal(principal);
6276
6565
  const dispatchPolicy = await resolveEffectiveDispatchPolicyForSpawn(ctx, request.params.type, {
6277
6566
  dispatchPolicy: parsed.dispatch_policy,
6278
6567
  parent: parsed.parent
@@ -6285,13 +6574,14 @@ async function spawnEntity(request, ctx) {
6285
6574
  parent: parsed.parent,
6286
6575
  dispatch_policy: dispatchPolicy,
6287
6576
  initialMessage: void 0,
6288
- wake: parsed.wake
6577
+ wake: parsed.wake,
6578
+ created_by: principal.url
6289
6579
  });
6290
- await linkEntityDispatchSubscription(ctx, entity);
6291
6580
  if (parsed.initialMessage !== void 0) await ctx.entityManager.send(entity.url, {
6292
- from: parsed.parent ?? `spawn`,
6581
+ from: principal.url,
6293
6582
  payload: parsed.initialMessage
6294
6583
  });
6584
+ await linkEntityDispatchSubscription(ctx, entity);
6295
6585
  return json({
6296
6586
  ...toPublicEntity(entity),
6297
6587
  txid: entity.txid
@@ -6307,6 +6597,8 @@ function headEntity() {
6307
6597
  return status(200);
6308
6598
  }
6309
6599
  async function killEntity(request, ctx) {
6600
+ const principalMutationError = rejectPrincipalEntityMutation(request, `killed`);
6601
+ if (principalMutationError) return principalMutationError;
6310
6602
  const { entityUrl, entity } = requireExistingEntityRoute(request);
6311
6603
  await unlinkEntityDispatchSubscription(ctx, entity);
6312
6604
  const result = await ctx.entityManager.kill(entityUrl);
@@ -6453,7 +6745,7 @@ function applyCors(response) {
6453
6745
  const headers = new Headers(response.headers);
6454
6746
  headers.set(`access-control-allow-origin`, `*`);
6455
6747
  headers.set(`access-control-allow-methods`, `GET, POST, PUT, PATCH, DELETE, OPTIONS`);
6456
- headers.set(`access-control-allow-headers`, `content-type, authorization, electric-claim-token, x-electric-asserted-email, x-electric-asserted-name, ngrok-skip-browser-warning`);
6748
+ headers.set(`access-control-allow-headers`, `content-type, authorization, electric-claim-token, ngrok-skip-browser-warning`);
6457
6749
  headers.set(`access-control-expose-headers`, `*`);
6458
6750
  return new Response(response.body, {
6459
6751
  status: response.status,
@@ -6533,9 +6825,9 @@ function firstQueryValue(value) {
6533
6825
  }
6534
6826
  async function registerRunner(request, ctx) {
6535
6827
  const parsed = routeBody(request);
6536
- const ownerUserId = parsed.owner_user_id ?? ctx.authenticatedUser?.userId;
6828
+ const ownerUserId = parsed.owner_user_id ?? ctx.principal?.key;
6537
6829
  if (!ownerUserId) throw new ElectricAgentsError(ErrCodeInvalidRequest, `owner_user_id is required when no authenticated user is present`, 400);
6538
- if (ctx.authenticatedUser && ownerUserId !== ctx.authenticatedUser.userId) throw new ElectricAgentsError(ErrCodeUnauthorized, `owner_user_id must match the authenticated user`, 403);
6830
+ if (ctx.principal && ownerUserId !== ctx.principal.key) throw new ElectricAgentsError(ErrCodeUnauthorized, `owner_user_id must match the authenticated user`, 403);
6539
6831
  const runner = await ctx.entityManager.registry.createRunner({
6540
6832
  id: parsed.id,
6541
6833
  ownerUserId,
@@ -6549,8 +6841,8 @@ async function registerRunner(request, ctx) {
6549
6841
  }
6550
6842
  async function listRunners(request, ctx) {
6551
6843
  const requestedOwner = firstQueryValue(request.query.owner_user_id);
6552
- if (ctx.authenticatedUser && requestedOwner && requestedOwner !== ctx.authenticatedUser.userId) throw new ElectricAgentsError(ErrCodeUnauthorized, `owner_user_id must match the authenticated user`, 403);
6553
- const runners$1 = await ctx.entityManager.registry.listRunners({ ownerUserId: ctx.authenticatedUser?.userId ?? requestedOwner });
6844
+ if (ctx.principal && requestedOwner && requestedOwner !== ctx.principal.key) throw new ElectricAgentsError(ErrCodeUnauthorized, `owner_user_id must match the authenticated user`, 403);
6845
+ const runners$1 = await ctx.entityManager.registry.listRunners({ ownerUserId: ctx.principal?.key ?? requestedOwner });
6554
6846
  return json(runners$1);
6555
6847
  }
6556
6848
  async function getRunner(request, ctx) {
@@ -6588,9 +6880,8 @@ async function setRunnerStatus(request, ctx, adminStatus) {
6588
6880
  }
6589
6881
  async function claimWake(request, ctx) {
6590
6882
  const runnerId = routeParam$1(request, `id`);
6591
- if (!ctx.authenticatedUser) throw new ElectricAgentsError(ErrCodeUnauthorized, `Authentication is required to claim runner work`, 401);
6592
6883
  const runner = await requireRunner(ctx, runnerId);
6593
- if (runner.owner_user_id !== ctx.authenticatedUser.userId) throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner claim requires the authenticated owner`, 403);
6884
+ if (ctx.principal && runner.owner_user_id !== ctx.principal.key) throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner claim requires the authenticated owner`, 403);
6594
6885
  if (runner.admin_status !== `enabled`) throw new ElectricAgentsError(ErrCodeNotRunning, `Runner is disabled`, 409);
6595
6886
  const parsed = routeBody(request);
6596
6887
  const expectedSubscriptionId = subscriptionIdForDispatchTarget({
@@ -6622,8 +6913,8 @@ async function requireRunner(ctx, runnerId) {
6622
6913
  return runner;
6623
6914
  }
6624
6915
  function assertRunnerOwnerIfAuthenticated(ctx, ownerUserId) {
6625
- if (!ctx.authenticatedUser) return;
6626
- if (ownerUserId === ctx.authenticatedUser.userId) return;
6916
+ if (!ctx.principal) return;
6917
+ if (ownerUserId === ctx.principal.key) return;
6627
6918
  throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner access requires the authenticated owner`, 403);
6628
6919
  }
6629
6920
  async function notificationFromClaim(ctx, input) {
@@ -6680,8 +6971,10 @@ async function notificationFromClaim(ctx, input) {
6680
6971
  url: entity.url,
6681
6972
  streams: entity.streams,
6682
6973
  tags: entity.tags,
6683
- spawnArgs: entity.spawn_args
6684
- }
6974
+ spawnArgs: entity.spawn_args,
6975
+ createdBy: entity.created_by
6976
+ },
6977
+ principal: principalFromCreatedBy(entity.created_by)
6685
6978
  };
6686
6979
  }
6687
6980