@electric-ax/agents-server 0.4.20 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ import { DurableStreamTestServer } from "@durable-streams/server";
4
4
  import { createServer } from "node:http";
5
5
  import { createServerAdapter } from "@whatwg-node/server";
6
6
  import { Agent } from "undici";
7
- import { COMPOSER_INPUT_MESSAGE_TYPE, appendPathToUrl, assertTags, buildEventSourceManifestEntry, buildTagsIndex, canonicalPgSyncOptions, createEntityRegistry, createRuntimeHandler, entityStateSchema, eventSourceSubscriptionManifestKey, getCronStreamPath, getCronStreamPathFromSpec, getEntitiesStreamPath, getNextCronFireAt, getPgSyncStreamPath, getSharedStateStreamPath, getWebhookStreamPath, hashString, manifestChildKey, manifestSharedStateKey, manifestSourceKey, normalizeTags, parseCronStreamPath, resolveCronScheduleSpec, resolveEventSourceSubscription, sourceRefForPgSync, sourceRefForTags, validateComposerInputPayload, validateSlashCommandDefinitions, verifyWebhookSignature } from "@electric-ax/agents-runtime";
7
+ import { COMMENTS_CONTRACT, COMPOSER_INPUT_MESSAGE_TYPE, appendPathToUrl, assertTags, buildEventSourceManifestEntry, buildTagsIndex, canonicalPgSyncOptions, createEntityRegistry, createRuntimeHandler, entityStateSchema, eventSourceSubscriptionManifestKey, getCronStreamPath, getCronStreamPathFromSpec, getEntitiesStreamPath, getNextCronFireAt, getPgSyncStreamPath, getSharedStateStreamPath, getWebhookStreamPath, hashString, manifestChildKey, manifestSharedStateKey, manifestSourceKey, normalizeTags, parseCronStreamPath, resolveCronScheduleSpec, resolveEventSourceSubscription, sourceRefForPgSync, sourceRefForTags, validateComposerInputPayload, validateSlashCommandDefinitions, verifyWebhookSignature } from "@electric-ax/agents-runtime";
8
8
  import fs, { existsSync } from "node:fs";
9
9
  import path, { dirname, resolve } from "node:path";
10
10
  import { drizzle } from "drizzle-orm/postgres-js";
