@electric-ax/agents-server 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -90,6 +90,7 @@ const entities = (0, drizzle_orm_pg_core.pgTable)(`entities`, {
90
90
  tagsIndex: (0, drizzle_orm_pg_core.text)(`tags_index`).array().notNull().default(drizzle_orm.sql`'{}'::text[]`),
91
91
  spawnArgs: (0, drizzle_orm_pg_core.jsonb)(`spawn_args`).default({}),
92
92
  parent: (0, drizzle_orm_pg_core.text)(`parent`),
93
+ createdBy: (0, drizzle_orm_pg_core.text)(`created_by`),
93
94
  typeRevision: (0, drizzle_orm_pg_core.integer)(`type_revision`),
94
95
  inboxSchemas: (0, drizzle_orm_pg_core.jsonb)(`inbox_schemas`),
95
96
  stateSchemas: (0, drizzle_orm_pg_core.jsonb)(`state_schemas`),
@@ -100,6 +101,7 @@ const entities = (0, drizzle_orm_pg_core.pgTable)(`entities`, {
100
101
  (0, drizzle_orm_pg_core.index)(`idx_entities_type`).on(table.tenantId, table.type),
101
102
  (0, drizzle_orm_pg_core.index)(`idx_entities_status`).on(table.tenantId, table.status),
102
103
  (0, drizzle_orm_pg_core.index)(`idx_entities_parent`).on(table.tenantId, table.parent),
104
+ (0, drizzle_orm_pg_core.index)(`idx_entities_created_by`).on(table.tenantId, table.createdBy),
103
105
  (0, drizzle_orm_pg_core.index)(`entities_tags_index_gin`).using(`gin`, table.tagsIndex),
104
106
  (0, drizzle_orm_pg_core.check)(`chk_entities_status`, drizzle_orm.sql`${table.status} IN ('spawning', 'running', 'idle', 'stopped')`)
105
107
  ]);
@@ -382,6 +384,7 @@ function toPublicEntity(entity) {
382
384
  tags: entity.tags,
383
385
  spawn_args: entity.spawn_args,
384
386
  parent: entity.parent,
387
+ created_by: entity.created_by,
385
388
  created_at: entity.created_at,
386
389
  updated_at: entity.updated_at
387
390
  };
@@ -621,6 +624,24 @@ var PostgresRegistry = class {
621
624
  }
622
625
  });
623
626
  }
