@electric-ax/agents-server 0.3.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.
@@ -32,36 +32,6 @@ var __export = (target, all) => {
32
32
  });
33
33
  };
34
34
 
35
- //#endregion
36
- //#region src/dev-asserted-auth.ts
37
- const DEV_ASSERTED_EMAIL_HEADER = `x-electric-asserted-email`;
38
- const DEV_ASSERTED_NAME_HEADER = `x-electric-asserted-name`;
39
- function clean(value) {
40
- const trimmed = value?.trim();
41
- return trimmed || void 0;
42
- }
43
- function createDevAssertedAuthenticateRequest(options) {
44
- if (!options.enabled) return void 0;
45
- return (request) => {
46
- const email = clean(request.headers.get(DEV_ASSERTED_EMAIL_HEADER)) ?? clean(options.defaultEmail);
47
- const name = clean(request.headers.get(DEV_ASSERTED_NAME_HEADER)) ?? clean(options.defaultName);
48
- const userId = email ?? name;
49
- if (!userId) return null;
50
- return {
51
- userId,
52
- email,
53
- name
54
- };
55
- };
56
- }
57
- function devAssertedAuthOptionsFromEnv(env = process.env) {
58
- return {
59
- enabled: env.ELECTRIC_AGENTS_DEV_ASSERTED_AUTH === `1`,
60
- defaultEmail: env.ELECTRIC_ASSERTED_AUTH_EMAIL,
61
- defaultName: env.ELECTRIC_ASSERTED_AUTH_NAME
62
- };
63
- }
64
-
65
35
  //#endregion
66
36
  //#region src/db/schema.ts
67
37
  var schema_exports = {};
@@ -106,6 +76,7 @@ const entities = pgTable(`entities`, {
106
76
  tagsIndex: text(`tags_index`).array().notNull().default(sql`'{}'::text[]`),
107
77
  spawnArgs: jsonb(`spawn_args`).default({}),
108
78
  parent: text(`parent`),
79
+ createdBy: text(`created_by`),
109
80
  typeRevision: integer(`type_revision`),
110
81
  inboxSchemas: jsonb(`inbox_schemas`),
111
82
  stateSchemas: jsonb(`state_schemas`),
@@ -116,6 +87,7 @@ const entities = pgTable(`entities`, {
116
87
  index(`idx_entities_type`).on(table.tenantId, table.type),
117
88
  index(`idx_entities_status`).on(table.tenantId, table.status),
118
89
  index(`idx_entities_parent`).on(table.tenantId, table.parent),
90
+ index(`idx_entities_created_by`).on(table.tenantId, table.createdBy),
119
91
  index(`entities_tags_index_gin`).using(`gin`, table.tagsIndex),
120
92
  check(`chk_entities_status`, sql`${table.status} IN ('spawning', 'running', 'idle', 'stopped')`)
121
93
  ]);
@@ -491,6 +463,7 @@ function toPublicEntity(entity) {
491
463
  tags: entity.tags,
492
464
  spawn_args: entity.spawn_args,
493
465
  parent: entity.parent,
466
+ created_by: entity.created_by,
494
467
  created_at: entity.created_at,
495
468
  updated_at: entity.updated_at
496
469
  };
@@ -1672,6 +1645,103 @@ async function proxyElectric(request, ctx) {
1672
1645
  });
1673
1646
  }
1674
1647
 
1648
+ //#endregion
1649
+ //#region src/principal.ts
1650
+ const ELECTRIC_PRINCIPAL_HEADER = `electric-principal`;
1651
+ const PRINCIPAL_KINDS = new Set([
1652
+ `user`,
1653
+ `agent`,
1654
+ `service`,
1655
+ `system`
1656
+ ]);
1657
+ function parsePrincipalKey(input) {
1658
+ const colon = input.indexOf(`:`);
1659
+ if (colon <= 0) throw new Error(`Invalid principal key`);
1660
+ const kind = input.slice(0, colon);
1661
+ const id = input.slice(colon + 1);
1662
+ if (!PRINCIPAL_KINDS.has(kind)) throw new Error(`Invalid principal kind`);
1663
+ if (!id || id.includes(`/`)) throw new Error(`Invalid principal id`);
1664
+ const key = `${kind}:${id}`;
1665
+ return {
1666
+ kind,
1667
+ id,
1668
+ key,
1669
+ url: `/principal/${encodeURIComponent(key)}`
1670
+ };
1671
+ }
1672
+ function principalUrl(key) {
1673
+ return parsePrincipalKey(key).url;
1674
+ }
1675
+ function principalKeyFromUrl(url) {
1676
+ if (!url.startsWith(`/principal/`)) return null;
1677
+ const segment = url.slice(`/principal/`.length);
1678
+ if (!segment || segment.includes(`/`)) return null;
1679
+ try {
1680
+ const key = decodeURIComponent(segment);
1681
+ return parsePrincipalKey(key).key;
1682
+ } catch {
1683
+ return null;
1684
+ }
1685
+ }
1686
+ function getPrincipalFromRequest(request) {
1687
+ const value = request.headers.get(ELECTRIC_PRINCIPAL_HEADER);
1688
+ return value ? parsePrincipalKey(value) : null;
1689
+ }
1690
+ function getDevPrincipal() {
1691
+ return parsePrincipalKey(`system:dev-local`);
1692
+ }
1693
+ const BUILT_IN_SYSTEM_PRINCIPAL_IDS = new Set([
1694
+ `framework`,
1695
+ `auth-sync`,
1696
+ `dev-local`
1697
+ ]);
1698
+ function isBuiltInSystemPrincipalUrl(url) {
1699
+ if (!url?.startsWith(`/principal/`)) return false;
1700
+ try {
1701
+ const key = principalKeyFromUrl(url);
1702
+ if (!key) return false;
1703
+ const principal = parsePrincipalKey(key);
1704
+ return principal.kind === `system` && BUILT_IN_SYSTEM_PRINCIPAL_IDS.has(principal.id);
1705
+ } catch {
1706
+ return false;
1707
+ }
1708
+ }
1709
+ function principalFromCreatedBy(createdBy) {
1710
+ if (!createdBy) return void 0;
1711
+ const key = principalKeyFromUrl(createdBy);
1712
+ if (!key) return {
1713
+ url: createdBy,
1714
+ key: null
1715
+ };
1716
+ const principal = parsePrincipalKey(key);
1717
+ return {
1718
+ url: principal.url,
1719
+ key: principal.key,
1720
+ kind: principal.kind,
1721
+ id: principal.id
1722
+ };
1723
+ }
1724
+ const principalIdentityStateSchema = Type.Object({
1725
+ kind: Type.Union([
1726
+ Type.Literal(`user`),
1727
+ Type.Literal(`agent`),
1728
+ Type.Literal(`service`),
1729
+ Type.Literal(`system`)
1730
+ ]),
1731
+ id: Type.String(),
1732
+ key: Type.String(),
1733
+ url: Type.String(),
1734
+ updated_at: Type.String(),
1735
+ display_name: Type.Optional(Type.String()),
1736
+ email: Type.Optional(Type.String()),
1737
+ avatar_url: Type.Optional(Type.String()),
1738
+ auth_provider: Type.Optional(Type.String()),
1739
+ auth_subject: Type.Optional(Type.String()),
1740
+ claims: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
1741
+ created_at: Type.Optional(Type.String())
1742
+ }, { additionalProperties: false });
1743
+ const principalUpdateIdentityMessageSchema = Type.Object({ identity: principalIdentityStateSchema }, { additionalProperties: false });
1744
+
1675
1745
  //#endregion
