@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.
- package/dist/entrypoint.js +107 -7
- package/dist/index.cjs +106 -6
- package/dist/index.d.cts +2484 -2431
- package/dist/index.d.ts +2483 -2432
- package/dist/index.js +107 -7
- package/drizzle/0016_entity_type_externally_writable_collections.sql +1 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +4 -4
- package/src/db/schema.ts +4 -0
- package/src/electric-agents-types.ts +23 -0
- package/src/entity-manager.ts +126 -0
- package/src/entity-registry.ts +14 -0
- package/src/routing/entities-router.ts +61 -2
- package/src/routing/entity-types-router.ts +56 -0
package/dist/entrypoint.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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) {
|