627
+ async ensureEntityType(et) {
628
+ const existing = await this.getEntityType(et.name);
629
+ if (existing) return existing;
630
+ await this.db.insert(entityTypes).values({
631
+ tenantId: this.tenantId,
632
+ name: et.name,
633
+ description: et.description,
634
+ creationSchema: et.creation_schema ?? null,
635
+ inboxSchemas: et.inbox_schemas ?? null,
636
+ stateSchemas: et.state_schemas ?? null,
637
+ serveEndpoint: et.serve_endpoint ?? null,
638
+ defaultDispatchPolicy: et.default_dispatch_policy ?? null,
639
+ revision: et.revision,
640
+ createdAt: et.created_at,
641
+ updatedAt: et.updated_at
642
+ }).onConflictDoNothing();
643
+ return await this.getEntityType(et.name);
644
+ }
624
645
  async getEntityType(name) {
625
646
  const rows = await this.db.select().from(entityTypes).where(this.entityTypeWhere(name)).limit(1);
626
647
  if (rows.length === 0) return null;
@@ -660,6 +681,7 @@ var PostgresRegistry = class {
660
681
  tagsIndex: (0, __electric_ax_agents_runtime.buildTagsIndex)(entity.tags),
661
682
  spawnArgs: entity.spawn_args ?? {},
662
683
  parent: entity.parent ?? null,
684
+ createdBy: entity.created_by ?? null,
663
685
  typeRevision: entity.type_revision ?? null,
664
686
  inboxSchemas: entity.inbox_schemas ?? null,
665
687
  stateSchemas: entity.state_schemas ?? null,
@@ -705,6 +727,7 @@ var PostgresRegistry = class {
705
727
  if (filter?.type) conditions.push((0, drizzle_orm.eq)(entities.type, filter.type));
706
728
  if (filter?.status) conditions.push((0, drizzle_orm.eq)(entities.status, filter.status));
707
729
  if (filter?.parent) conditions.push((0, drizzle_orm.eq)(entities.parent, filter.parent));
730
+ if (filter?.created_by) conditions.push((0, drizzle_orm.eq)(entities.createdBy, filter.created_by));
708
731
  const whereClause = (0, drizzle_orm.and)(...conditions);
709
732
  const countResult = await this.db.select({ count: drizzle_orm.sql`count(*)` }).from(entities).where(whereClause);
710
733
  const total = Number(countResult[0].count);
@@ -991,6 +1014,7 @@ var PostgresRegistry = class {
991
1014
  tags: row.tags ?? {},
992
1015
  spawn_args: row.spawnArgs,
993
1016
  parent: row.parent ?? void 0,
1017
+ created_by: row.createdBy ?? void 0,
994
1018
  type_revision: row.typeRevision ?? void 0,
995
1019
  inbox_schemas: row.inboxSchemas,
996
1020
  state_schemas: row.stateSchemas,
@@ -1716,6 +1740,239 @@ function parseDispatchPolicy(value, label = `dispatch_policy`) {
1716
1740
  throw new Error(details ? `${label} does not match dispatch policy schema: ${details}` : `${label} does not match dispatch policy schema`);
1717
1741
  }
1718
1742
 
1743
+ //#endregion
1744
+ //#region src/utils/webhook-url.ts
1745
+ function rewriteLoopbackWebhookUrl(value) {
1746
+ if (!value) return void 0;
1747
+ const rewriteTarget = process.env.ELECTRIC_AGENTS_REWRITE_LOOPBACK_WEBHOOKS_TO?.trim();
1748
+ if (!rewriteTarget) return value;
1749
+ const url = new URL(value);
1750
+ if (!isLoopbackHostname(url.hostname)) return value;
1751
+ if (rewriteTarget.includes(`://`)) {
1752
+ const target = new URL(rewriteTarget);
1753
+ url.protocol = target.protocol;
1754
+ url.username = target.username;
1755
+ url.password = target.password;
1756
+ url.hostname = target.hostname;
1757
+ url.port = target.port;
1758
+ return url.toString();
1759
+ }
1760
+ url.host = rewriteTarget;
1761
+ return url.toString();
1762
+ }
1763
+ function isLoopbackHostname(hostname) {
1764
+ return hostname === `localhost` || hostname === `127.0.0.1` || hostname === `::1`;
1765
+ }
1766
+
1767
+ //#endregion
1768
+ //#region src/routing/dispatch-policy.ts
1769
+ function subscriptionIdForDispatchTarget(target) {
1770
+ if (target.subscription_id) return target.subscription_id;
1771
+ if (target.type === `runner`) return `runner:${target.runnerId}`;
1772
+ const digest = (0, node_crypto.createHash)(`sha256`).update(target.url).digest(`hex`);
1773
+ return `webhook:${digest.slice(0, 16)}`;
1774
+ }
1775
+ function subscriptionIdForEntityDispatchTarget(target, entityUrl) {
1776
+ const base = subscriptionIdForDispatchTarget(target);
1777
+ if (!target.subscription_id) return base;
1778
+ const digest = (0, node_crypto.createHash)(`sha256`).update(entityUrl).digest(`hex`);
1779
+ return `${base}:${digest.slice(0, 16)}`;
1780
+ }
1781
+ async function resolveEffectiveDispatchPolicyForSpawn(ctx, typeName, opts) {
1782
+ if (opts.dispatchPolicy) return opts.dispatchPolicy;
1783
+ const entityType = await ctx.entityManager.registry.getEntityType(typeName);
1784
+ if (opts.parent) {
1785
+ const parent = await ctx.entityManager.registry.getEntity(opts.parent);
1786
+ if (parent?.dispatch_policy) return applyTypeDefaultSubscriptionScope(parent.dispatch_policy, entityType?.default_dispatch_policy);
1787
+ }
1788
+ return entityType?.default_dispatch_policy;
1789
+ }
1790
+ async function resolveEffectiveDispatchPolicyForEntity(ctx, entity) {
1791
+ if (entity.dispatch_policy) return entity.dispatch_policy;
1792
+ const entityType = await ctx.entityManager.registry.getEntityType(entity.type);
1793
+ return entityType?.default_dispatch_policy;
1794
+ }
1795
+ async function backfillEntityDispatchPolicy(ctx, entity) {
1796
+ if (entity.dispatch_policy) return entity;
1797
+ const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
1798
+ if (!dispatchPolicy) return entity;
1799
+ return await ctx.entityManager.registry.updateEntityDispatchPolicy(entity.url, dispatchPolicy) ?? {
1800
+ ...entity,
1801
+ dispatch_policy: dispatchPolicy
1802
+ };
1803
+ }
1804
+ function applyTypeDefaultSubscriptionScope(policy, typeDefault) {
1805
+ const target = policy.targets[0];
1806
+ const defaultTarget = typeDefault?.targets[0];
1807
+ if (!target || !defaultTarget?.subscription_id) return policy;
1808
+ if (!sameDispatchDestination(target, defaultTarget)) return policy;
1809
+ if (target.subscription_id === defaultTarget.subscription_id) return policy;
1810
+ return { targets: [{
1811
+ ...target,
1812
+ subscription_id: defaultTarget.subscription_id
1813
+ }] };
1814
+ }
1815
+ function sameDispatchDestination(a, b) {
1816
+ if (a.type !== b.type) return false;
1817
+ if (a.type === `runner` && b.type === `runner`) return a.runnerId === b.runnerId;
1818
+ if (a.type === `webhook` && b.type === `webhook`) return a.url === b.url;
1819
+ return false;
1820
+ }
1821
+ async function assertDispatchPolicyAllowed(ctx, policy) {
1822
+ const target = policy?.targets[0];
1823
+ if (!target || target.type !== `runner`) return;
1824
+ const runner = await ctx.entityManager.registry.getRunner(target.runnerId);
1825
+ if (!runner) throw new ElectricAgentsError(ErrCodeNotFound, `Runner "${target.runnerId}" not found`, 404);
1826
+ if (ctx.principal && runner.owner_user_id !== ctx.principal.key) throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner dispatch requires the authenticated owner`, 403);
1827
+ }
1828
+ async function linkEntityDispatchSubscription(ctx, entity) {
1829
+ const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
1830
+ const target = dispatchPolicy?.targets[0];
1831
+ if (!target) return;
1832
+ await linkStreamToTargetSubscription(ctx, target, entity);
1833
+ }
1834
+ async function unlinkEntityDispatchSubscription(ctx, entity) {
1835
+ const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
1836
+ const target = dispatchPolicy?.targets[0];
1837
+ if (!target) return;
1838
+ const subscriptionId = subscriptionIdForEntityDispatchTarget(target, entity.url);
1839
+ await ctx.streamClient.removeSubscriptionStream(subscriptionId, entity.streams.main).catch((err) => {
1840
+ serverLog.warn(`[dispatch-policy] failed to remove stream from subscription`, {
1841
+ subscriptionId,
1842
+ stream: entity.streams.main
1843
+ }, err);
1844
+ });
1845
+ }
1846
+ async function linkStreamToTargetSubscription(ctx, target, entity) {
1847
+ const streamPath = entity.streams.main;
1848
+ const subscriptionId = subscriptionIdForEntityDispatchTarget(target, entity.url);
1849
+ const existing = await ctx.streamClient.getSubscription(subscriptionId);
1850
+ if (target.type === `runner`) {
1851
+ const runner = await ctx.entityManager.registry.getRunner(target.runnerId);
1852
+ if (!runner) throw new ElectricAgentsError(ErrCodeNotFound, `Runner "${target.runnerId}" not found`, 404);
1853
+ const wakeStream = runner.wake_stream || runnerWakeStream(target.runnerId);
1854
+ await ctx.streamClient.ensure(wakeStream, { contentType: `application/json` });
1855
+ if (!existing) {
1856
+ await ctx.streamClient.putSubscription(subscriptionId, {
1857
+ type: `pull-wake`,
1858
+ streams: [streamPath],
1859
+ wake_stream: wakeStream,
1860
+ description: `Electric Agents runner ${target.runnerId}`
1861
+ });
1862
+ return;
1863
+ }
1864
+ await ctx.streamClient.addSubscriptionStreams(subscriptionId, [streamPath]);
1865
+ return;
1866
+ }
1867
+ const webhookUrl = rewriteLoopbackWebhookUrl(target.url);
1868
+ if (!webhookUrl) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Webhook dispatch target must include a valid URL`, 400);
1869
+ const forwardUrl = (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/webhook-forward/${encodeURIComponent(subscriptionId)}`);
1870
+ if (!existing) await ctx.streamClient.putSubscription(subscriptionId, {
1871
+ type: `webhook`,
1872
+ streams: [streamPath],
1873
+ webhook: { url: forwardUrl },
1874
+ description: `Electric Agents webhook ${subscriptionId}`
1875
+ });
1876
+ else await ctx.streamClient.addSubscriptionStreams(subscriptionId, [streamPath]);
1877
+ await ctx.pgDb.insert(subscriptionWebhooks).values({
1878
+ tenantId: ctx.service,
1879
+ subscriptionId,
1880
+ webhookUrl
1881
+ }).onConflictDoUpdate({
1882
+ target: [subscriptionWebhooks.tenantId, subscriptionWebhooks.subscriptionId],
1883
+ set: { webhookUrl }
1884
+ });
1885
+ }
1886
+
1887
+ //#endregion
1888
+ //#region src/principal.ts
1889
+ const PRINCIPAL_KINDS = new Set([
1890
+ `user`,
1891
+ `agent`,
1892
+ `service`,
1893
+ `system`
1894
+ ]);
1895
+ function parsePrincipalKey(input) {
1896
+ const colon = input.indexOf(`:`);
1897
+ if (colon <= 0) throw new Error(`Invalid principal key`);
1898
+ const kind = input.slice(0, colon);
1899
+ const id = input.slice(colon + 1);
1900
+ if (!PRINCIPAL_KINDS.has(kind)) throw new Error(`Invalid principal kind`);
1901
+ if (!id || id.includes(`/`)) throw new Error(`Invalid principal id`);
1902
+ const key = `${kind}:${id}`;
1903
+ return {
1904
+ kind,
1905
+ id,
1906
+ key,
1907
+ url: `/principal/${encodeURIComponent(key)}`
1908
+ };
1909
+ }
1910
+ function principalUrl(key) {
1911
+ return parsePrincipalKey(key).url;
1912
+ }
1913
+ function principalKeyFromUrl(url) {
1914
+ if (!url.startsWith(`/principal/`)) return null;
1915
+ const segment = url.slice(`/principal/`.length);
1916
+ if (!segment || segment.includes(`/`)) return null;
1917
+ try {
1918
+ const key = decodeURIComponent(segment);
1919
+ return parsePrincipalKey(key).key;
1920
+ } catch {
1921
+ return null;
1922
+ }
1923
+ }
1924
+ const BUILT_IN_SYSTEM_PRINCIPAL_IDS = new Set([
1925
+ `framework`,
1926
+ `auth-sync`,
1927
+ `dev-local`
1928
+ ]);
1929
+ function isBuiltInSystemPrincipalUrl(url) {
1930
+ if (!url?.startsWith(`/principal/`)) return false;
1931
+ try {
1932
+ const key = principalKeyFromUrl(url);
1933
+ if (!key) return false;
1934
+ const principal = parsePrincipalKey(key);
1935
+ return principal.kind === `system` && BUILT_IN_SYSTEM_PRINCIPAL_IDS.has(principal.id);
1936
+ } catch {
1937
+ return false;
1938
+ }
1939
+ }
1940
+ function principalFromCreatedBy(createdBy) {
1941
+ if (!createdBy) return void 0;
1942
+ const key = principalKeyFromUrl(createdBy);
1943
+ if (!key) return {
1944
+ url: createdBy,
1945
+ key: null
1946
+ };
1947
+ const principal = parsePrincipalKey(key);
1948
+ return {
1949
+ url: principal.url,
1950
+ key: principal.key,
1951
+ kind: principal.kind,
1952
+ id: principal.id
1953
+ };
1954
+ }
1955
+ const principalIdentityStateSchema = __sinclair_typebox.Type.Object({
1956
+ kind: __sinclair_typebox.Type.Union([
1957
+ __sinclair_typebox.Type.Literal(`user`),
1958
+ __sinclair_typebox.Type.Literal(`agent`),
1959
+ __sinclair_typebox.Type.Literal(`service`),
1960
+ __sinclair_typebox.Type.Literal(`system`)
1961
+ ]),
1962
+ id: __sinclair_typebox.Type.String(),
1963
+ key: __sinclair_typebox.Type.String(),
1964
+ url: __sinclair_typebox.Type.String(),
1965
+ updated_at: __sinclair_typebox.Type.String(),
1966
+ display_name: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
1967
+ email: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
1968
+ avatar_url: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
1969
+ auth_provider: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
1970
+ auth_subject: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
1971
+ claims: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown())),
1972
+ created_at: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
1973
+ }, { additionalProperties: false });
1974
+ const principalUpdateIdentityMessageSchema = __sinclair_typebox.Type.Object({ identity: principalIdentityStateSchema }, { additionalProperties: false });
1975
+
1719
1976
  //#endregion
1720
1977
  //#region src/manifest-side-effects.ts
1721
1978
  function isRecord$1(value) {
@@ -1833,25 +2090,11 @@ function extractTraceContext(headers) {
1833
2090
 
1834
2091
  //#endregion
1835
2092
  //#region src/entity-manager.ts
2093
+ function createInitialQueuePosition(date) {
2094
+ return `${String(date.getTime()).padStart(16, `0`)}:a0`;
2095
+ }
1836
2096
  const DEFAULT_FORK_WAIT_TIMEOUT_MS = 12e4;
1837
2097
  const DEFAULT_FORK_WAIT_POLL_MS = 250;
1838
- function applyTypeDefaultSubscriptionScope$1(policy, typeDefault) {
1839
- const target = policy.targets[0];
1840
- const defaultTarget = typeDefault?.targets[0];
1841
- if (!target || !defaultTarget?.subscription_id) return policy;
1842
- if (!sameDispatchDestination$1(target, defaultTarget)) return policy;
1843
- if (target.subscription_id === defaultTarget.subscription_id) return policy;
1844
- return { targets: [{
1845
- ...target,
1846
- subscription_id: defaultTarget.subscription_id
1847
- }] };
1848
- }
1849
- function sameDispatchDestination$1(a, b) {
1850
- if (a.type !== b.type) return false;
1851
- if (a.type === `runner` && b.type === `runner`) return a.runnerId === b.runnerId;
1852
- if (a.type === `webhook` && b.type === `webhook`) return a.url === b.url;
1853
- return false;
1854
- }
1855
2098
  function sleep(ms) {
1856
2099
  return new Promise((resolve$1) => setTimeout(resolve$1, ms));
1857
2100
  }
@@ -1922,6 +2165,7 @@ var EntityManager = class {
1922
2165
  }
1923
2166
  async registerEntityType(req) {
1924
2167
  if (!req.name) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: name`, 400);
2168
+ if (req.name === `principal`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type "principal" is built in and cannot be registered or updated`, 400);
1925
2169
  if (req.name.startsWith(`_`)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type names starting with "_" are reserved`, 400);
1926
2170
  if (!req.description) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: description`, 400);
1927
2171
  this.validateSchema(req.creation_schema);
@@ -1948,10 +2192,63 @@ var EntityManager = class {
1948
2192
  return stored;
1949
2193
  }
1950
2194
  async deleteEntityType(name) {
2195
+ if (name === `principal`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type "principal" is built in and cannot be deleted`, 400);
1951
2196
  const existing = await this.registry.getEntityType(name);
1952
2197
  if (!existing) throw new ElectricAgentsError(ErrCodeNotFound, `Entity type "${name}" not found`, 404);
1953
2198
  await this.registry.deleteEntityType(name);
1954
2199
  }
2200
+ async ensurePrincipalEntityType() {
2201
+ const now = new Date().toISOString();
2202
+ return await this.registry.ensureEntityType({
2203
+ name: `principal`,
2204
+ description: `built-in principal entity`,
2205
+ inbox_schemas: { update_identity: principalUpdateIdentityMessageSchema },
2206
+ state_schemas: { identity: principalIdentityStateSchema },
2207
+ revision: 1,
2208
+ created_at: now,
2209
+ updated_at: now
2210
+ });
2211
+ }
2212
+ async ensurePrincipal(principal) {
2213
+ const existing = await this.registry.getEntity(principal.url);
2214
+ if (existing) return existing;
2215
+ await this.ensurePrincipalEntityType();
2216
+ try {
2217
+ const entity = await this.spawn(`principal`, {
2218
+ instance_id: principal.key,
2219
+ args: {
2220
+ kind: principal.kind,
2221
+ id: principal.id,
2222
+ key: principal.key
2223
+ },
2224
+ tags: {
2225
+ principal_kind: principal.kind,
2226
+ principal_id: principal.id
2227
+ },
2228
+ created_by: principal.url
2229
+ });
2230
+ const now = new Date().toISOString();
2231
+ await this.streamClient.append(entity.streams.main, this.encodeChangeEvent({
2232
+ type: `identity`,
2233
+ key: `self`,
2234
+ value: {
2235
+ kind: principal.kind,
2236
+ id: principal.id,
2237
+ key: principal.key,
2238
+ url: principal.url,
2239
+ created_at: now,
2240
+ updated_at: now
2241
+ }
2242
+ }));
2243
+ return entity;
2244
+ } catch (error) {
2245
+ if (error instanceof ElectricAgentsError && error.code === ErrCodeDuplicateURL) {
2246
+ const raced = await this.registry.getEntity(principal.url);
2247
+ if (raced) return raced;
2248
+ }
2249
+ throw error;
2250
+ }
2251
+ }
1955
2252
  /**
1956
2253
  * Spawn a new entity of the given type with durable streams.
1957
2254
  */
@@ -1967,6 +2264,7 @@ var EntityManager = class {
1967
2264
  });
1968
2265
  }