1676
1746
  //#region src/dispatch-policy-schema.ts
1677
1747
  const nonEmptyStringSchema = Type.String({ minLength: 1 });
@@ -1913,6 +1983,24 @@ var PostgresRegistry = class {
1913
1983
  }
1914
1984
  });
1915
1985
  }
1986
+ async ensureEntityType(et) {
1987
+ const existing = await this.getEntityType(et.name);
1988
+ if (existing) return existing;
1989
+ await this.db.insert(entityTypes).values({
1990
+ tenantId: this.tenantId,
1991
+ name: et.name,
1992
+ description: et.description,
1993
+ creationSchema: et.creation_schema ?? null,
1994
+ inboxSchemas: et.inbox_schemas ?? null,
1995
+ stateSchemas: et.state_schemas ?? null,
1996
+ serveEndpoint: et.serve_endpoint ?? null,
1997
+ defaultDispatchPolicy: et.default_dispatch_policy ?? null,
1998
+ revision: et.revision,
1999
+ createdAt: et.created_at,
2000
+ updatedAt: et.updated_at
2001
+ }).onConflictDoNothing();
2002
+ return await this.getEntityType(et.name);
2003
+ }
1916
2004
  async getEntityType(name) {
1917
2005
  const rows = await this.db.select().from(entityTypes).where(this.entityTypeWhere(name)).limit(1);
1918
2006
  if (rows.length === 0) return null;
@@ -1952,6 +2040,7 @@ var PostgresRegistry = class {
1952
2040
  tagsIndex: buildTagsIndex(entity.tags),
1953
2041
  spawnArgs: entity.spawn_args ?? {},
1954
2042
  parent: entity.parent ?? null,
2043
+ createdBy: entity.created_by ?? null,
1955
2044
  typeRevision: entity.type_revision ?? null,
1956
2045
  inboxSchemas: entity.inbox_schemas ?? null,
1957
2046
  stateSchemas: entity.state_schemas ?? null,
@@ -1997,6 +2086,7 @@ var PostgresRegistry = class {
1997
2086
  if (filter?.type) conditions.push(eq(entities.type, filter.type));
1998
2087
  if (filter?.status) conditions.push(eq(entities.status, filter.status));
1999
2088
  if (filter?.parent) conditions.push(eq(entities.parent, filter.parent));
2089
+ if (filter?.created_by) conditions.push(eq(entities.createdBy, filter.created_by));
2000
2090
  const whereClause = and(...conditions);
2001
2091
  const countResult = await this.db.select({ count: sql`count(*)` }).from(entities).where(whereClause);
2002
2092
  const total = Number(countResult[0].count);
@@ -2283,6 +2373,7 @@ var PostgresRegistry = class {
2283
2373
  tags: row.tags ?? {},
2284
2374
  spawn_args: row.spawnArgs,
2285
2375
  parent: row.parent ?? void 0,
2376
+ created_by: row.createdBy ?? void 0,
2286
2377
  type_revision: row.typeRevision ?? void 0,
2287
2378
  inbox_schemas: row.inboxSchemas,
2288
2379
  state_schemas: row.stateSchemas,
@@ -2430,25 +2521,11 @@ function buildManifestWakeRegistration(subscriberUrl, manifest, manifestKey) {
2430
2521
 
2431
2522
  //#endregion
2432
2523
  //#region src/entity-manager.ts
2524
+ function createInitialQueuePosition(date) {
2525
+ return `${String(date.getTime()).padStart(16, `0`)}:a0`;
2526
+ }
2433
2527
  const DEFAULT_FORK_WAIT_TIMEOUT_MS = 12e4;
2434
2528
  const DEFAULT_FORK_WAIT_POLL_MS = 250;
2435
- function applyTypeDefaultSubscriptionScope$1(policy, typeDefault) {
2436
- const target = policy.targets[0];
2437
- const defaultTarget = typeDefault?.targets[0];
2438
- if (!target || !defaultTarget?.subscription_id) return policy;
2439
- if (!sameDispatchDestination$1(target, defaultTarget)) return policy;
2440
- if (target.subscription_id === defaultTarget.subscription_id) return policy;
2441
- return { targets: [{
2442
- ...target,
2443
- subscription_id: defaultTarget.subscription_id
2444
- }] };
2445
- }
2446
- function sameDispatchDestination$1(a, b) {
2447
- if (a.type !== b.type) return false;
2448
- if (a.type === `runner` && b.type === `runner`) return a.runnerId === b.runnerId;
2449
- if (a.type === `webhook` && b.type === `webhook`) return a.url === b.url;
2450
- return false;
2451
- }
2452
2529
  function sleep(ms) {
2453
2530
  return new Promise((resolve$1) => setTimeout(resolve$1, ms));
2454
2531
  }
@@ -2519,6 +2596,7 @@ var EntityManager = class {
2519
2596
  }
2520
2597
  async registerEntityType(req) {
2521
2598
  if (!req.name) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: name`, 400);
2599
+ if (req.name === `principal`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type "principal" is built in and cannot be registered or updated`, 400);
2522
2600
  if (req.name.startsWith(`_`)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type names starting with "_" are reserved`, 400);
2523
2601
  if (!req.description) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: description`, 400);
2524
2602
  this.validateSchema(req.creation_schema);
@@ -2545,10 +2623,63 @@ var EntityManager = class {
2545
2623
  return stored;
2546
2624
  }
2547
2625
  async deleteEntityType(name) {
2626
+ if (name === `principal`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type "principal" is built in and cannot be deleted`, 400);
2548
2627
  const existing = await this.registry.getEntityType(name);
2549
2628
  if (!existing) throw new ElectricAgentsError(ErrCodeNotFound, `Entity type "${name}" not found`, 404);
2550
2629
  await this.registry.deleteEntityType(name);
2551
2630
  }
2631
+ async ensurePrincipalEntityType() {
2632
+ const now = new Date().toISOString();
2633
+ return await this.registry.ensureEntityType({
2634
+ name: `principal`,
2635
+ description: `built-in principal entity`,
2636
+ inbox_schemas: { update_identity: principalUpdateIdentityMessageSchema },
2637
+ state_schemas: { identity: principalIdentityStateSchema },
2638
+ revision: 1,
2639
+ created_at: now,
2640
+ updated_at: now
2641
+ });
2642
+ }
2643
+ async ensurePrincipal(principal) {
2644
+ const existing = await this.registry.getEntity(principal.url);
2645
+ if (existing) return existing;
2646
+ await this.ensurePrincipalEntityType();
2647
+ try {
2648
+ const entity = await this.spawn(`principal`, {
2649
+ instance_id: principal.key,
2650
+ args: {
2651
+ kind: principal.kind,
2652
+ id: principal.id,
2653
+ key: principal.key
2654
+ },
2655
+ tags: {
2656
+ principal_kind: principal.kind,
2657
+ principal_id: principal.id
2658
+ },
2659
+ created_by: principal.url
2660
+ });
2661
+ const now = new Date().toISOString();
2662
+ await this.streamClient.append(entity.streams.main, this.encodeChangeEvent({
2663
+ type: `identity`,
2664
+ key: `self`,
2665
+ value: {
2666
+ kind: principal.kind,
2667
+ id: principal.id,
2668
+ key: principal.key,
2669
+ url: principal.url,
2670
+ created_at: now,
2671
+ updated_at: now
2672
+ }
2673
+ }));
2674
+ return entity;
2675
+ } catch (error) {
2676
+ if (error instanceof ElectricAgentsError && error.code === ErrCodeDuplicateURL) {
2677
+ const raced = await this.registry.getEntity(principal.url);
2678
+ if (raced) return raced;
2679
+ }
2680
+ throw error;
2681
+ }
2682
+ }
2552
2683
  /**
2553
2684
  * Spawn a new entity of the given type with durable streams.
2554
2685
  */
@@ -2564,6 +2695,7 @@ var EntityManager = class {
2564
2695
  });
2565
2696
  }
2566
2697
  async spawnInner(typeName, req) {
2698
+ 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);
2567
2699
  if (typeName.startsWith(`_`)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type names starting with "_" are reserved`, 400);
2568
2700
  const entityType = await this.registry.getEntityType(typeName);
2569
2701
  if (!entityType) throw new ElectricAgentsError(ErrCodeUnknownEntityType, `Entity type "${typeName}" not found`, 404);
@@ -2575,7 +2707,7 @@ var EntityManager = class {
2575
2707
  const instanceId = req.instance_id || randomUUID();
2576
2708
  if (instanceId.includes(`/`)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `instance_id must not contain forward slashes`, 400);
2577
2709
  const writeToken = randomUUID();
2578
- const entityURL = `/${typeName}/${instanceId}`;
2710
+ const entityURL = typeName === `principal` ? principalUrl(instanceId) : `/${typeName}/${instanceId}`;
2579
2711
  const mainPath = `${entityURL}/main`;
2580
2712
  const errorPath = `${entityURL}/error`;
2581
2713
  const subscriptionId = `${typeName}-handler`;
@@ -2587,7 +2719,7 @@ var EntityManager = class {
2587
2719
  parentEntity = await this.registry.getEntity(req.parent);
2588
2720
  if (!parentEntity) throw new ElectricAgentsError(ErrCodeNotFound, `Parent entity "${req.parent}" not found`, 404);
2589
2721
  }
2590
- 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;
2722
+ 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;
2591
2723
  const now = Date.now();
2592
2724
  const entityData = {
2593
2725
  type: typeName,
@@ -2606,6 +2738,7 @@ var EntityManager = class {
2606
2738
  inbox_schemas: entityType.inbox_schemas,
2607
2739
  state_schemas: entityType.state_schemas,
2608
2740
  created_at: now,
2741
+ created_by: req.created_by ?? parentEntity?.created_by,
2609
2742
  updated_at: now
2610
2743
  };
2611
2744
  if (req.parent) entityData.parent = req.parent;
@@ -2635,7 +2768,7 @@ var EntityManager = class {
2635
2768
  const inboxEvent = entityStateSchema.inbox.insert({
2636
2769
  key: `msg-in-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
2637
2770
  value: {
2638
- from: req.parent ?? `spawn`,
2771
+ from: req.created_by ?? req.parent ?? `spawn`,
2639
2772
  payload: req.initialMessage,
2640
2773
  timestamp: msgNow
2641
2774
  }
@@ -3178,10 +3311,10 @@ var EntityManager = class {
3178
3311
  changed = true;
3179
3312
  }
3180
3313
  }
3181
- if (typeof next.from === `string`) {
3182
- const forkFrom = entityUrlMap.get(next.from);
3183
- if (forkFrom) {
3184
- next.from = forkFrom;
3314
+ if (typeof next.senderUrl === `string`) {
3315
+ const forkSender = entityUrlMap.get(next.senderUrl);
3316
+ if (forkSender) {
3317
+ next.senderUrl = forkSender;
3185
3318
  changed = true;
3186
3319
  }
3187
3320
  }
@@ -3247,6 +3380,7 @@ var EntityManager = class {
3247
3380
  const fireAtRaw = manifest.fireAt;
3248
3381
  const producerId = manifest.producerId;
3249
3382
  const targetUrl = manifest.targetUrl;
3383
+ const senderUrl = typeof manifest.senderUrl === `string` ? manifest.senderUrl : ownerEntityUrl;
3250
3384
  if (typeof fireAtRaw !== `string` || typeof producerId !== `string` || typeof targetUrl !== `string`) {
3251
3385
  serverLog.warn(`[agent-server] invalid forked future_send manifest entry for ${ownerEntityUrl}/${manifestKey}`);
3252
3386
  return;
@@ -3258,7 +3392,7 @@ var EntityManager = class {
3258
3392
  }
3259
3393
  await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
3260
3394
  entityUrl: targetUrl,
3261
- from: typeof manifest.from === `string` ? manifest.from : ownerEntityUrl,
3395
+ from: senderUrl,
3262
3396
  payload: manifest.payload,
3263
3397
  key: `scheduled-${producerId}`,
3264
3398
  type: typeof manifest.messageType === `string` ? manifest.messageType : void 0,
@@ -3272,6 +3406,7 @@ var EntityManager = class {
3272
3406
  kind: `schedule`,
3273
3407
  scheduleType: `future_send`,
3274
3408
  targetUrl,
3409
+ senderUrl,
3275
3410
  fireAt: fireAt.toISOString(),
3276
3411
  producerId,
3277
3412
  status: `pending`
@@ -3299,9 +3434,14 @@ var EntityManager = class {
3299
3434
  const value = {
3300
3435
  from: req.from,
3301
3436
  payload: req.payload,
3302
- timestamp: now
3437
+ timestamp: now,
3438
+ mode: req.mode ?? `immediate`,
3439
+ status: req.mode === `queued` || req.mode === `paused` ? `pending` : `processed`
3303
3440
  };
3304
3441
  if (req.type) value.message_type = req.type;
3442
+ if (req.position) value.position = req.position;
3443
+ else if (value.mode === `queued` || value.mode === `paused`) value.position = createInitialQueuePosition(new Date(now));
3444
+ if (value.status === `processed`) value.processed_at = now;
3305
3445
  const envelope = entityStateSchema.inbox.insert({
3306
3446
  key,
3307
3447
  value
@@ -3313,11 +3453,47 @@ var EntityManager = class {
3313
3453
  return;
3314
3454
  }
3315
3455
  await this.streamClient.append(entity.streams.main, encoded);
3456
+ if (entity.type === `principal` && req.type === `update_identity`) {
3457
+ const identity = req.payload?.identity;
3458
+ await this.streamClient.append(entity.streams.main, this.encodeChangeEvent({
3459
+ type: `identity`,
3460
+ key: `self`,
3461
+ value: identity
3462
+ }));
3463
+ }
3316
3464
  } catch (err) {
3317
3465
  if (this.isClosedStreamError(err)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409);
3318
3466
  throw err;
3319
3467
  }
3320
3468
  }
3469
+ async updateInboxMessage(entityUrl, key, req) {
3470
+ const entity = await this.registry.getEntity(entityUrl);
3471
+ if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
3472
+ if (entity.status === `stopped`) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409);
3473
+ const now = new Date().toISOString();
3474
+ const value = {};
3475
+ if (`payload` in req) value.payload = req.payload;
3476
+ if (req.position !== void 0) value.position = req.position;
3477
+ if (req.mode !== void 0) value.mode = req.mode;
3478
+ if (req.status !== void 0) {
3479
+ value.status = req.status;
3480
+ if (req.status === `processed`) value.processed_at = now;
3481
+ if (req.status === `cancelled`) value.cancelled_at = now;
3482
+ }
3483
+ if (Object.keys(value).length === 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `No inbox fields to update`, 400);
3484
+ const envelope = entityStateSchema.inbox.update({
3485
+ key,
3486
+ value
3487
+ });
3488
+ await this.streamClient.append(entity.streams.main, this.encodeChangeEvent(envelope));
3489
+ }
3490
+ async deleteInboxMessage(entityUrl, key) {
3491
+ const entity = await this.registry.getEntity(entityUrl);
3492
+ if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
3493
+ if (entity.status === `stopped`) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409);
3494
+ const envelope = entityStateSchema.inbox.delete({ key });
3495
+ await this.streamClient.append(entity.streams.main, this.encodeChangeEvent(envelope));
3496
+ }
3321
3497
  async setTag(entityUrl, key, req, token) {
3322
3498
  const entity = await this.registry.getEntity(entityUrl);
3323
3499
  if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
@@ -3403,7 +3579,7 @@ var EntityManager = class {
3403
3579
  async upsertFutureSendSchedule(ownerEntityUrl, req) {
3404
3580
  if (!this.scheduler) throw new Error(`Scheduler not configured`);
3405
3581
  const targetUrl = req.targetUrl ?? ownerEntityUrl;
3406
- const from = req.from ?? ownerEntityUrl;
3582
+ const from = req.senderUrl ?? ownerEntityUrl;
3407
3583
  const fireAt = new Date(req.fireAt);
3408
3584
  if (Number.isNaN(fireAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid fireAt timestamp: ${req.fireAt}`, 400);
3409
3585
  await this.validateSendRequest(targetUrl, {
@@ -3431,9 +3607,9 @@ var EntityManager = class {
3431
3607
  scheduleType: `future_send`,
3432
3608
  fireAt: fireAt.toISOString(),
3433
3609
  targetUrl,
3610
+ senderUrl: from,
3434
3611
  payload: req.payload,
3435
3612
  producerId,
3436
- ...req.from ? { from: req.from } : {},
3437
3613
  ...req.messageType ? { messageType: req.messageType } : {},
3438
3614
  status: `pending`
3439
3615
  }
@@ -3447,9 +3623,9 @@ var EntityManager = class {
3447
3623
  scheduleType: `future_send`,
3448
3624
  fireAt: fireAt.toISOString(),
3449
3625
  targetUrl,
3626
+ senderUrl: from,
3450
3627
  payload: req.payload,
3451
3628
  producerId,
3452
- ...req.from ? { from: req.from } : {},
3453
3629
  ...req.messageType ? { messageType: req.messageType } : {},
3454
3630
  status: `pending`
3455
3631
  }, { txid });
@@ -3487,7 +3663,9 @@ var EntityManager = class {
3487
3663
  from: req.from,
3488
3664
  payload: req.payload,
3489
3665
  key: req.key,
3490
- type: req.type
3666
+ type: req.type,
3667
+ mode: req.mode,
3668
+ position: req.position
3491
3669
  }, fireAt);
3492
3670
  }
3493
3671
  /**
@@ -3637,6 +3815,7 @@ var EntityManager = class {
3637
3815
  * Add new input/output schema keys to an entity type directly in Postgres.
3638
3816
  */
3639
3817
  async amendSchemas(typeName, schemas) {
3818
+ if (typeName === `principal`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Entity type "principal" is built in and cannot be amended`, 400);
3640
3819
  this.validateSchemaMap(schemas.inbox_schemas);
3641
3820
  this.validateSchemaMap(schemas.state_schemas);
3642
3821
  const existing = await this.registry.getEntityType(typeName);
@@ -3686,9 +3865,11 @@ var EntityManager = class {
3686
3865
  url: entity.url,
3687
3866
  streams: entity.streams,
3688
3867
  tags: entity.tags,
3689
- spawnArgs: entity.spawn_args
3868
+ spawnArgs: entity.spawn_args,
3869
+ createdBy: entity.created_by
3690
3870
  },
3691
- triggerEvent: `message_received`
3871
+ principal: principalFromCreatedBy(entity.created_by),
3872
+ triggerEvent: `inbox`
3692
3873
  };
3693
3874
  }
3694
3875
  validateSchema(schema) {
@@ -3728,6 +3909,7 @@ var EntityManager = class {
3728
3909
  }
3729
3910
  }
3730
3911
  if (!req.from) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: from`, 400);
3912
+ 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);
3731
3913
  if (req.payload === void 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Missing required field: payload`, 400);
3732
3914
  return entity;
3733
3915
  }
@@ -3842,8 +4024,7 @@ async function assertDispatchPolicyAllowed(ctx, policy) {
3842
4024
  if (!target || target.type !== `runner`) return;
3843
4025
  const runner = await ctx.entityManager.registry.getRunner(target.runnerId);
3844
4026
  if (!runner) throw new ElectricAgentsError(ErrCodeNotFound, `Runner "${target.runnerId}" not found`, 404);
3845
- if (!ctx.authenticatedUser) throw new ElectricAgentsError(ErrCodeUnauthorized, `Authentication is required for runner-targeted dispatch`, 401);
3846
- if (runner.owner_user_id !== ctx.authenticatedUser.userId) throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner dispatch requires the authenticated owner`, 403);
4027
+ if (ctx.principal && runner.owner_user_id !== ctx.principal.key) throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner dispatch requires the authenticated owner`, 403);
3847
4028
  }
3848
4029
  async function linkEntityDispatchSubscription(ctx, entity) {
3849
4030
  const dispatchPolicy = await resolveEffectiveDispatchPolicyForEntity(ctx, entity);
@@ -3856,7 +4037,12 @@ async function unlinkEntityDispatchSubscription(ctx, entity) {
3856
4037
  const target = dispatchPolicy?.targets[0];
3857
4038
  if (!target) return;
3858
4039
  const subscriptionId = subscriptionIdForEntityDispatchTarget(target, entity.url);
3859
- await ctx.streamClient.removeSubscriptionStream(subscriptionId, entity.streams.main).catch(() => {});
4040
+ await ctx.streamClient.removeSubscriptionStream(subscriptionId, entity.streams.main).catch((err) => {
4041
+ serverLog.warn(`[dispatch-policy] failed to remove stream from subscription`, {
4042
+ subscriptionId,
4043
+ stream: entity.streams.main
4044
+ }, err);
4045
+ });
3860
4046
  }
3861
4047
  async function linkStreamToTargetSubscription(ctx, target, entity) {
3862
4048
  const streamPath = entity.streams.main;
@@ -3931,11 +4117,33 @@ const spawnBodySchema = Type.Object({
3931
4117
  }))
3932
4118
  });
3933
4119
  const sendBodySchema = Type.Object({
3934
- from: Type.Optional(Type.String()),
3935
4120
  payload: Type.Optional(Type.Unknown()),
3936
4121
  key: Type.Optional(Type.String()),
3937
4122
  type: Type.Optional(Type.String()),
3938
- afterMs: Type.Optional(Type.Number())
4123
+ mode: Type.Optional(Type.Union([
4124
+ Type.Literal(`immediate`),
4125
+ Type.Literal(`queued`),
4126
+ Type.Literal(`paused`),
4127
+ Type.Literal(`steer`)
4128
+ ])),
4129
+ position: Type.Optional(Type.String()),
4130
+ afterMs: Type.Optional(Type.Number()),
4131
+ from: Type.Optional(Type.String())
4132
+ });
4133
+ const inboxMessageBodySchema = Type.Object({
4134
+ payload: Type.Optional(Type.Unknown()),
4135
+ position: Type.Optional(Type.String()),
4136
+ mode: Type.Optional(Type.Union([
4137
+ Type.Literal(`immediate`),
4138
+ Type.Literal(`queued`),
4139
+ Type.Literal(`paused`),
4140
+ Type.Literal(`steer`)
4141
+ ])),
4142
+ status: Type.Optional(Type.Union([
4143
+ Type.Literal(`pending`),
4144
+ Type.Literal(`processed`),
4145
+ Type.Literal(`cancelled`)
4146
+ ]))
3939
4147
  });
3940
4148
  const forkBodySchema = Type.Object({
3941
4149
  instance_id: Type.Optional(Type.String()),
@@ -3954,8 +4162,8 @@ const scheduleBodySchema = Type.Union([Type.Object({
3954
4162
  payload: Type.Unknown(),
3955
4163
  targetUrl: Type.Optional(Type.String()),
3956
4164
  fireAt: Type.String(),
3957
- from: Type.Optional(Type.String()),
3958
- messageType: Type.Optional(Type.String())
4165
+ messageType: Type.Optional(Type.String()),
4166
+ from: Type.Optional(Type.String())
3959
4167
  })]);
3960
4168
  const entitiesRegisterBodySchema = Type.Object({ tags: Type.Optional(stringRecordSchema) });
3961
4169
  const entitiesRouter = Router({ base: `/_electric/entities` });
@@ -3966,6 +4174,8 @@ entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, getEntity);
3966
4174
  entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity);
3967
4175
  entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, killEntity);
3968
4176
  entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), sendEntity);
4177
+ entitiesRouter.patch(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, withSchema(inboxMessageBodySchema), updateInboxMessage);
4178
+ entitiesRouter.delete(`/:type/:instanceId/inbox/:messageKey`, withExistingEntity, deleteInboxMessage);
3969
4179
  entitiesRouter.post(`/:type/:instanceId/fork`, withExistingEntity, withSchema(forkBodySchema), forkEntity);
3970
4180
  entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withSchema(setTagBodySchema), setTag);
3971
4181
  entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, removeTag);
@@ -3974,6 +4184,11 @@ entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEn
3974
4184
  function entityUrlFromSegments(type, instanceId) {
3975
4185
  if (!type || !instanceId) return null;
3976
4186
  if (type.startsWith(`_`) || type.includes(`*`) || instanceId.includes(`*`)) return null;
4187
+ if (type === `principal`) try {
4188
+ return principalUrl(decodeURIComponent(instanceId));
4189
+ } catch {
4190
+ return null;
4191
+ }
3977
4192
  return `/${type}/${instanceId}`;
3978
4193
  }
3979
4194
  function firstQueryValue$1(value) {
@@ -3983,12 +4198,27 @@ function requireExistingEntityRoute(request) {
3983
4198
  if (!request.entityRoute) throw new Error(`existing entity middleware did not run`);
3984
4199
  return request.entityRoute;
3985
4200
  }
4201
+ function rejectPrincipalEntityMutation(request, action) {
4202
+ const { entity } = requireExistingEntityRoute(request);
4203
+ if (entity.type !== `principal`) return void 0;
4204
+ return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be ${action}`);
4205
+ }
3986
4206
  async function withExistingEntity(request, ctx) {
3987
4207
  const entityUrl = entityUrlFromSegments(request.params.type, request.params.instanceId);
3988
4208
  if (!entityUrl) return void 0;
3989
4209
  const entity = await ctx.entityManager.registry.getEntity(entityUrl);
3990
4210
  if (!entity) {
3991
4211
  const entityType = await ctx.entityManager.registry.getEntityType(request.params.type);
4212
+ if (request.params.type === `principal`) try {
4213
+ const materialized = await ctx.entityManager.ensurePrincipal(parsePrincipalKey(decodeURIComponent(request.params.instanceId)));
4214
+ request.entityRoute = {
4215
+ entityUrl,
4216
+ entity: materialized
4217
+ };
4218
+ return void 0;
4219
+ } catch (error) {
4220
+ return apiError(400, ErrCodeInvalidRequest, error instanceof Error ? error.message : `Invalid principal`);
4221
+ }
3992
4222
  if (entityType) return apiError(404, ErrCodeNotFound, `Entity not found at ${entityUrl}`);
3993
4223
  return apiError(404, ErrCodeUnknownEntityType, `Entity type "${request.params.type}" not found`);
3994
4224
  }
@@ -4000,6 +4230,7 @@ async function withExistingEntity(request, ctx) {
4000
4230
  }
4001
4231
  async function withSpawnableEntityType(request, ctx) {
4002
4232
  if (!entityUrlFromSegments(request.params.type, request.params.instanceId)) return void 0;
4233
+ if (request.params.type === `principal`) return apiError(400, ErrCodeInvalidRequest, `Principal entities are built in and cannot be spawned directly`);
4003
4234
  const entityType = await ctx.entityManager.registry.getEntityType(request.params.type);
4004
4235
  if (!entityType) return apiError(404, ErrCodeUnknownEntityType, `Entity type "${request.params.type}" not found`);
4005
4236
  return void 0;
@@ -4008,7 +4239,8 @@ async function listEntities({ query }, ctx) {
4008
4239
  const { entities: entities$1 } = await ctx.entityManager.registry.listEntities({
4009
4240
  type: firstQueryValue$1(query.type),
4010
4241
  status: firstQueryValue$1(query.status),
4011
- parent: firstQueryValue$1(query.parent)
4242
+ parent: firstQueryValue$1(query.parent),
4243
+ created_by: firstQueryValue$1(query.created_by)
4012
4244
  });
4013
4245
  return json(entities$1.map((entity) => toPublicEntity(entity)));
4014
4246
  }
@@ -4018,6 +4250,8 @@ async function registerEntitiesSource(request, ctx) {
4018
4250
  return json(result);
4019
4251
  }
4020
4252
  async function upsertSchedule(request, ctx) {
4253
+ const principalMutationError = rejectPrincipalEntityMutation(request, `scheduled`);
4254
+ if (principalMutationError) return principalMutationError;
4021
4255
  const parsed = routeBody(request);
4022
4256
  const { entityUrl } = requireExistingEntityRoute(request);
4023
4257
  const scheduleId = decodeURIComponent(request.params.scheduleId);
@@ -4033,12 +4267,13 @@ async function upsertSchedule(request, ctx) {
4033
4267
  return json(result);
4034
4268
  }
4035
4269
  if (parsed.scheduleType === `future_send`) {
4270
+ if (parsed.from !== void 0 && parsed.from !== ctx.principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from must match Electric-Principal`);
4036
4271
  const result = await ctx.entityManager.upsertFutureSendSchedule(entityUrl, {
4037
4272
  id: scheduleId,
4038
4273
  payload: parsed.payload,
4039
4274
  targetUrl: parsed.targetUrl,
4040
4275
  fireAt: parsed.fireAt,
4041
- from: parsed.from,
4276
+ senderUrl: ctx.principal.url,
4042
4277
  messageType: parsed.messageType
4043
4278
  });
4044
4279
  return json(result);
@@ -4046,11 +4281,15 @@ async function upsertSchedule(request, ctx) {
4046
4281
  throw new Error(`schedule schema accepted an unknown scheduleType`);
4047
4282
  }
4048
4283
  async function deleteSchedule(request, ctx) {
4284
+ const principalMutationError = rejectPrincipalEntityMutation(request, `unscheduled`);
4285
+ if (principalMutationError) return principalMutationError;
4049
4286
  const { entityUrl } = requireExistingEntityRoute(request);
4050
4287
  const result = await ctx.entityManager.deleteSchedule(entityUrl, { id: decodeURIComponent(request.params.scheduleId) });
4051
4288
  return json(result);
4052
4289
  }
4053
4290
  async function setTag(request, ctx) {
4291
+ const principalMutationError = rejectPrincipalEntityMutation(request, `tagged`);
4292
+ if (principalMutationError) return principalMutationError;
4054
4293
  const parsed = routeBody(request);
4055
4294
  const { entityUrl } = requireExistingEntityRoute(request);
4056
4295
  const token = writeTokenFromRequest(request);
@@ -4058,12 +4297,16 @@ async function setTag(request, ctx) {
4058
4297
  return json(toPublicEntity(updated));
4059
4298
  }
4060
4299
  async function removeTag(request, ctx) {
4300
+ const principalMutationError = rejectPrincipalEntityMutation(request, `untagged`);
4301
+ if (principalMutationError) return principalMutationError;
4061
4302
  const { entityUrl } = requireExistingEntityRoute(request);
4062
4303
  const token = writeTokenFromRequest(request);
4063
4304
  const updated = await ctx.entityManager.removeTag(entityUrl, decodeURIComponent(request.params.tagKey), token);
4064
4305
  return json(toPublicEntity(updated));
4065
4306
  }
4066
4307
  async function forkEntity(request, ctx) {
4308
+ const principalMutationError = rejectPrincipalEntityMutation(request, `forked`);
4309
+ if (principalMutationError) return principalMutationError;
4067
4310
  const parsed = routeBody(request);
4068
4311
  const { entityUrl, entity } = requireExistingEntityRoute(request);
4069
4312
  await assertDispatchPolicyAllowed(ctx, entity.dispatch_policy);
@@ -4079,27 +4322,47 @@ async function forkEntity(request, ctx) {
4079
4322
  }
4080
4323
  async function sendEntity(request, ctx) {
4081
4324
  const parsed = routeBody(request);
4325
+ const principal = ctx.principal;
4326
+ if (parsed.from !== void 0 && parsed.from !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from must match Electric-Principal`);
4327
+ await ctx.entityManager.ensurePrincipal(principal);
4082
4328
  const { entityUrl, entity } = requireExistingEntityRoute(request);
4083
4329
  if (!entity.dispatch_policy) {
4084
4330
  const updatedEntity = await backfillEntityDispatchPolicy(ctx, entity);
4085
4331
  await linkEntityDispatchSubscription(ctx, updatedEntity);
4086
4332
  }
4087
4333
  if (parsed.afterMs && parsed.afterMs > 0) await ctx.entityManager.enqueueDelayedSend(entityUrl, {
4088
- from: parsed.from,
4334
+ from: principal.url,
4089
4335
  payload: parsed.payload,
4090
4336
  key: parsed.key,
4091
- type: parsed.type
4337
+ type: parsed.type,
4338
+ mode: parsed.mode,
4339
+ position: parsed.position
4092
4340
  }, new Date(Date.now() + parsed.afterMs));
4093
4341
  else await ctx.entityManager.send(entityUrl, {
4094
- from: parsed.from,
4342
+ from: principal.url,
4095
4343
  payload: parsed.payload,
4096
4344
  key: parsed.key,
4097
- type: parsed.type
4345
+ type: parsed.type,
4346
+ mode: parsed.mode,
4347
+ position: parsed.position
4098
4348
  });
4099
4349
  return status(204);
4100
4350
  }
4351
+ async function updateInboxMessage(request, ctx) {
4352
+ const parsed = routeBody(request);
4353
+ const { entityUrl } = requireExistingEntityRoute(request);
4354
+ await ctx.entityManager.updateInboxMessage(entityUrl, decodeURIComponent(request.params.messageKey), parsed);
4355
+ return status(204);
4356
+ }
4357
+ async function deleteInboxMessage(request, ctx) {
4358
+ const { entityUrl } = requireExistingEntityRoute(request);
4359
+ await ctx.entityManager.deleteInboxMessage(entityUrl, decodeURIComponent(request.params.messageKey));
4360
+ return status(204);
4361
+ }
4101
4362
  async function spawnEntity(request, ctx) {
4102
4363
  const parsed = routeBody(request);
4364
+ const principal = ctx.principal;
4365
+ await ctx.entityManager.ensurePrincipal(principal);
4103
4366
  const dispatchPolicy = await resolveEffectiveDispatchPolicyForSpawn(ctx, request.params.type, {
4104
4367
  dispatchPolicy: parsed.dispatch_policy,
4105
4368
  parent: parsed.parent
@@ -4112,11 +4375,12 @@ async function spawnEntity(request, ctx) {
4112
4375
  parent: parsed.parent,
4113
4376
  dispatch_policy: dispatchPolicy,
4114
4377
  initialMessage: void 0,
4115
- wake: parsed.wake
4378
+ wake: parsed.wake,
4379
+ created_by: principal.url
4116
4380
  });
4117
4381
  await linkEntityDispatchSubscription(ctx, entity);
4118
4382
  if (parsed.initialMessage !== void 0) await ctx.entityManager.send(entity.url, {
4119
- from: parsed.parent ?? `spawn`,
4383
+ from: principal.url,
4120
4384
  payload: parsed.initialMessage
4121
4385
  });
4122
4386
  return json({
@@ -4134,6 +4398,8 @@ function headEntity() {
4134
4398
  return status(200);
4135
4399
  }
4136
4400
  async function killEntity(request, ctx) {
4401
+ const principalMutationError = rejectPrincipalEntityMutation(request, `killed`);
4402
+ if (principalMutationError) return principalMutationError;
4137
4403
  const { entityUrl, entity } = requireExistingEntityRoute(request);
4138
4404
  await unlinkEntityDispatchSubscription(ctx, entity);
4139
4405
  const result = await ctx.entityManager.kill(entityUrl);
@@ -4280,7 +4546,7 @@ function applyCors(response) {
4280
4546
  const headers = new Headers(response.headers);
4281
4547
  headers.set(`access-control-allow-origin`, `*`);
4282
4548
  headers.set(`access-control-allow-methods`, `GET, POST, PUT, PATCH, DELETE, OPTIONS`);
4283
- headers.set(`access-control-allow-headers`, `content-type, authorization, electric-claim-token, x-electric-asserted-email, x-electric-asserted-name, ngrok-skip-browser-warning`);
4549
+ headers.set(`access-control-allow-headers`, `content-type, authorization, electric-claim-token, ngrok-skip-browser-warning`);
4284
4550
  headers.set(`access-control-expose-headers`, `*`);
4285
4551
  return new Response(response.body, {
4286
4552
  status: response.status,
@@ -4360,9 +4626,9 @@ function firstQueryValue(value) {
4360
4626
  }
4361
4627
  async function registerRunner(request, ctx) {
4362
4628
  const parsed = routeBody(request);
4363
- const ownerUserId = parsed.owner_user_id ?? ctx.authenticatedUser?.userId;
4629
+ const ownerUserId = parsed.owner_user_id ?? ctx.principal?.key;
4364
4630
  if (!ownerUserId) throw new ElectricAgentsError(ErrCodeInvalidRequest, `owner_user_id is required when no authenticated user is present`, 400);
4365
- if (ctx.authenticatedUser && ownerUserId !== ctx.authenticatedUser.userId) throw new ElectricAgentsError(ErrCodeUnauthorized, `owner_user_id must match the authenticated user`, 403);
4631
+ if (ctx.principal && ownerUserId !== ctx.principal.key) throw new ElectricAgentsError(ErrCodeUnauthorized, `owner_user_id must match the authenticated user`, 403);
4366
4632
  const runner = await ctx.entityManager.registry.createRunner({
4367
4633
  id: parsed.id,
4368
4634
  ownerUserId,
@@ -4376,8 +4642,8 @@ async function registerRunner(request, ctx) {
4376
4642
  }
4377
4643
  async function listRunners(request, ctx) {
4378
4644
  const requestedOwner = firstQueryValue(request.query.owner_user_id);
4379
- if (ctx.authenticatedUser && requestedOwner && requestedOwner !== ctx.authenticatedUser.userId) throw new ElectricAgentsError(ErrCodeUnauthorized, `owner_user_id must match the authenticated user`, 403);
4380
- const runners$1 = await ctx.entityManager.registry.listRunners({ ownerUserId: ctx.authenticatedUser?.userId ?? requestedOwner });
4645
+ if (ctx.principal && requestedOwner && requestedOwner !== ctx.principal.key) throw new ElectricAgentsError(ErrCodeUnauthorized, `owner_user_id must match the authenticated user`, 403);
4646
+ const runners$1 = await ctx.entityManager.registry.listRunners({ ownerUserId: ctx.principal?.key ?? requestedOwner });
4381
4647
  return json(runners$1);
4382
4648
  }
4383
4649
  async function getRunner(request, ctx) {
@@ -4415,9 +4681,8 @@ async function setRunnerStatus(request, ctx, adminStatus) {
4415
4681
  }
4416
4682
  async function claimWake(request, ctx) {
4417
4683
  const runnerId = routeParam$1(request, `id`);
4418
- if (!ctx.authenticatedUser) throw new ElectricAgentsError(ErrCodeUnauthorized, `Authentication is required to claim runner work`, 401);
4419
4684
  const runner = await requireRunner(ctx, runnerId);
4420
- if (runner.owner_user_id !== ctx.authenticatedUser.userId) throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner claim requires the authenticated owner`, 403);
4685
+ if (ctx.principal && runner.owner_user_id !== ctx.principal.key) throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner claim requires the authenticated owner`, 403);
4421
4686
  if (runner.admin_status !== `enabled`) throw new ElectricAgentsError(ErrCodeNotRunning, `Runner is disabled`, 409);
4422
4687
  const parsed = routeBody(request);
4423
4688
  const expectedSubscriptionId = subscriptionIdForDispatchTarget({
@@ -4449,8 +4714,8 @@ async function requireRunner(ctx, runnerId) {
4449
4714
  return runner;
4450
4715
  }
4451
4716
  function assertRunnerOwnerIfAuthenticated(ctx, ownerUserId) {
4452
- if (!ctx.authenticatedUser) return;
4453
- if (ownerUserId === ctx.authenticatedUser.userId) return;
4717
+ if (!ctx.principal) return;
4718
+ if (ownerUserId === ctx.principal.key) return;
4454
4719
  throw new ElectricAgentsError(ErrCodeUnauthorized, `Runner access requires the authenticated owner`, 403);
4455
4720
  }
4456
4721
  async function notificationFromClaim(ctx, input) {
@@ -4507,8 +4772,10 @@ async function notificationFromClaim(ctx, input) {
4507
4772
  url: entity.url,
4508
4773
  streams: entity.streams,
4509
4774
  tags: entity.tags,
4510
- spawnArgs: entity.spawn_args
4511
- }
4775
+ spawnArgs: entity.spawn_args,
4776
+ createdBy: entity.created_by
4777
+ },
4778
+ principal: principalFromCreatedBy(entity.created_by)
4512
4779
  };
4513
4780
  }
4514
4781
 
@@ -4916,7 +5183,7 @@ const ENTITY_SHAPE_COLUMNS = [
4916
5183
  `updated_at`
4917
5184
  ];
4918
5185
  function parseElectricOffset(offset) {
4919
- if (offset === `-1` || offset === `now`) return offset;
5186
+ if (offset === `-1`) return offset;
4920
5187
  return /^\d+_\d+$/.test(offset) ? offset : null;
4921
5188
  }
4922
5189
  function sameMember(left, right) {
@@ -6128,6 +6395,7 @@ var ElectricAgentsTenantRuntime = class {
6128
6395
  const fireAtRaw = value.fireAt;
6129
6396
  const producerId = value.producerId;
6130
6397
  const targetUrl = value.targetUrl;
6398
+ const senderUrl = typeof value.senderUrl === `string` ? value.senderUrl : ownerEntityUrl;
6131
6399
  if (typeof fireAtRaw !== `string` || typeof producerId !== `string` || typeof targetUrl !== `string`) {
6132
6400
  serverLog.warn(`[agent-server] invalid future_send manifest entry for ${ownerEntityUrl}/${manifestKey}`);
6133
6401
  return;
@@ -6139,7 +6407,7 @@ var ElectricAgentsTenantRuntime = class {
6139
6407
  }
6140
6408
  await this.scheduler.syncManifestDelayedSend(ownerEntityUrl, manifestKey, {
6141
6409
  entityUrl: targetUrl,
6142
- from: typeof value.from === `string` ? value.from : ownerEntityUrl,
6410
+ from: senderUrl,
6143
6411
  payload: value.payload,
6144
6412
  key: `scheduled-${producerId}`,
6145
6413
  type: typeof value.messageType === `string` ? value.messageType : void 0,
@@ -6153,6 +6421,7 @@ var ElectricAgentsTenantRuntime = class {
6153
6421
  kind: `schedule`,
6154
6422
  scheduleType: `future_send`,
6155
6423
  targetUrl,
6424
+ senderUrl,
6156
6425
  fireAt: fireAt.toISOString(),
6157
6426
  producerId,
6158
6427
  status: `pending`
@@ -7062,6 +7331,7 @@ var ElectricAgentsServer = class {
7062
7331
  });
7063
7332
  this.electricAgentsManager = this.standaloneRuntime.manager;
7064
7333
  this.entityBridgeManager = this.standaloneRuntime.entityBridgeManager;
7334
+ await this.electricAgentsManager.ensurePrincipalEntityType();
7065
7335
  const serverAdapter = createServerAdapter((request) => this.handleRequest(request));
7066
7336
  const server = createServer(serverAdapter);
7067
7337
  this.server = server;
@@ -7139,13 +7409,30 @@ var ElectricAgentsServer = class {
7139
7409
  }
7140
7410
  async handleRequest(request) {
7141
7411
  if (!this._url || !this.standaloneRuntime || !this.electricAgentsManager || !this.entityBridgeManager || !this.pgDb || !this.streamsAgent || !this.options.durableStreamsUrl) return new Response(null, { status: 503 });
7142
- return await ossServerRouter.fetch(request, await this.buildTenantContext(request));
7412
+ try {
7413
+ return await ossServerRouter.fetch(request, await this.buildTenantContext(request));
7414
+ } catch (error) {
7415
+ if (error instanceof ElectricAgentsError) return apiError(error.status, error.code, error.message, error.details);
7416
+ throw error;
7417
+ }
7418
+ }
7419
+ allowDevPrincipalFallback() {
7420
+ if (this.options.allowDevPrincipalFallback !== void 0) return this.options.allowDevPrincipalFallback;
7421
+ return process.env.ELECTRIC_INSECURE === `true` || process.env.NODE_ENV !== `production` || Boolean(this.options.durableStreamsServer);
7143
7422
  }
7144
7423
  async buildTenantContext(request) {
7145
7424
  if (!this.standaloneRuntime || !this.electricAgentsManager || !this.entityBridgeManager || !this.pgDb || !this.streamsAgent || !this.options.durableStreamsUrl) throw new Error(`agents-server runtime is not started`);
7425
+ let principal;
7426
+ try {
7427
+ principal = await this.options.authenticateRequest?.(request) ?? getPrincipalFromRequest(request);
7428
+ } catch (error) {
7429
+ throw new ElectricAgentsError(ErrCodeInvalidRequest, error instanceof Error ? error.message : `Invalid principal`, 400);
7430
+ }
7431
+ if (!principal && this.allowDevPrincipalFallback()) principal = getDevPrincipal();
7432
+ if (!principal) throw new ElectricAgentsError(ErrCodeUnauthorized, `Missing Electric-Principal`, 401);
7146
7433
  return {
7147
7434
  service: this.tenantId,
7148
- authenticatedUser: await this.options.authenticateRequest?.(request) ?? void 0,
7435
+ principal,
7149
7436
  publicUrl: this.publicUrl,
7150
7437
  localUrl: this._url,
7151
7438
  durableStreamsUrl: this.options.durableStreamsUrl,
@@ -7223,9 +7510,7 @@ function resolveElectricAgentsEntrypointOptions(env = process.env, cwd = process
7223
7510
  const electricUrl = readEnv(env, [`ELECTRIC_AGENTS_ELECTRIC_URL`, `ELECTRIC_URL`]);
7224
7511
  const electricSecret = readEnv(env, [`ELECTRIC_AGENTS_ELECTRIC_SECRET`]);
7225
7512
  const baseUrl = readEnv(env, [`ELECTRIC_AGENTS_BASE_URL`, `BASE_URL`]);
7226
- const authenticateRequest = createDevAssertedAuthenticateRequest(devAssertedAuthOptionsFromEnv(env));
7227
7513
  return {
7228
- ...authenticateRequest ? { authenticateRequest } : {},
7229
7514
  service: readEnv(env, [`ELECTRIC_AGENTS_SERVICE`, `SERVICE`]),
7230
7515
  tenantId: readEnv(env, [`ELECTRIC_AGENTS_TENANT_ID`, `TENANT_ID`]),
7231
7516
  baseUrl: baseUrl ? validateUrl(`base URL`, baseUrl) : void 0,