@@ -65,6 +65,7 @@ const entityTypes = pgTable(`entity_types`, {
65
65
  creationSchema: jsonb(`creation_schema`),
66
66
  inboxSchemas: jsonb(`inbox_schemas`),
67
67
  stateSchemas: jsonb(`state_schemas`),
68
+ externallyWritableCollections: jsonb(`externally_writable_collections`).$type(),
68
69
  slashCommands: jsonb(`slash_commands`),
69
70
  serveEndpoint: text(`serve_endpoint`),
70
71
  defaultDispatchPolicy: jsonb(`default_dispatch_policy`),
@@ -2796,6 +2797,7 @@ var PostgresRegistry = class {
2796
2797
  creationSchema: et.creation_schema ?? null,
2797
2798
  inboxSchemas: et.inbox_schemas ?? null,
2798
2799
  stateSchemas: et.state_schemas ?? null,
2800
+ externallyWritableCollections: et.externally_writable_collections ?? null,
2799
2801
  slashCommands: et.slash_commands ?? null,
2800
2802
  serveEndpoint: et.serve_endpoint ?? null,
2801
2803
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
@@ -2809,6 +2811,7 @@ var PostgresRegistry = class {
2809
2811
  creationSchema: et.creation_schema ?? null,
2810
2812
  inboxSchemas: et.inbox_schemas ?? null,
2811
2813
  stateSchemas: et.state_schemas ?? null,
2814
+ externallyWritableCollections: et.externally_writable_collections ?? null,
2812
2815
  slashCommands: et.slash_commands ?? null,
2813
2816
  serveEndpoint: et.serve_endpoint ?? null,
2814
2817
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
@@ -2827,6 +2830,7 @@ var PostgresRegistry = class {
2827
2830
  creationSchema: et.creation_schema ?? null,
2828
2831
  inboxSchemas: et.inbox_schemas ?? null,
2829
2832
  stateSchemas: et.state_schemas ?? null,
2833
+ externallyWritableCollections: et.externally_writable_collections ?? null,
2830
2834
  slashCommands: et.slash_commands ?? null,
2831
2835
  serveEndpoint: et.serve_endpoint ?? null,
2832
2836
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
@@ -2854,6 +2858,7 @@ var PostgresRegistry = class {
2854
2858
  creationSchema: et.creation_schema ?? null,
2855
2859
  inboxSchemas: et.inbox_schemas ?? null,
2856
2860
  stateSchemas: et.state_schemas ?? null,
2861
+ externallyWritableCollections: et.externally_writable_collections ?? null,
2857
2862
  slashCommands: et.slash_commands ?? null,
2858
2863
  serveEndpoint: et.serve_endpoint ?? null,
2859
2864
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
@@ -3499,6 +3504,7 @@ var PostgresRegistry = class {
3499
3504
  creation_schema: row.creationSchema,
3500
3505
  inbox_schemas: row.inboxSchemas,
3501
3506
  state_schemas: row.stateSchemas,
3507
+ externally_writable_collections: row.externallyWritableCollections ?? void 0,
3502
3508
  slash_commands: row.slashCommands ?? void 0,
3503
3509
  serve_endpoint: row.serveEndpoint ?? void 0,
3504
3510
  default_dispatch_policy: row.defaultDispatchPolicy ?? void 0,
@@ -3861,6 +3867,7 @@ var EntityManager = class {
3861
3867
  creation_schema: req.creation_schema,
3862
3868
  inbox_schemas: req.inbox_schemas,
3863
3869
  state_schemas: req.state_schemas,
3870
+ externally_writable_collections: req.externally_writable_collections,
3864
3871
  slash_commands: req.slash_commands,
3865
3872
  serve_endpoint: req.serve_endpoint,
3866
3873
  default_dispatch_policy: defaultDispatchPolicy,
@@ -4956,6 +4963,40 @@ var EntityManager = class {
4956
4963
  throw err;
4957
4964
  }
4958
4965
  }
4966
+ async writeCollection(entityUrl, collection, req) {
4967
+ const entity = await this.registry.getEntity(entityUrl);
4968
+ if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
4969
+ const { externallyWritableCollections } = await this.getEffectiveSchemas(entity);
4970
+ const config = externallyWritableCollections?.[collection];
4971
+ if (!config) throw new ElectricAgentsError(ErrCodeUnauthorized, `Collection "${collection}" is not writable`, 403);
4972
+ const allowedOperations = config.operations ?? [`insert`];
4973
+ if (!allowedOperations.includes(req.operation)) throw new ElectricAgentsError(ErrCodeUnauthorized, `Operation "${req.operation}" is not allowed on collection "${collection}"`, 403);
4974
+ if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
4975
+ if (this.isForkWorkLockedEntity(entityUrl)) this.assertEntityNotForkWorkLocked(entityUrl);
4976
+ if (req.operation !== `delete` && (req.value === void 0 || req.value === null)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `value is required for ${req.operation}`, 400);
4977
+ if (req.operation !== `insert` && !req.key) throw new ElectricAgentsError(ErrCodeInvalidRequest, `key is required for ${req.operation}`, 400);
4978
+ const key = req.key ?? `${collection}-${randomUUID()}`;
4979
+ const event = {
4980
+ type: config.type,
4981
+ key,
4982
+ headers: {
4983
+ operation: req.operation,
4984
+ timestamp: new Date().toISOString(),
4985
+ principal: req.principal
4986
+ }
4987
+ };
4988
+ if (req.operation !== `delete`) event.value = req.value;
4989
+ const validationError = await this.validateWriteEvent(entity, event);
4990
+ if (validationError) throw new ElectricAgentsError(validationError.code, validationError.message, validationError.status);
4991
+ const encoded = this.encodeChangeEvent(event);
4992
+ try {
4993
+ await this.streamClient.append(entity.streams.main, encoded);
4994
+ } catch (err) {
4995
+ if (this.isClosedStreamError(err)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409);
4996
+ throw err;
4997
+ }
4998
+ return { key };
4999
+ }
4959
5000
  async updateInboxMessage(entityUrl, key, req) {
4960
5001
  const entity = await this.registry.getEntity(entityUrl);
4961
5002
  if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
@@ -5648,7 +5689,8 @@ var EntityManager = class {
5648
5689
  async getEffectiveSchemas(entity) {
5649
5690
  if (!entity.type) return {
5650
5691
  inboxSchemas: entity.inbox_schemas,
5651
- stateSchemas: entity.state_schemas
5692
+ stateSchemas: entity.state_schemas,
5693
+ externallyWritableCollections: void 0
5652
5694
  };
5653
5695
  const latestType = await this.registry.getEntityType(entity.type);
5654
5696
  return {
@@ -5659,7 +5701,8 @@ var EntityManager = class {
5659
5701
  stateSchemas: latestType?.state_schemas ? {
5660
5702
  ...entity.state_schemas ?? {},
5661
5703
  ...latestType.state_schemas
5662
- } : entity.state_schemas
5704
+ } : entity.state_schemas,
5705
+ externallyWritableCollections: latestType?.externally_writable_collections
5663
5706
  };
5664
5707
  }
5665
5708
  isClosedStreamError(err) {
@@ -5922,6 +5965,15 @@ const spawnBodySchema = Type.Object({
5922
5965
  manifestKey: Type.Optional(Type.String())
5923
5966
  }))
5924
5967
  });
5968
+ const writeCollectionBodySchema = Type.Object({
5969
+ operation: Type.Union([
5970
+ Type.Literal(`insert`),
5971
+ Type.Literal(`update`),
5972
+ Type.Literal(`delete`)
5973
+ ]),
5974
+ key: Type.Optional(Type.String()),
5975
+ value: Type.Optional(Type.Record(Type.String(), Type.Unknown()))
5976
+ }, { additionalProperties: false });
5925
5977
  const sendBodySchema = Type.Object({
5926
5978
  payload: Type.Optional(Type.Unknown()),
5927
5979
  key: Type.Optional(Type.String()),
@@ -6054,6 +6106,7 @@ entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, withEntityPermissi
6054
6106
  entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`delete`), killEntity);
6055
6107
  entitiesRouter.post(`/:type/:instanceId/signal`, withExistingEntity, withSchema(signalBodySchema), withEntityPermission(`signal`), signalEntity);
6056
6108
  entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), withEntityPermission(`write`), sendEntity);
6109
+ entitiesRouter.post(`/:type/:instanceId/collections/:collection`, withExistingEntity, withSchema(writeCollectionBodySchema), withEntityPermission(`write`), writeCollection);
6057
6110
  entitiesRouter.post(`/:type/:instanceId/attachments`, withExistingEntity, withEntityPermission(`write`), createAttachment);
6058
6111
  entitiesRouter.get(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, withEntityPermission(`read`), readAttachment);
6059
6112
  entitiesRouter.delete(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, withEntityPermission(`write`), deleteAttachment);
@@ -6452,6 +6505,23 @@ async function sendEntity(request, ctx) {
6452
6505
  const result = await ctx.entityManager.send(entityUrl, sendReq);
6453
6506
  return json(result);
6454
6507
  }
6508
+ async function writeCollection(request, ctx) {
6509
+ const parsed = routeBody(request);
6510
+ await ctx.entityManager.ensurePrincipal(ctx.principal);
6511
+ const { entityUrl } = requireExistingEntityRoute(request);
6512
+ const collection = request.params.collection;
6513
+ const result = await ctx.entityManager.writeCollection(entityUrl, collection, {
6514
+ operation: parsed.operation,
6515
+ key: parsed.key,
6516
+ value: parsed.value,
6517
+ principal: {
6518
+ url: ctx.principal.url,
6519
+ kind: ctx.principal.kind,
6520
+ id: ctx.principal.id
6521
+ }
6522
+ });
6523
+ return json(result, { status: parsed.operation === `insert` ? 201 : 200 });
6524
+ }
6455
6525
  async function createAttachment(request, ctx) {
6456
6526
  const principalMutationError = rejectPrincipalEntityMutation(request, `given attachments`);
6457
6527
  if (principalMutationError) return principalMutationError;
@@ -6549,8 +6619,13 @@ async function spawnEntity(request, ctx) {
6549
6619
  headers: { "x-write-token": entity.write_token }
6550
6620
  });
6551
6621
  }
6552
- function getEntity(request) {
6553
- return json(toPublicEntity(requireExistingEntityRoute(request).entity));
6622
+ async function getEntity(request, ctx) {
6623
+ const { entity } = requireExistingEntityRoute(request);
6624
+ const entityType = entity.type ? await ctx.entityManager.registry.getEntityType(entity.type) : null;
6625
+ return json({
6626
+ ...toPublicEntity(entity),
6627
+ ...entityType?.externally_writable_collections && { externally_writable_collections: entityType.externally_writable_collections }
6628
+ });
6554
6629
  }
6555
6630
  function headEntity() {
6556
6631
  return status(200);
@@ -6585,6 +6660,16 @@ async function signalEntity(request, ctx) {
6585
6660
  //#region src/routing/entity-types-router.ts
6586
6661
  const jsonObjectSchema = Type.Record(Type.String(), Type.Unknown());
6587
6662
  const schemaMapSchema = Type.Record(Type.String(), jsonObjectSchema);
6663
+ const externallyWritableCollectionsSchema = Type.Record(Type.String(), Type.Object({
6664
+ type: Type.String(),
6665
+ contract: Type.Optional(Type.String()),
6666
+ operations: Type.Optional(Type.Array(Type.Union([
6667
+ Type.Literal(`insert`),
6668
+ Type.Literal(`update`),
6669
+ Type.Literal(`delete`)
6670
+ ]))),
6671
+ principalColumn: Type.Optional(Type.String())
6672
+ }, { additionalProperties: false }));
6588
6673
  const slashCommandArgumentSchema = Type.Object({
6589
6674
  name: Type.String(),
6590
6675
  type: Type.Union([
@@ -6615,7 +6700,8 @@ const registerEntityTypeBodySchema = Type.Object({
6615
6700
  slash_commands: Type.Optional(Type.Array(slashCommandSchema)),
6616
6701
  serve_endpoint: Type.Optional(Type.String()),
6617
6702
  default_dispatch_policy: Type.Optional(dispatchPolicySchema),
6618
- permission_grants: Type.Optional(Type.Array(typePermissionGrantInputSchema))
6703
+ permission_grants: Type.Optional(Type.Array(typePermissionGrantInputSchema)),
6704
+ externally_writable_collections: Type.Optional(externallyWritableCollectionsSchema)
6619
6705
  }, { additionalProperties: false });
6620
6706
  const amendEntityTypeSchemasBodySchema = Type.Object({
6621
6707
  inbox_schemas: Type.Optional(schemaMapSchema),
@@ -6746,7 +6832,20 @@ function parseExpiresAt(value) {
6746
6832
  if (Number.isNaN(expiresAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid expires_at timestamp`, 400);
6747
6833
  return expiresAt;
6748
6834
  }
6835
+ /**
6836
+ * The `comments` collection name is reserved for the canonical comments
6837
+ * contract: the UI keys its comment affordances on it, so a divergent
6838
+ * collection registered under that name (or the contract mounted under
6839
+ * another name) would break that assumption silently.
6840
+ */
6841
+ function validateExternallyWritableCollections(collections) {
6842
+ for (const [name, config] of Object.entries(collections ?? {})) {
6843
+ if (name === `comments` && config.contract !== COMMENTS_CONTRACT) throw new ElectricAgentsError(ErrCodeInvalidRequest, `The externally-writable collection name "comments" is reserved for the "${COMMENTS_CONTRACT}" contract`, 400);
6844
+ if (config.contract === COMMENTS_CONTRACT && name !== `comments`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `The "${COMMENTS_CONTRACT}" contract must be registered under the collection name "comments"`, 400);
6845
+ }
6846
+ }
6749
6847
  function normalizeEntityTypeRequest(parsed) {
6848
+ validateExternallyWritableCollections(parsed.externally_writable_collections);
6750
6849
  const serveEndpoint = rewriteLoopbackWebhookUrl(parsed.serve_endpoint);
6751
6850
  return {
6752
6851
  name: parsed.name ?? ``,
@@ -6760,7 +6859,8 @@ function normalizeEntityTypeRequest(parsed) {
6760
6859
  type: `webhook`,
6761
6860
  url: serveEndpoint
6762
6861
  }] } : void 0),
6763
- permission_grants: parsed.permission_grants
6862
+ permission_grants: parsed.permission_grants,
6863
+ externally_writable_collections: parsed.externally_writable_collections
6764
6864
  };
6765
6865
  }
6766
6866
  function toPublicEntityType(entityType) {
package/dist/index.cjs CHANGED
@@ -79,6 +79,7 @@ const entityTypes = (0, drizzle_orm_pg_core.pgTable)(`entity_types`, {
79
79
  creationSchema: (0, drizzle_orm_pg_core.jsonb)(`creation_schema`),
80
80
  inboxSchemas: (0, drizzle_orm_pg_core.jsonb)(`inbox_schemas`),
81
81
  stateSchemas: (0, drizzle_orm_pg_core.jsonb)(`state_schemas`),
82
+ externallyWritableCollections: (0, drizzle_orm_pg_core.jsonb)(`externally_writable_collections`).$type(),
82
83
  slashCommands: (0, drizzle_orm_pg_core.jsonb)(`slash_commands`),
83
84
  serveEndpoint: (0, drizzle_orm_pg_core.text)(`serve_endpoint`),
84
85
  defaultDispatchPolicy: (0, drizzle_orm_pg_core.jsonb)(`default_dispatch_policy`),
@@ -866,6 +867,7 @@ var PostgresRegistry = class {
866
867
  creationSchema: et.creation_schema ?? null,
867
868
  inboxSchemas: et.inbox_schemas ?? null,
868
869
  stateSchemas: et.state_schemas ?? null,
870
+ externallyWritableCollections: et.externally_writable_collections ?? null,
869
871
  slashCommands: et.slash_commands ?? null,
870
872
  serveEndpoint: et.serve_endpoint ?? null,
871
873
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
@@ -879,6 +881,7 @@ var PostgresRegistry = class {
879
881
  creationSchema: et.creation_schema ?? null,
880
882
  inboxSchemas: et.inbox_schemas ?? null,
881
883
  stateSchemas: et.state_schemas ?? null,
884
+ externallyWritableCollections: et.externally_writable_collections ?? null,
882
885
  slashCommands: et.slash_commands ?? null,
883
886
  serveEndpoint: et.serve_endpoint ?? null,
884
887
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
@@ -897,6 +900,7 @@ var PostgresRegistry = class {
897
900
  creationSchema: et.creation_schema ?? null,
898
901
  inboxSchemas: et.inbox_schemas ?? null,
899
902
  stateSchemas: et.state_schemas ?? null,
903
+ externallyWritableCollections: et.externally_writable_collections ?? null,
900
904
  slashCommands: et.slash_commands ?? null,
901
905
  serveEndpoint: et.serve_endpoint ?? null,
902
906
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
@@ -924,6 +928,7 @@ var PostgresRegistry = class {
924
928
  creationSchema: et.creation_schema ?? null,
925
929
  inboxSchemas: et.inbox_schemas ?? null,
926
930
  stateSchemas: et.state_schemas ?? null,
931
+ externallyWritableCollections: et.externally_writable_collections ?? null,
927
932
  slashCommands: et.slash_commands ?? null,
928
933
  serveEndpoint: et.serve_endpoint ?? null,
929
934
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
@@ -1569,6 +1574,7 @@ var PostgresRegistry = class {
1569
1574
  creation_schema: row.creationSchema,
1570
1575
  inbox_schemas: row.inboxSchemas,
1571
1576
  state_schemas: row.stateSchemas,
1577
+ externally_writable_collections: row.externallyWritableCollections ?? void 0,
1572
1578
  slash_commands: row.slashCommands ?? void 0,
1573
1579
  serve_endpoint: row.serveEndpoint ?? void 0,
1574
1580
  default_dispatch_policy: row.defaultDispatchPolicy ?? void 0,
@@ -3498,6 +3504,7 @@ var EntityManager = class {
3498
3504
  creation_schema: req.creation_schema,
3499
3505
  inbox_schemas: req.inbox_schemas,
3500
3506
  state_schemas: req.state_schemas,
3507
+ externally_writable_collections: req.externally_writable_collections,
3501
3508
  slash_commands: req.slash_commands,
3502
3509
  serve_endpoint: req.serve_endpoint,
3503
3510
  default_dispatch_policy: defaultDispatchPolicy,
@@ -4593,6 +4600,40 @@ var EntityManager = class {
4593
4600
  throw err;
4594
4601
  }
4595
4602
  }
4603
+ async writeCollection(entityUrl, collection, req) {
4604
+ const entity = await this.registry.getEntity(entityUrl);
4605
+ if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
4606
+ const { externallyWritableCollections } = await this.getEffectiveSchemas(entity);
4607
+ const config = externallyWritableCollections?.[collection];
4608
+ if (!config) throw new ElectricAgentsError(ErrCodeUnauthorized, `Collection "${collection}" is not writable`, 403);
4609
+ const allowedOperations = config.operations ?? [`insert`];
4610
+ if (!allowedOperations.includes(req.operation)) throw new ElectricAgentsError(ErrCodeUnauthorized, `Operation "${req.operation}" is not allowed on collection "${collection}"`, 403);
4611
+ if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
4612
+ if (this.isForkWorkLockedEntity(entityUrl)) this.assertEntityNotForkWorkLocked(entityUrl);
4613
+ if (req.operation !== `delete` && (req.value === void 0 || req.value === null)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `value is required for ${req.operation}`, 400);
4614
+ if (req.operation !== `insert` && !req.key) throw new ElectricAgentsError(ErrCodeInvalidRequest, `key is required for ${req.operation}`, 400);
4615
+ const key = req.key ?? `${collection}-${(0, node_crypto.randomUUID)()}`;
4616
+ const event = {
4617
+ type: config.type,
4618
+ key,
4619
+ headers: {
4620
+ operation: req.operation,
4621
+ timestamp: new Date().toISOString(),
4622
+ principal: req.principal
4623
+ }
4624
+ };
4625
+ if (req.operation !== `delete`) event.value = req.value;
4626
+ const validationError = await this.validateWriteEvent(entity, event);
4627
+ if (validationError) throw new ElectricAgentsError(validationError.code, validationError.message, validationError.status);
4628
+ const encoded = this.encodeChangeEvent(event);
4629
+ try {
4630
+ await this.streamClient.append(entity.streams.main, encoded);
4631
+ } catch (err) {
4632
+ if (this.isClosedStreamError(err)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409);
4633
+ throw err;
4634
+ }
4635
+ return { key };
4636
+ }
4596
4637
  async updateInboxMessage(entityUrl, key, req) {
4597
4638
  const entity = await this.registry.getEntity(entityUrl);
4598
4639
  if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
@@ -5285,7 +5326,8 @@ var EntityManager = class {
5285
5326
  async getEffectiveSchemas(entity) {
5286
5327
  if (!entity.type) return {
5287
5328
  inboxSchemas: entity.inbox_schemas,
5288
- stateSchemas: entity.state_schemas
5329
+ stateSchemas: entity.state_schemas,
5330
+ externallyWritableCollections: void 0
5289
5331
  };
5290
5332
  const latestType = await this.registry.getEntityType(entity.type);
5291
5333
  return {
@@ -5296,7 +5338,8 @@ var EntityManager = class {
5296
5338
  stateSchemas: latestType?.state_schemas ? {
5297
5339
  ...entity.state_schemas ?? {},
5298
5340
  ...latestType.state_schemas
5299
- } : entity.state_schemas
5341
+ } : entity.state_schemas,
5342
+ externallyWritableCollections: latestType?.externally_writable_collections
5300
5343
  };
5301
5344
  }
5302
5345
  isClosedStreamError(err) {
@@ -8534,6 +8577,15 @@ const spawnBodySchema = __sinclair_typebox.Type.Object({
8534
8577
  manifestKey: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
8535
8578
  }))
8536
8579
  });
8580
+ const writeCollectionBodySchema = __sinclair_typebox.Type.Object({
8581
+ operation: __sinclair_typebox.Type.Union([
8582
+ __sinclair_typebox.Type.Literal(`insert`),
8583
+ __sinclair_typebox.Type.Literal(`update`),
8584
+ __sinclair_typebox.Type.Literal(`delete`)
8585
+ ]),
8586
+ key: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
8587
+ value: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown()))
8588
+ }, { additionalProperties: false });
8537
8589
  const sendBodySchema = __sinclair_typebox.Type.Object({
8538
8590
  payload: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Unknown()),
8539
8591
  key: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
@@ -8666,6 +8718,7 @@ entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, withEntityPermissi
8666
8718
  entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`delete`), killEntity);
8667
8719
  entitiesRouter.post(`/:type/:instanceId/signal`, withExistingEntity, withSchema(signalBodySchema), withEntityPermission(`signal`), signalEntity);
8668
8720
  entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), withEntityPermission(`write`), sendEntity);
8721
+ entitiesRouter.post(`/:type/:instanceId/collections/:collection`, withExistingEntity, withSchema(writeCollectionBodySchema), withEntityPermission(`write`), writeCollection);
8669
8722
  entitiesRouter.post(`/:type/:instanceId/attachments`, withExistingEntity, withEntityPermission(`write`), createAttachment);
8670
8723
  entitiesRouter.get(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, withEntityPermission(`read`), readAttachment);
8671
8724
  entitiesRouter.delete(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, withEntityPermission(`write`), deleteAttachment);
@@ -9064,6 +9117,23 @@ async function sendEntity(request, ctx) {
9064
9117
  const result = await ctx.entityManager.send(entityUrl, sendReq);
9065
9118
  return (0, itty_router.json)(result);
9066
9119
  }
9120
+ async function writeCollection(request, ctx) {
9121
+ const parsed = routeBody(request);
9122
+ await ctx.entityManager.ensurePrincipal(ctx.principal);
9123
+ const { entityUrl } = requireExistingEntityRoute(request);
9124
+ const collection = request.params.collection;
9125
+ const result = await ctx.entityManager.writeCollection(entityUrl, collection, {
9126
+ operation: parsed.operation,
9127
+ key: parsed.key,
9128
+ value: parsed.value,
9129
+ principal: {
9130
+ url: ctx.principal.url,
9131
+ kind: ctx.principal.kind,
9132
+ id: ctx.principal.id
9133
+ }
9134
+ });
9135
+ return (0, itty_router.json)(result, { status: parsed.operation === `insert` ? 201 : 200 });
9136
+ }
9067
9137
  async function createAttachment(request, ctx) {
9068
9138
  const principalMutationError = rejectPrincipalEntityMutation(request, `given attachments`);
9069
9139
  if (principalMutationError) return principalMutationError;
@@ -9161,8 +9231,13 @@ async function spawnEntity(request, ctx) {
9161
9231
  headers: { "x-write-token": entity.write_token }
9162
9232
  });
9163
9233
  }
9164
- function getEntity(request) {
9165
- return (0, itty_router.json)(toPublicEntity(requireExistingEntityRoute(request).entity));
9234
+ async function getEntity(request, ctx) {
9235
+ const { entity } = requireExistingEntityRoute(request);
9236
+ const entityType = entity.type ? await ctx.entityManager.registry.getEntityType(entity.type) : null;
9237
+ return (0, itty_router.json)({
9238
+ ...toPublicEntity(entity),
9239
+ ...entityType?.externally_writable_collections && { externally_writable_collections: entityType.externally_writable_collections }
9240
+ });
9166
9241
  }
9167
9242
  function headEntity() {
9168
9243
  return (0, itty_router.status)(200);
@@ -9197,6 +9272,16 @@ async function signalEntity(request, ctx) {
9197
9272
  //#region src/routing/entity-types-router.ts
9198
9273
  const jsonObjectSchema = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown());
9199
9274
  const schemaMapSchema = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), jsonObjectSchema);
9275
+ const externallyWritableCollectionsSchema = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Object({
9276
+ type: __sinclair_typebox.Type.String(),
9277
+ contract: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
9278
+ operations: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(__sinclair_typebox.Type.Union([
9279
+ __sinclair_typebox.Type.Literal(`insert`),
9280
+ __sinclair_typebox.Type.Literal(`update`),
9281
+ __sinclair_typebox.Type.Literal(`delete`)
9282
+ ]))),
9283
+ principalColumn: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
9284
+ }, { additionalProperties: false }));
9200
9285
  const slashCommandArgumentSchema = __sinclair_typebox.Type.Object({
9201
9286
  name: __sinclair_typebox.Type.String(),
9202
9287
  type: __sinclair_typebox.Type.Union([
@@ -9227,7 +9312,8 @@ const registerEntityTypeBodySchema = __sinclair_typebox.Type.Object({
9227
9312
  slash_commands: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(slashCommandSchema)),
9228
9313
  serve_endpoint: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
9229
9314
  default_dispatch_policy: __sinclair_typebox.Type.Optional(dispatchPolicySchema),
9230
- permission_grants: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(typePermissionGrantInputSchema))
9315
+ permission_grants: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(typePermissionGrantInputSchema)),
9316
+ externally_writable_collections: __sinclair_typebox.Type.Optional(externallyWritableCollectionsSchema)
9231
9317
  }, { additionalProperties: false });
9232
9318
  const amendEntityTypeSchemasBodySchema = __sinclair_typebox.Type.Object({
9233
9319
  inbox_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
@@ -9358,7 +9444,20 @@ function parseExpiresAt(value) {
9358
9444
  if (Number.isNaN(expiresAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid expires_at timestamp`, 400);
9359
9445
  return expiresAt;
9360
9446
  }
9447
+ /**
9448
+ * The `comments` collection name is reserved for the canonical comments
9449
+ * contract: the UI keys its comment affordances on it, so a divergent
9450
+ * collection registered under that name (or the contract mounted under
9451
+ * another name) would break that assumption silently.
9452
+ */
9453
+ function validateExternallyWritableCollections(collections) {
9454
+ for (const [name, config] of Object.entries(collections ?? {})) {
9455
+ if (name === `comments` && config.contract !== __electric_ax_agents_runtime.COMMENTS_CONTRACT) throw new ElectricAgentsError(ErrCodeInvalidRequest, `The externally-writable collection name "comments" is reserved for the "${__electric_ax_agents_runtime.COMMENTS_CONTRACT}" contract`, 400);
9456
+ if (config.contract === __electric_ax_agents_runtime.COMMENTS_CONTRACT && name !== `comments`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `The "${__electric_ax_agents_runtime.COMMENTS_CONTRACT}" contract must be registered under the collection name "comments"`, 400);
9457
+ }
9458
+ }
9361
9459
  function normalizeEntityTypeRequest(parsed) {
9460
+ validateExternallyWritableCollections(parsed.externally_writable_collections);
9362
9461
  const serveEndpoint = rewriteLoopbackWebhookUrl(parsed.serve_endpoint);
9363
9462
  return {
9364
9463
  name: parsed.name ?? ``,
@@ -9372,7 +9471,8 @@ function normalizeEntityTypeRequest(parsed) {
9372
9471
  type: `webhook`,
9373
9472
  url: serveEndpoint
9374
9473
  }] } : void 0),
9375
- permission_grants: parsed.permission_grants
9474
+ permission_grants: parsed.permission_grants,
9475
+ externally_writable_collections: parsed.externally_writable_collections
9376
9476
  };
9377
9477
  }
9378
9478
  function toPublicEntityType(entityType) {