1969
2266
  async spawnInner(typeName, req) {
2267
+ 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);
1970
2268
  if (typeName.startsWith(`_`)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type names starting with "_" are reserved`, 400);
1971
2269
  const entityType = await this.registry.getEntityType(typeName);
1972
2270
  if (!entityType) throw new ElectricAgentsError(ErrCodeUnknownEntityType, `Entity type "${typeName}" not found`, 404);
@@ -1978,7 +2276,7 @@ var EntityManager = class {
1978
2276
  const instanceId = req.instance_id || (0, node_crypto.randomUUID)();
1979
2277
  if (instanceId.includes(`/`)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `instance_id must not contain forward slashes`, 400);
1980
2278
  const writeToken = (0, node_crypto.randomUUID)();
1981
- const entityURL = `/${typeName}/${instanceId}`;
2279
+ const entityURL = typeName === `principal` ? principalUrl(instanceId) : `/${typeName}/${instanceId}`;
1982
2280
  const mainPath = `${entityURL}/main`;
1983
2281
  const errorPath = `${entityURL}/error`;
1984
2282
  const subscriptionId = `${typeName}-handler`;
@@ -1990,7 +2288,7 @@ var EntityManager = class {
1990
2288
  parentEntity = await this.registry.getEntity(req.parent);
1991
2289
  if (!parentEntity) throw new ElectricAgentsError(ErrCodeNotFound, `Parent entity "${req.parent}" not found`, 404);
1992
2290
  }
1993
- 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;
2291
+ 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;
1994
2292
  const now = Date.now();
1995
2293
  const entityData = {
1996
2294
  type: typeName,
@@ -2009,6 +2307,7 @@ var EntityManager = class {
2009
2307
  inbox_schemas: entityType.inbox_schemas,
2010
2308
  state_schemas: entityType.state_schemas,
2011
2309
  created_at: now,
2310
+ created_by: req.created_by ?? parentEntity?.created_by,
2012
2311
  updated_at: now
2013
2312
  };
2014
2313
  if (req.parent) entityData.parent = req.parent;
@@ -2038,7 +2337,7 @@ var EntityManager = class {
2038
2337
  const inboxEvent = __electric_ax_agents_runtime.entityStateSchema.inbox.insert({
2039
2338
  key: `msg-in-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
2040
2339
  value: {
2041
- from: req.parent ?? `spawn`,
2340
+ from: req.created_by ?? req.parent ?? `spawn`,
2042
2341
  payload: req.initialMessage,
2043
2342
  timestamp: msgNow
2044
2343
  }
@@ -2581,10 +2880,10 @@ var EntityManager = class {
2581
2880
  changed = true;
2582
2881
  }
2583
2882
  }
2584
- if (typeof next.from === `string`) {
2585
- const forkFrom = entityUrlMap.get(next.from);
2586
- if (forkFrom) {
2587
- next.from = forkFrom;
2883
+ if (typeof next.senderUrl === `string`) {
2884
+ const forkSender = entityUrlMap.get(next.senderUrl);
2885
+ if (forkSender) {
2886
+ next.senderUrl = forkSender;
2588
2887
  changed = true;
2589
2888
  }
2590
2889
  }
@@ -2650,6 +2949,7 @@ var EntityManager = class {
2650
2949
  const fireAtRaw = manifest.fireAt;
2651
2950
  const producerId = manifest.producerId;
2652
2951
  const targetUrl = manifest.targetUrl;
2952
+ const senderUrl = typeof manifest.senderUrl === `string` ? manifest.senderUrl : ownerEntityUrl;
2653
2953
  if (typeof fireAtRaw !== `string` || typeof producerId !== `string` || typeof targetUrl !== `string`) {
2654
2954
  serverLog.warn(`[agent-server] invalid forked future_send manifest entry for ${ownerEntityUrl}/${manifestKey}`);
2655
2955
  return;
@@ -2661,7 +2961,7 @@ var EntityManager = class {
2661
2961
  }
2662
2962
  await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
2663
2963
  entityUrl: targetUrl,
2664
- from: typeof manifest.from === `string` ? manifest.from : ownerEntityUrl,
2964
+ from: senderUrl,
2665
2965
  payload: manifest.payload,
2666
2966
  key: `scheduled-${producerId}`,
2667
2967
  type: typeof manifest.messageType === `string` ? manifest.messageType : void 0,
@@ -2675,6 +2975,7 @@ var EntityManager = class {
2675
2975
  kind: `schedule`,
2676
2976
  scheduleType: `future_send`,
2677
2977
  targetUrl,
2978
+ senderUrl,
2678
2979
  fireAt: fireAt.toISOString(),
2679
2980
  producerId,
2680
2981
  status: `pending`
@@ -2702,9 +3003,14 @@ var EntityManager = class {
2702
3003
  const value = {
2703
3004
  from: req.from,
2704
3005
  payload: req.payload,
2705
- timestamp: now
3006
+ timestamp: now,
3007
+ mode: req.mode ?? `immediate`,
3008
+ status: req.mode === `queued` || req.mode === `paused` ? `pending` : `processed`
2706
3009
  };
2707
3010
  if (req.type) value.message_type = req.type;
3011
+ if (req.position) value.position = req.position;
3012
+ else if (value.mode === `queued` || value.mode === `paused`) value.position = createInitialQueuePosition(new Date(now));
3013
+ if (value.status === `processed`) value.processed_at = now;
2708
3014
  const envelope = __electric_ax_agents_runtime.entityStateSchema.inbox.insert({
2709
3015
  key,
2710
3016
  value
@@ -2716,11 +3022,47 @@ var EntityManager = class {
2716
3022
  return;
2717
3023
  }
2718
3024
  await this.streamClient.append(entity.streams.main, encoded);
3025
+ if (entity.type === `principal` && req.type === `update_identity`) {
3026
+ const identity = req.payload?.identity;
3027
+ await this.streamClient.append(entity.streams.main, this.encodeChangeEvent({
3028
+ type: `identity`,
3029
+ key: `self`,
3030
+ value: identity
3031
+ }));
3032
+ }
2719
3033
  } catch (err) {
2720
3034
  if (this.isClosedStreamError(err)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409);
2721
3035
  throw err;
2722
3036
  }
2723
3037
  }
3038
+ async updateInboxMessage(entityUrl, key, req) {
3039
+ const entity = await this.registry.getEntity(entityUrl);
3040
+ if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
3041
+ if (entity.status === `stopped`) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409);
3042
+ const now = new Date().toISOString();
3043
+ const value = {};
3044
+ if (`payload` in req) value.payload = req.payload;
3045
+ if (req.position !== void 0) value.position = req.position;
3046
+ if (req.mode !== void 0) value.mode = req.mode;
3047
+ if (req.status !== void 0) {
3048
+ value.status = req.status;
3049
+ if (req.status === `processed`) value.processed_at = now;
3050
+ if (req.status === `cancelled`) value.cancelled_at = now;
3051
+ }
3052
+ if (Object.keys(value).length === 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `No inbox fields to update`, 400);
3053
+ const envelope = __electric_ax_agents_runtime.entityStateSchema.inbox.update({
3054
+ key,
3055
+ value
3056
+ });
3057
+ await this.streamClient.append(entity.streams.main, this.encodeChangeEvent(envelope));
3058
+ }
3059
+ async deleteInboxMessage(entityUrl, key) {
3060
+ const entity = await this.registry.getEntity(entityUrl);
3061
+ if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
3062
+ if (entity.status === `stopped`) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409);
3063
+ const envelope = __electric_ax_agents_runtime.entityStateSchema.inbox.delete({ key });
3064
+ await this.streamClient.append(entity.streams.main, this.encodeChangeEvent(envelope));
3065
+ }
2724
3066
  async setTag(entityUrl, key, req, token) {
2725
3067
  const entity = await this.registry.getEntity(entityUrl);
2726
3068
  if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
@@ -2806,7 +3148,7 @@ var EntityManager = class {
2806
3148
  async upsertFutureSendSchedule(ownerEntityUrl, req) {
2807
3149
  if (!this.scheduler) throw new Error(`Scheduler not configured`);
2808
3150
  const targetUrl = req.targetUrl ?? ownerEntityUrl;
2809
- const from = req.from ?? ownerEntityUrl;
3151
+ const from = req.senderUrl ?? ownerEntityUrl;
2810
3152
  const fireAt = new Date(req.fireAt);
2811
3153
  if (Number.isNaN(fireAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid fireAt timestamp: ${req.fireAt}`, 400);
2812
3154
  await this.validateSendRequest(targetUrl, {
@@ -2834,9 +3176,9 @@ var EntityManager = class {
2834
3176
  scheduleType: `future_send`,
2835
3177
  fireAt: fireAt.toISOString(),
2836
3178
  targetUrl,
3179
+ senderUrl: from,
2837
3180
  payload: req.payload,
2838
3181
  producerId,
2839
- ...req.from ? { from: req.from } : {},
2840
3182
  ...req.messageType ? { messageType: req.messageType } : {},
2841
3183
  status: `pending`
2842
3184
  }
@@ -2850,9 +3192,9 @@ var EntityManager = class {
2850
3192
  scheduleType: `future_send`,
2851
3193
  fireAt: fireAt.toISOString(),
2852
3194
  targetUrl,
3195
+ senderUrl: from,
2853
3196
  payload: req.payload,
2854
3197
  producerId,
2855
- ...req.from ? { from: req.from } : {},
2856
3198
  ...req.messageType ? { messageType: req.messageType } : {},
2857
3199
  status: `pending`
2858
3200
  }, { txid });
@@ -2890,7 +3232,9 @@ var EntityManager = class {
2890
3232
  from: req.from,
2891
3233
  payload: req.payload,
2892
3234
  key: req.key,
2893
- type: req.type
3235
+ type: req.type,
3236
+ mode: req.mode,
3237
+ position: req.position
2894
3238
  }, fireAt);
2895
3239
  }
2896
3240
  /**
@@ -3040,6 +3384,7 @@ var EntityManager = class {
3040
3384
  * Add new input/output schema keys to an entity type directly in Postgres.
3041
3385
  */
3042
3386
  async amendSchemas(typeName, schemas) {
3387
+ if (typeName === `principal`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type "principal" is built in and cannot be amended`, 400);
3043
3388
  this.validateSchemaMap(schemas.inbox_schemas);
3044
3389
  this.validateSchemaMap(schemas.state_schemas);
3045
3390
  const existing = await this.registry.getEntityType(typeName);
@@ -3089,9 +3434,11 @@ var EntityManager = class {
3089
3434
  url: entity.url,
3090
3435
  streams: entity.streams,
3091
3436
  tags: entity.tags,
3092
- spawnArgs: entity.spawn_args
3437
+ spawnArgs: entity.spawn_args,
3438
+ createdBy: entity.created_by
3093
3439
  },
3094
- triggerEvent: `message_received`
3440
+ principal: principalFromCreatedBy(entity.created_by),
3441
+ triggerEvent: `inbox`
3095
3442
  };
3096
3443
  }
3097
3444
  validateSchema(schema) {
@@ -3131,6 +3478,7 @@ var EntityManager = class {
3131
3478
  }
3132
3479
  }
3133
3480
  if (!req.from) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: from`, 400);
3481
+ 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);
3134
3482
  if (req.payload === void 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: payload`, 400);
3135
3483
  return entity;
3136
3484
  }
@@ -4530,6 +4878,7 @@ var ElectricAgentsTenantRuntime = class {
4530
4878
  const fireAtRaw = value.fireAt;
4531
4879
  const producerId = value.producerId;
4532
4880
  const targetUrl = value.targetUrl;
4881
+ const senderUrl = typeof value.senderUrl === `string` ? value.senderUrl : ownerEntityUrl;
4533
4882
  if (typeof fireAtRaw !== `string` || typeof producerId !== `string` || typeof targetUrl !== `string`) {
4534
4883
  serverLog.warn(`[agent-server] invalid future_send manifest entry for ${ownerEntityUrl}/${manifestKey}`);
4535
4884
  return;
@@ -4541,7 +4890,7 @@ var ElectricAgentsTenantRuntime = class {
4541
4890
  }
4542
4891
  await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
4543
4892
  entityUrl: targetUrl,
4544
- from: typeof value.from === `string` ? value.from : ownerEntityUrl,
4893
+ from: senderUrl,
4545
4894
  payload: value.payload,
4546
4895
  key: `scheduled-${producerId}`,
4547
4896
  type: typeof value.messageType === `string` ? value.messageType : void 0,
@@ -4555,6 +4904,7 @@ var ElectricAgentsTenantRuntime = class {
4555
4904
  kind: `schedule`,
4556
4905
  scheduleType: `future_send`,
4557
4906
  targetUrl,
4907
+ senderUrl,
4558
4908
  fireAt: fireAt.toISOString(),
4559
4909
  producerId,
4560
4910
  status: `pending`
@@ -5403,6 +5753,7 @@ var AgentsHost = class {
5403
5753
  for (const runtime of runtimes) await this.startTenantRuntime(runtime);
5404
5754
  }
5405
5755
  async startTenantRuntime(runtime) {
5756
+ await runtime.manager.ensurePrincipalEntityType();
5406
5757
  if (this.rehydrateTenantOnStart) await runtime.rehydrateCronSchedules();
5407
5758
  if (this.startEntityBridgeManager) await this.entityProjector.loadTenantBridges(runtime.serviceId, runtime.registry);
5408
5759
  }
@@ -5420,6 +5771,7 @@ var AgentsHost = class {
5420
5771
  scheduler,
5421
5772
  entityBridgeManager: this.entityProjector.forTenant(serviceId, registry)
5422
5773
  });
5774
+ await runtime.manager.ensurePrincipalEntityType();
5423
5775
  return runtime;
5424
5776
  }
5425
5777
  createStreamClient(config) {
@@ -5602,30 +5954,6 @@ function validateParsedBody(schema, parsed) {
5602
5954
  };
5603
5955
  }
5604
5956
 
5605
- //#endregion
5606
- //#region src/utils/webhook-url.ts
5607
- function rewriteLoopbackWebhookUrl(value) {
5608
- if (!value) return void 0;
5609
- const rewriteTarget = process.env.ELECTRIC_AGENTS_REWRITE_LOOPBACK_WEBHOOKS_TO?.trim();
5610
- if (!rewriteTarget) return value;
5611
- const url = new URL(value);
5612
- if (!isLoopbackHostname(url.hostname)) return value;
5613
- if (rewriteTarget.includes(`://`)) {
5614
- const target = new URL(rewriteTarget);
5615
- url.protocol = target.protocol;
5616
- url.username = target.username;
5617
- url.password = target.password;
5618
- url.hostname = target.hostname;
5619
- url.port = target.port;
5620
- return url.toString();
5621
- }
5622
- url.host = rewriteTarget;
5623
- return url.toString();
5624
- }
5625
- function isLoopbackHostname(hostname) {
5626
- return hostname === `localhost` || hostname === `127.0.0.1` || hostname === `::1`;
5627
- }
5628
-
5629
5957
  //#endregion
5630
5958
  //#region src/routing/tenant-stream-paths.ts
5631
5959
  function withoutLeadingSlash(path$2) {
@@ -5985,122 +6313,6 @@ async function proxyElectric(request, ctx) {
5985
6313
  });
5986
6314
  }
5987
6315
 
5988
- //#endregion
5989
- //#region src/routing/dispatch-policy.ts
5990
- function subscriptionIdForDispatchTarget(target) {
5991
- if (target.subscription_id) return target.subscription_id;
5992
- if (target.type === `runner`) return `runner:${target.runnerId}`;
5993
- const digest = (0, node_crypto.createHash)(`sha256`).update(target.url).digest(`hex`);
5994
- return `webhook:${digest.slice(0, 16)}`;
5995
- }
5996
- function subscriptionIdForEntityDispatchTarget(target, entityUrl) {
5997
- const base = subscriptionIdForDispatchTarget(target);
5998
- if (!target.subscription_id) return base;
5999
- const digest = (0, node_crypto.createHash)(`sha256`).update(entityUrl).digest(`hex`);
6000
- return `${base}:${digest.slice(0, 16)}`;
6001
- }
6002
- async function resolveEffectiveDispatchPolicyForSpawn(ctx, typeName, opts) {
6003
- if (opts.dispatchPolicy) return opts.dispatchPolicy;
6004
- const entityType = await ctx.entityManager.registry.getEntityType(typeName);
6005
- if (opts.parent) {
6006
- const parent = await ctx.entityManager.registry.getEntity(opts.parent);
6007
- if (parent?.dispatch_policy) return applyTypeDefaultSubscriptionScope(parent.dispatch_policy, entityType?.default_dispatch_policy);
6008
- }
6009
- return entityType?.default_dispatch_policy;
6010
- }
6011
- async function resolveEffectiveDispatchPolicyForEntity(ctx, entity) {
6012
- if (entity.dispatch_policy) return entity.dispatch_policy;
6013
- const entityType = await ctx.entityManager.registry.getEntityType(entity.type);
6014
- return entityType?.default_dispatch_policy;
6015
- }
6016
- async function backfillEntityDispatchPolicy(ctx, entity) {
6017
- if (entity.dispatch_policy) return entity;
6018
- const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
6019
- if (!dispatchPolicy) return entity;
6020
- return await ctx.entityManager.registry.updateEntityDispatchPolicy(entity.url, dispatchPolicy) ?? {
6021
- ...entity,
6022
- dispatch_policy: dispatchPolicy
6023
- };
6024
- }
6025
- function applyTypeDefaultSubscriptionScope(policy, typeDefault) {
6026
- const target = policy.targets[0];
6027
- const defaultTarget = typeDefault?.targets[0];
6028
- if (!target || !defaultTarget?.subscription_id) return policy;
6029
- if (!sameDispatchDestination(target, defaultTarget)) return policy;
6030
- if (target.subscription_id === defaultTarget.subscription_id) return policy;
6031
- return { targets: [{
6032
- ...target,
6033
- subscription_id: defaultTarget.subscription_id
6034
- }] };
6035
- }
6036
- function sameDispatchDestination(a, b) {
6037
- if (a.type !== b.type) return false;
6038
- if (a.type === `runner` && b.type === `runner`) return a.runnerId === b.runnerId;
6039
- if (a.type === `webhook` && b.type === `webhook`) return a.url === b.url;
6040
- return false;
6041
- }
6042
- async function assertDispatchPolicyAllowed(ctx, policy) {
6043
- const target = policy?.targets[0];
6044
- if (!target || target.type !== `runner`) return;
6045
- const runner = await ctx.entityManager.registry.getRunner(target.runnerId);
6046
- if (!runner) throw new ElectricAgentsError(ErrCodeNotFound, `Runner "${target.runnerId}" not found`, 404);
6047
- if (!ctx.authenticatedUser) throw new ElectricAgentsError(ErrCodeUnauthorized, `Authentication is required for runner-targeted dispatch`, 401);
6048
- if (runner.owner_user_id !== ctx.authenticatedUser.userId) throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner dispatch requires the authenticated owner`, 403);
6049
- }
6050
- async function linkEntityDispatchSubscription(ctx, entity) {
6051
- const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
6052
- const target = dispatchPolicy?.targets[0];
6053
- if (!target) return;
6054
- await linkStreamToTargetSubscription(ctx, target, entity);
6055
- }
6056
- async function unlinkEntityDispatchSubscription(ctx, entity) {
6057
- const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
6058
- const target = dispatchPolicy?.targets[0];
6059
- if (!target) return;
6060
- const subscriptionId = subscriptionIdForEntityDispatchTarget(target, entity.url);
6061
- await ctx.streamClient.removeSubscriptionStream(subscriptionId, entity.streams.main).catch(() => {});
6062
- }
6063
- async function linkStreamToTargetSubscription(ctx, target, entity) {
6064
- const streamPath = entity.streams.main;
6065
- const subscriptionId = subscriptionIdForEntityDispatchTarget(target, entity.url);
6066
- const existing = await ctx.streamClient.getSubscription(subscriptionId);
6067
- if (target.type === `runner`) {
6068
- const runner = await ctx.entityManager.registry.getRunner(target.runnerId);
6069
- if (!runner) throw new ElectricAgentsError(ErrCodeNotFound, `Runner "${target.runnerId}" not found`, 404);
6070
- const wakeStream = runner.wake_stream || runnerWakeStream(target.runnerId);
6071
- await ctx.streamClient.ensure(wakeStream, { contentType: `application/json` });
6072
- if (!existing) {
6073
- await ctx.streamClient.putSubscription(subscriptionId, {
6074
- type: `pull-wake`,
6075
- streams: [streamPath],
6076
- wake_stream: wakeStream,
6077
- description: `Electric Agents runner ${target.runnerId}`
6078
- });
6079
- return;
6080
- }
6081
- await ctx.streamClient.addSubscriptionStreams(subscriptionId, [streamPath]);
6082
- return;
6083
- }
6084
- const webhookUrl = rewriteLoopbackWebhookUrl(target.url);
6085
- if (!webhookUrl) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Webhook dispatch target must include a valid URL`, 400);
6086
- const forwardUrl = (0, __electric_ax_agents_runtime.appendPathToUrl)(ctx.publicUrl, `/_electric/webhook-forward/${encodeURIComponent(subscriptionId)}`);
6087
- if (!existing) await ctx.streamClient.putSubscription(subscriptionId, {
6088
- type: `webhook`,
6089
- streams: [streamPath],
6090
- webhook: { url: forwardUrl },
6091
- description: `Electric Agents webhook ${subscriptionId}`
6092
- });
6093
- else await ctx.streamClient.addSubscriptionStreams(subscriptionId, [streamPath]);
6094
- await ctx.pgDb.insert(subscriptionWebhooks).values({
6095
- tenantId: ctx.service,
6096
- subscriptionId,
6097
- webhookUrl
6098
- }).onConflictDoUpdate({
6099
- target: [subscriptionWebhooks.tenantId, subscriptionWebhooks.subscriptionId],
6100
- set: { webhookUrl }
6101
- });
6102
- }
6103
-
6104
6316
  //#endregion
6105
6317
  //#region src/routing/entities-router.ts
6106
6318
  const stringRecordSchema = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.String());
@@ -6133,11 +6345,33 @@ const spawnBodySchema = __sinclair_typebox.Type.Object({
6133
6345
  }))
6134
6346
  });
6135
6347
  const sendBodySchema = __sinclair_typebox.Type.Object({
6136
- from: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
6137
6348
  payload: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Unknown()),
6138
6349
  key: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
6139
6350
  type: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
6140
- afterMs: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number())
6351
+ mode: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Union([
6352
+ __sinclair_typebox.Type.Literal(`immediate`),
6353
+ __sinclair_typebox.Type.Literal(`queued`),
6354
+ __sinclair_typebox.Type.Literal(`paused`),
6355
+ __sinclair_typebox.Type.Literal(`steer`)
6356
+ ])),
6357
+ position: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
6358
+ afterMs: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Number()),
6359
+ from: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
6360
+ });
6361
+ const inboxMessageBodySchema = __sinclair_typebox.Type.Object({
6362
+ payload: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Unknown()),
6363
+ position: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
6364
+ mode: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Union([
6365
+ __sinclair_typebox.Type.Literal(`immediate`),
6366
+ __sinclair_typebox.Type.Literal(`queued`),
6367
+ __sinclair_typebox.Type.Literal(`paused`),
6368
+ __sinclair_typebox.Type.Literal(`steer`)
6369
+ ])),
6370
+ status: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Union([
6371
+ __sinclair_typebox.Type.Literal(`pending`),
6372
+ __sinclair_typebox.Type.Literal(`processed`),
6373
+ __sinclair_typebox.Type.Literal(`cancelled`)
6374
+ ]))
6141
6375
  });
6142
6376
  const forkBodySchema = __sinclair_typebox.Type.Object({
6143
6377
  instance_id: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
@@ -6156,8 +6390,8 @@ const scheduleBodySchema = __sinclair_typebox.Type.Union([__sinclair_typebox.Typ
6156
6390
  payload: __sinclair_typebox.Type.Unknown(),
6157
6391
  targetUrl: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
6158
6392
  fireAt: __sinclair_typebox.Type.String(),
6159
- from: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
6160
- messageType: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
6393
+ messageType: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
6394
+ from: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
6161
6395
  })]);
6162
6396
  const entitiesRegisterBodySchema = __sinclair_typebox.Type.Object({ tags: __sinclair_typebox.Type.Optional(stringRecordSchema) });
6163
6397
  const entitiesRouter = (0, itty_router.Router)({ base: `/_electric/entities` });
@@ -6168,6 +6402,8 @@ entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, getEntity);
6168
6402
  entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity);
6169
6403
  entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, killEntity);
6170
6404
  entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), sendEntity);
6405
+ entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), updateInboxMessage);
6406
+ entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, deleteInboxMessage);
6171
6407
  entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), forkEntity);
6172
6408
  entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), setTag);
6173
6409
  entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, removeTag);
@@ -6176,6 +6412,11 @@ entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEn
6176
6412
  function entityUrlFromSegments(type, instanceId) {
6177
6413
  if (!type || !instanceId) return null;
6178
6414
  if (type.startsWith(`_`) || type.includes(`*`) || instanceId.includes(`*`)) return null;
6415
+ if (type === `principal`) try {
6416
+ return principalUrl(decodeURIComponent(instanceId));
6417
+ } catch {
6418
+ return null;
6419
+ }
6179
6420
  return `/${type}/${instanceId}`;
6180
6421
  }
6181
6422
  function firstQueryValue$1(value) {
@@ -6185,12 +6426,27 @@ function requireExistingEntityRoute(request) {
6185
6426
  if (!request.entityRoute) throw new Error(`existing entity middleware did not run`);
6186
6427
  return request.entityRoute;
6187
6428
  }
6429
+ function rejectPrincipalEntityMutation(request, action) {
6430
+ const { entity } = requireExistingEntityRoute(request);
6431
+ if (entity.type !== `principal`) return void 0;
6432
+ return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be ${action}`);
6433
+ }
6188
6434
  async function withExistingEntity(request, ctx) {
6189
6435
  const entityUrl = entityUrlFromSegments(request.params.type, request.params.instanceId);
6190
6436
  if (!entityUrl) return void 0;
6191
6437
  const entity = await ctx.entityManager.registry.getEntity(entityUrl);
6192
6438
  if (!entity) {
6193
6439
  const entityType = await ctx.entityManager.registry.getEntityType(request.params.type);
6440
+ if (request.params.type === `principal`) try {
6441
+ const materialized = await ctx.entityManager.ensurePrincipal(parsePrincipalKey(decodeURIComponent(request.params.instanceId)));
6442
+ request.entityRoute = {
6443
+ entityUrl,
6444
+ entity: materialized
6445
+ };
6446
+ return void 0;
6447
+ } catch (error) {
6448
+ return apiError(400, ErrCodeInvalidRequest, error instanceof Error ? error.message : `Invalid principal`);
6449
+ }
6194
6450
  if (entityType) return apiError(404, ErrCodeNotFound, `Entity not found at ${entityUrl}`);
6195
6451
  return apiError(404, ErrCodeUnknownEntityType, `Entity type "${request.params.type}" not found`);
6196
6452
  }
@@ -6202,6 +6458,7 @@ async function withExistingEntity(request, ctx) {
6202
6458
  }
6203
6459
  async function withSpawnableEntityType(request, ctx) {
6204
6460
  if (!entityUrlFromSegments(request.params.type, request.params.instanceId)) return void 0;
6461
+ if (request.params.type === `principal`) return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be spawned directly`);
6205
6462
  const entityType = await ctx.entityManager.registry.getEntityType(request.params.type);
6206
6463
  if (!entityType) return apiError(404, ErrCodeUnknownEntityType, `Entity type "${request.params.type}" not found`);
6207
6464
  return void 0;
@@ -6210,7 +6467,8 @@ async function listEntities({ query }, ctx) {
6210
6467
  const { entities: entities$1 } = await ctx.entityManager.registry.listEntities({
6211
6468
  type: firstQueryValue$1(query.type),
6212
6469
  status: firstQueryValue$1(query.status),
6213
- parent: firstQueryValue$1(query.parent)
6470
+ parent: firstQueryValue$1(query.parent),
6471
+ created_by: firstQueryValue$1(query.created_by)
6214
6472
  });
6215
6473
  return (0, itty_router.json)(entities$1.map((entity) => toPublicEntity(entity)));
6216
6474
  }
@@ -6220,6 +6478,8 @@ async function registerEntitiesSource(request, ctx) {
6220
6478
  return (0, itty_router.json)(result);
6221
6479
  }
6222
6480
  async function upsertSchedule(request, ctx) {
6481
+ const principalMutationError = rejectPrincipalEntityMutation(request, `scheduled`);
6482
+ if (principalMutationError) return principalMutationError;
6223
6483
  const parsed = routeBody(request);
6224
6484
  const { entityUrl } = requireExistingEntityRoute(request);
6225
6485
  const scheduleId = decodeURIComponent(request.params.scheduleId);
@@ -6235,12 +6495,13 @@ async function upsertSchedule(request, ctx) {
6235
6495
  return (0, itty_router.json)(result);
6236
6496
  }
6237
6497
  if (parsed.scheduleType === `future_send`) {
6498
+ if (parsed.from !== void 0 && parsed.from !== ctx.principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from must match Electric-Principal`);
6238
6499
  const result = await ctx.entityManager.upsertFutureSendSchedule(entityUrl, {
6239
6500
  id: scheduleId,
6240
6501
  payload: parsed.payload,
6241
6502
  targetUrl: parsed.targetUrl,
6242
6503
  fireAt: parsed.fireAt,
6243
- from: parsed.from,
6504
+ senderUrl: ctx.principal.url,
6244
6505
  messageType: parsed.messageType
6245
6506
  });
6246
6507
  return (0, itty_router.json)(result);
@@ -6248,11 +6509,15 @@ async function upsertSchedule(request, ctx) {
6248
6509
  throw new Error(`schedule schema accepted an unknown scheduleType`);
6249
6510
  }
6250
6511
  async function deleteSchedule(request, ctx) {
6512
+ const principalMutationError = rejectPrincipalEntityMutation(request, `unscheduled`);
6513
+ if (principalMutationError) return principalMutationError;
6251
6514
  const { entityUrl } = requireExistingEntityRoute(request);
6252
6515
  const result = await ctx.entityManager.deleteSchedule(entityUrl, { id: decodeURIComponent(request.params.scheduleId) });
6253
6516
  return (0, itty_router.json)(result);
6254
6517
  }
6255
6518
  async function setTag(request, ctx) {
6519
+ const principalMutationError = rejectPrincipalEntityMutation(request, `tagged`);
6520
+ if (principalMutationError) return principalMutationError;
6256
6521
  const parsed = routeBody(request);
6257
6522
  const { entityUrl } = requireExistingEntityRoute(request);
6258
6523
  const token = writeTokenFromRequest(request);
@@ -6260,12 +6525,16 @@ async function setTag(request, ctx) {
6260
6525
  return (0, itty_router.json)(toPublicEntity(updated));
6261
6526
  }
6262
6527
  async function removeTag(request, ctx) {
6528
+ const principalMutationError = rejectPrincipalEntityMutation(request, `untagged`);
6529
+ if (principalMutationError) return principalMutationError;
6263
6530
  const { entityUrl } = requireExistingEntityRoute(request);
6264
6531
  const token = writeTokenFromRequest(request);
6265
6532
  const updated = await ctx.entityManager.removeTag(entityUrl, decodeURIComponent(request.params.tagKey), token);
6266
6533
  return (0, itty_router.json)(toPublicEntity(updated));
6267
6534
  }
6268
6535
  async function forkEntity(request, ctx) {
6536
+ const principalMutationError = rejectPrincipalEntityMutation(request, `forked`);
6537
+ if (principalMutationError) return principalMutationError;
6269
6538
  const parsed = routeBody(request);
6270
6539
  const { entityUrl, entity } = requireExistingEntityRoute(request);
6271
6540
  await assertDispatchPolicyAllowed(ctx, entity.dispatch_policy);
@@ -6281,27 +6550,47 @@ async function forkEntity(request, ctx) {
6281
6550
  }
6282
6551
  async function sendEntity(request, ctx) {
6283
6552
  const parsed = routeBody(request);
6553
+ const principal = ctx.principal;
6554
+ if (parsed.from !== void 0 && parsed.from !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from must match Electric-Principal`);
6555
+ await ctx.entityManager.ensurePrincipal(principal);
6284
6556
  const { entityUrl, entity } = requireExistingEntityRoute(request);
6285
6557
  if (!entity.dispatch_policy) {
6286
6558
  const updatedEntity = await backfillEntityDispatchPolicy(ctx, entity);
6287
6559
  await linkEntityDispatchSubscription(ctx, updatedEntity);
6288
6560
  }
6289
6561
  if (parsed.afterMs && parsed.afterMs > 0) await ctx.entityManager.enqueueDelayedSend(entityUrl, {
6290
- from: parsed.from,
6562
+ from: principal.url,
6291
6563
  payload: parsed.payload,
6292
6564
  key: parsed.key,
6293
- type: parsed.type
6565
+ type: parsed.type,
6566
+ mode: parsed.mode,
6567
+ position: parsed.position
6294
6568
  }, new Date(Date.now() + parsed.afterMs));
6295
6569
  else await ctx.entityManager.send(entityUrl, {
6296
- from: parsed.from,
6570
+ from: principal.url,
6297
6571
  payload: parsed.payload,
6298
6572
  key: parsed.key,
6299
- type: parsed.type
6573
+ type: parsed.type,
6574
+ mode: parsed.mode,
6575
+ position: parsed.position
6300
6576
  });
6301
6577
  return (0, itty_router.status)(204);
6302
6578
  }
6579
+ async function updateInboxMessage(request, ctx) {
6580
+ const parsed = routeBody(request);
6581
+ const { entityUrl } = requireExistingEntityRoute(request);
6582
+ await ctx.entityManager.updateInboxMessage(entityUrl, decodeURIComponent(request.params.messageKey), parsed);
6583
+ return (0, itty_router.status)(204);
6584
+ }
6585
+ async function deleteInboxMessage(request, ctx) {
6586
+ const { entityUrl } = requireExistingEntityRoute(request);
6587
+ await ctx.entityManager.deleteInboxMessage(entityUrl, decodeURIComponent(request.params.messageKey));
6588
+ return (0, itty_router.status)(204);
6589
+ }
6303
6590
  async function spawnEntity(request, ctx) {
6304
6591
  const parsed = routeBody(request);
6592
+ const principal = ctx.principal;
6593
+ await ctx.entityManager.ensurePrincipal(principal);
6305
6594
  const dispatchPolicy = await resolveEffectiveDispatchPolicyForSpawn(ctx, request.params.type, {
6306
6595
  dispatchPolicy: parsed.dispatch_policy,
6307
6596
  parent: parsed.parent
@@ -6314,11 +6603,12 @@ async function spawnEntity(request, ctx) {
6314
6603
  parent: parsed.parent,
6315
6604
  dispatch_policy: dispatchPolicy,
6316
6605
  initialMessage: void 0,
6317
- wake: parsed.wake
6606
+ wake: parsed.wake,
6607
+ created_by: principal.url
6318
6608
  });
6319
6609
  await linkEntityDispatchSubscription(ctx, entity);
6320
6610
  if (parsed.initialMessage !== void 0) await ctx.entityManager.send(entity.url, {
6321
- from: parsed.parent ?? `spawn`,
6611
+ from: principal.url,
6322
6612
  payload: parsed.initialMessage
6323
6613
  });
6324
6614
  return (0, itty_router.json)({
@@ -6336,6 +6626,8 @@ function headEntity() {
6336
6626
  return (0, itty_router.status)(200);
6337
6627
  }
6338
6628
  async function killEntity(request, ctx) {
6629
+ const principalMutationError = rejectPrincipalEntityMutation(request, `killed`);
6630
+ if (principalMutationError) return principalMutationError;
6339
6631
  const { entityUrl, entity } = requireExistingEntityRoute(request);
6340
6632
  await unlinkEntityDispatchSubscription(ctx, entity);
6341
6633
  const result = await ctx.entityManager.kill(entityUrl);
@@ -6482,7 +6774,7 @@ function applyCors(response) {
6482
6774
  const headers = new Headers(response.headers);
6483
6775
  headers.set(`access-control-allow-origin`, `*`);
6484
6776
  headers.set(`access-control-allow-methods`, `GET, POST, PUT, PATCH, DELETE, OPTIONS`);
6485
- headers.set(`access-control-allow-headers`, `content-type, authorization, electric-claim-token, x-electric-asserted-email, x-electric-asserted-name, ngrok-skip-browser-warning`);
6777
+ headers.set(`access-control-allow-headers`, `content-type, authorization, electric-claim-token, ngrok-skip-browser-warning`);
6486
6778
  headers.set(`access-control-expose-headers`, `*`);
6487
6779
  return new Response(response.body, {
6488
6780
  status: response.status,
@@ -6562,9 +6854,9 @@ function firstQueryValue(value) {
6562
6854
  }
6563
6855
  async function registerRunner(request, ctx) {
6564
6856
  const parsed = routeBody(request);
6565
- const ownerUserId = parsed.owner_user_id ?? ctx.authenticatedUser?.userId;
6857
+ const ownerUserId = parsed.owner_user_id ?? ctx.principal?.key;
6566
6858
  if (!ownerUserId) throw new ElectricAgentsError(ErrCodeInvalidRequest, `owner_user_id is required when no authenticated user is present`, 400);
6567
- if (ctx.authenticatedUser && ownerUserId !== ctx.authenticatedUser.userId) throw new ElectricAgentsError(ErrCodeUnauthorized, `owner_user_id must match the authenticated user`, 403);
6859
+ if (ctx.principal && ownerUserId !== ctx.principal.key) throw new ElectricAgentsError(ErrCodeUnauthorized, `owner_user_id must match the authenticated user`, 403);
6568
6860
  const runner = await ctx.entityManager.registry.createRunner({
6569
6861
  id: parsed.id,
6570
6862
  ownerUserId,
@@ -6578,8 +6870,8 @@ async function registerRunner(request, ctx) {
6578
6870
  }
6579
6871
  async function listRunners(request, ctx) {
6580
6872
  const requestedOwner = firstQueryValue(request.query.owner_user_id);
6581
- if (ctx.authenticatedUser && requestedOwner && requestedOwner !== ctx.authenticatedUser.userId) throw new ElectricAgentsError(ErrCodeUnauthorized, `owner_user_id must match the authenticated user`, 403);
6582
- const runners$1 = await ctx.entityManager.registry.listRunners({ ownerUserId: ctx.authenticatedUser?.userId ?? requestedOwner });
6873
+ if (ctx.principal && requestedOwner && requestedOwner !== ctx.principal.key) throw new ElectricAgentsError(ErrCodeUnauthorized, `owner_user_id must match the authenticated user`, 403);
6874
+ const runners$1 = await ctx.entityManager.registry.listRunners({ ownerUserId: ctx.principal?.key ?? requestedOwner });
6583
6875
  return (0, itty_router.json)(runners$1);
6584
6876
  }
6585
6877
  async function getRunner(request, ctx) {
@@ -6617,9 +6909,8 @@ async function setRunnerStatus(request, ctx, adminStatus) {
6617
6909
  }
6618
6910
  async function claimWake(request, ctx) {
6619
6911
  const runnerId = routeParam$1(request, `id`);
6620
- if (!ctx.authenticatedUser) throw new ElectricAgentsError(ErrCodeUnauthorized, `Authentication is required to claim runner work`, 401);
6621
6912
  const runner = await requireRunner(ctx, runnerId);
6622
- if (runner.owner_user_id !== ctx.authenticatedUser.userId) throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner claim requires the authenticated owner`, 403);
6913
+ if (ctx.principal && runner.owner_user_id !== ctx.principal.key) throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner claim requires the authenticated owner`, 403);
6623
6914
  if (runner.admin_status !== `enabled`) throw new ElectricAgentsError(ErrCodeNotRunning, `Runner is disabled`, 409);
6624
6915
  const parsed = routeBody(request);
6625
6916
  const expectedSubscriptionId = subscriptionIdForDispatchTarget({
@@ -6651,8 +6942,8 @@ async function requireRunner(ctx, runnerId) {
6651
6942
  return runner;
6652
6943
  }
6653
6944
  function assertRunnerOwnerIfAuthenticated(ctx, ownerUserId) {
6654
- if (!ctx.authenticatedUser) return;
6655
- if (ownerUserId === ctx.authenticatedUser.userId) return;
6945
+ if (!ctx.principal) return;
6946
+ if (ownerUserId === ctx.principal.key) return;
6656
6947
  throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner access requires the authenticated owner`, 403);
6657
6948
  }
6658
6949
  async function notificationFromClaim(ctx, input) {
@@ -6709,8 +7000,10 @@ async function notificationFromClaim(ctx, input) {
6709
7000
  url: entity.url,
6710
7001
  streams: entity.streams,
6711
7002
  tags: entity.tags,
6712
- spawnArgs: entity.spawn_args
6713
- }
7003
+ spawnArgs: entity.spawn_args,
7004
+ createdBy: entity.created_by
7005
+ },
7006
+ principal: principalFromCreatedBy(entity.created_by)
6714
7007
  };
6715
7008
  }
6716
7009