@electric-ax/agents-server 0.4.16 → 0.4.18
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 +169 -12
- package/dist/index.cjs +168 -11
- package/dist/index.d.cts +304 -236
- package/dist/index.d.ts +304 -236
- package/dist/index.js +169 -12
- package/drizzle/0014_entity_type_slash_commands.sql +1 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +5 -5
- package/src/db/schema.ts +1 -0
- package/src/electric-agents-types.ts +4 -0
- package/src/entity-manager.ts +210 -12
- package/src/entity-registry.ts +7 -0
- package/src/routing/entities-router.ts +122 -1
- package/src/routing/entity-types-router.ts +23 -0
- package/src/utils/server-utils.ts +1 -1
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 { appendPathToUrl, assertTags, buildEventSourceManifestEntry, buildTagsIndex, createEntityRegistry, createRuntimeHandler, entityStateSchema, eventSourceSubscriptionManifestKey, getCronStreamPath, getCronStreamPathFromSpec, getEntitiesStreamPath, getNextCronFireAt, getSharedStateStreamPath, getWebhookStreamPath, hashString, manifestChildKey, manifestSharedStateKey, manifestSourceKey, normalizeTags, parseCronStreamPath, resolveCronScheduleSpec, resolveEventSourceSubscription, sourceRefForTags, verifyWebhookSignature } from "@electric-ax/agents-runtime";
|
|
7
|
+
import { COMPOSER_INPUT_MESSAGE_TYPE, appendPathToUrl, assertTags, buildEventSourceManifestEntry, buildTagsIndex, createEntityRegistry, createRuntimeHandler, entityStateSchema, eventSourceSubscriptionManifestKey, getCronStreamPath, getCronStreamPathFromSpec, getEntitiesStreamPath, getNextCronFireAt, getSharedStateStreamPath, getWebhookStreamPath, hashString, manifestChildKey, manifestSharedStateKey, manifestSourceKey, normalizeTags, parseCronStreamPath, resolveCronScheduleSpec, resolveEventSourceSubscription, 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";
|
|
@@ -64,6 +64,7 @@ const entityTypes = pgTable(`entity_types`, {
|
|
|
64
64
|
creationSchema: jsonb(`creation_schema`),
|
|
65
65
|
inboxSchemas: jsonb(`inbox_schemas`),
|
|
66
66
|
stateSchemas: jsonb(`state_schemas`),
|
|
67
|
+
slashCommands: jsonb(`slash_commands`),
|
|
67
68
|
serveEndpoint: text(`serve_endpoint`),
|
|
68
69
|
defaultDispatchPolicy: jsonb(`default_dispatch_policy`),
|
|
69
70
|
revision: integer(`revision`).notNull().default(1),
|
|
@@ -1152,7 +1153,7 @@ function buildElectricProxyTarget(options) {
|
|
|
1152
1153
|
permissionBypass: options.permissionBypass
|
|
1153
1154
|
}));
|
|
1154
1155
|
} else if (table === `entity_types`) {
|
|
1155
|
-
target.searchParams.set(`columns`, `"tenant_id","name","description","creation_schema","inbox_schemas","state_schemas","serve_endpoint","default_dispatch_policy","revision","created_at","updated_at"`);
|
|
1156
|
+
target.searchParams.set(`columns`, `"tenant_id","name","description","creation_schema","inbox_schemas","state_schemas","slash_commands","serve_endpoint","default_dispatch_policy","revision","created_at","updated_at"`);
|
|
1156
1157
|
applyShapeWhere(target, buildSpawnableEntityTypesWhere({
|
|
1157
1158
|
tenantId: options.tenantId,
|
|
1158
1159
|
principalUrl: options.principalUrl ?? ``,
|
|
@@ -2725,6 +2726,7 @@ var PostgresRegistry = class {
|
|
|
2725
2726
|
creationSchema: et.creation_schema ?? null,
|
|
2726
2727
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
2727
2728
|
stateSchemas: et.state_schemas ?? null,
|
|
2729
|
+
slashCommands: et.slash_commands ?? null,
|
|
2728
2730
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
2729
2731
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
2730
2732
|
revision: et.revision,
|
|
@@ -2737,6 +2739,7 @@ var PostgresRegistry = class {
|
|
|
2737
2739
|
creationSchema: et.creation_schema ?? null,
|
|
2738
2740
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
2739
2741
|
stateSchemas: et.state_schemas ?? null,
|
|
2742
|
+
slashCommands: et.slash_commands ?? null,
|
|
2740
2743
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
2741
2744
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
2742
2745
|
revision: et.revision,
|
|
@@ -2754,6 +2757,7 @@ var PostgresRegistry = class {
|
|
|
2754
2757
|
creationSchema: et.creation_schema ?? null,
|
|
2755
2758
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
2756
2759
|
stateSchemas: et.state_schemas ?? null,
|
|
2760
|
+
slashCommands: et.slash_commands ?? null,
|
|
2757
2761
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
2758
2762
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
2759
2763
|
revision: et.revision,
|
|
@@ -2780,6 +2784,7 @@ var PostgresRegistry = class {
|
|
|
2780
2784
|
creationSchema: et.creation_schema ?? null,
|
|
2781
2785
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
2782
2786
|
stateSchemas: et.state_schemas ?? null,
|
|
2787
|
+
slashCommands: et.slash_commands ?? null,
|
|
2783
2788
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
2784
2789
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
2785
2790
|
revision: et.revision,
|
|
@@ -3370,6 +3375,7 @@ var PostgresRegistry = class {
|
|
|
3370
3375
|
creation_schema: row.creationSchema,
|
|
3371
3376
|
inbox_schemas: row.inboxSchemas,
|
|
3372
3377
|
state_schemas: row.stateSchemas,
|
|
3378
|
+
slash_commands: row.slashCommands ?? void 0,
|
|
3373
3379
|
serve_endpoint: row.serveEndpoint ?? void 0,
|
|
3374
3380
|
default_dispatch_policy: row.defaultDispatchPolicy ?? void 0,
|
|
3375
3381
|
revision: row.revision,
|
|
@@ -3696,6 +3702,7 @@ var EntityManager = class {
|
|
|
3696
3702
|
this.validateSchema(req.creation_schema);
|
|
3697
3703
|
this.validateSchemaMap(req.inbox_schemas);
|
|
3698
3704
|
this.validateSchemaMap(req.state_schemas);
|
|
3705
|
+
this.validateSlashCommands(req.slash_commands);
|
|
3699
3706
|
const defaultDispatchPolicy = req.default_dispatch_policy ? this.validateDispatchPolicy(req.default_dispatch_policy, { label: `default_dispatch_policy` }) : void 0;
|
|
3700
3707
|
const existing = await this.registry.getEntityType(req.name);
|
|
3701
3708
|
const now = new Date().toISOString();
|
|
@@ -3705,6 +3712,7 @@ var EntityManager = class {
|
|
|
3705
3712
|
creation_schema: req.creation_schema,
|
|
3706
3713
|
inbox_schemas: req.inbox_schemas,
|
|
3707
3714
|
state_schemas: req.state_schemas,
|
|
3715
|
+
slash_commands: req.slash_commands,
|
|
3708
3716
|
serve_endpoint: req.serve_endpoint,
|
|
3709
3717
|
default_dispatch_policy: defaultDispatchPolicy,
|
|
3710
3718
|
revision: existing ? existing.revision + 1 : 1,
|
|
@@ -3866,6 +3874,18 @@ var EntityManager = class {
|
|
|
3866
3874
|
}
|
|
3867
3875
|
});
|
|
3868
3876
|
const initialEvents = [createdEvent];
|
|
3877
|
+
const slashCommandTimestamp = new Date().toISOString();
|
|
3878
|
+
for (const command of entityType.slash_commands ?? []) {
|
|
3879
|
+
const slashCommandEvent = entityStateSchema.slashCommands.insert({
|
|
3880
|
+
key: command.name,
|
|
3881
|
+
value: {
|
|
3882
|
+
...command,
|
|
3883
|
+
source: `static`,
|
|
3884
|
+
updated_at: slashCommandTimestamp
|
|
3885
|
+
}
|
|
3886
|
+
});
|
|
3887
|
+
initialEvents.push(slashCommandEvent);
|
|
3888
|
+
}
|
|
3869
3889
|
if (req.initialMessage !== void 0) {
|
|
3870
3890
|
const msgNow = new Date().toISOString();
|
|
3871
3891
|
const inboxEvent = entityStateSchema.inbox.insert({
|
|
@@ -3873,6 +3893,7 @@ var EntityManager = class {
|
|
|
3873
3893
|
value: {
|
|
3874
3894
|
from: req.created_by ?? req.parent ?? `spawn`,
|
|
3875
3895
|
payload: req.initialMessage,
|
|
3896
|
+
message_type: req.initialMessageType,
|
|
3876
3897
|
timestamp: msgNow
|
|
3877
3898
|
}
|
|
3878
3899
|
});
|
|
@@ -3952,9 +3973,10 @@ var EntityManager = class {
|
|
|
3952
3973
|
const workLocks = new Set();
|
|
3953
3974
|
const writeEntityLocks = new Set();
|
|
3954
3975
|
const writeStreamLocks = new Set();
|
|
3976
|
+
const usePointerPath = opts.forkPointer !== void 0 || opts.anchor !== void 0;
|
|
3955
3977
|
try {
|
|
3956
3978
|
let sourceTree;
|
|
3957
|
-
if (
|
|
3979
|
+
if (usePointerPath) {
|
|
3958
3980
|
const rootEntity = await this.registry.getEntity(rootUrl);
|
|
3959
3981
|
if (!rootEntity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
3960
3982
|
if (isTerminalEntityStatus(rootEntity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Cannot fork terminal entity "${rootEntity.url}"`, 409);
|
|
@@ -3963,10 +3985,13 @@ var EntityManager = class {
|
|
|
3963
3985
|
const sourceRoot = sourceTree[0];
|
|
3964
3986
|
if (sourceRoot.parent) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Only top-level entities can be forked`, 400);
|
|
3965
3987
|
let preFilteredRoot;
|
|
3966
|
-
|
|
3988
|
+
let effectiveForkPointer = opts.forkPointer;
|
|
3989
|
+
if (usePointerPath) {
|
|
3967
3990
|
const sourceEvents = await this.streamClient.readJson(sourceRoot.streams.main);
|
|
3968
3991
|
const flat = sourceEvents.flatMap((item) => Array.isArray(item) ? item : [item]);
|
|
3969
|
-
|
|
3992
|
+
if (!effectiveForkPointer && opts.anchor === `latest_completed_run`) effectiveForkPointer = this.resolveLatestCompletedRunPointer(flat, sourceRoot.streams.main);
|
|
3993
|
+
if (!effectiveForkPointer) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Internal: pointer-path fork with no resolved pointer`, 500);
|
|
3994
|
+
const target = this.resolveForkPointerTarget(flat, effectiveForkPointer, sourceRoot.streams.main);
|
|
3970
3995
|
const filteredEvents = flat.slice(0, target);
|
|
3971
3996
|
const rootManifests = this.reduceStateRows(filteredEvents, `manifest`);
|
|
3972
3997
|
const sharedStateIds = new Set();
|
|
@@ -3979,7 +4004,7 @@ var EntityManager = class {
|
|
|
3979
4004
|
};
|
|
3980
4005
|
}
|
|
3981
4006
|
const effectiveSubtree = preFilteredRoot ? this.computeEffectiveSubtree(sourceTree, sourceRoot.url, preFilteredRoot.manifests) : sourceTree;
|
|
3982
|
-
if (
|
|
4007
|
+
if (usePointerPath) {
|
|
3983
4008
|
const descendants = effectiveSubtree.filter((entity) => entity.url !== sourceRoot.url);
|
|
3984
4009
|
if (descendants.length > 0) await this.waitForGivenEntitiesIdle(descendants, opts, workLocks);
|
|
3985
4010
|
}
|
|
@@ -4004,6 +4029,14 @@ var EntityManager = class {
|
|
|
4004
4029
|
const sharedStateIdMap = await this.buildForkSharedStateIdMap(snapshot.sharedStateIds, suffix);
|
|
4005
4030
|
const stringMap = this.buildForkStringMap(entityUrlMap, sharedStateIdMap);
|
|
4006
4031
|
const entityPlans = this.buildForkEntityPlans(effectiveSubtree, entityUrlMap, stringMap, opts.createdBy);
|
|
4032
|
+
const rootPlanForOverrides = entityPlans.find((plan) => plan.source.url === rootUrl);
|
|
4033
|
+
if (rootPlanForOverrides) {
|
|
4034
|
+
if (opts.parent !== void 0) rootPlanForOverrides.fork.parent = opts.parent;
|
|
4035
|
+
if (opts.tags !== void 0) rootPlanForOverrides.fork.tags = {
|
|
4036
|
+
...rootPlanForOverrides.fork.tags ?? {},
|
|
4037
|
+
...opts.tags
|
|
4038
|
+
};
|
|
4039
|
+
}
|
|
4007
4040
|
this.addForkLocks(this.forkWriteLockedEntities, effectiveSubtree.map((entity) => entity.url), writeEntityLocks);
|
|
4008
4041
|
this.addForkLocks(this.forkWriteLockedStreams, [...snapshot.sharedStateIds].map((id) => getSharedStateStreamPath(id)), writeStreamLocks);
|
|
4009
4042
|
const createdStreams = [];
|
|
@@ -4012,7 +4045,7 @@ var EntityManager = class {
|
|
|
4012
4045
|
try {
|
|
4013
4046
|
for (const plan of entityPlans) {
|
|
4014
4047
|
const isRoot = plan.source.url === rootUrl;
|
|
4015
|
-
await this.streamClient.fork(plan.fork.streams.main, plan.source.streams.main, isRoot &&
|
|
4048
|
+
await this.streamClient.fork(plan.fork.streams.main, plan.source.streams.main, isRoot && effectiveForkPointer ? { forkPointer: effectiveForkPointer } : void 0);
|
|
4016
4049
|
createdStreams.push(plan.fork.streams.main);
|
|
4017
4050
|
}
|
|
4018
4051
|
for (const [sourceId, forkId] of sharedStateIdMap) {
|
|
@@ -4039,6 +4072,20 @@ var EntityManager = class {
|
|
|
4039
4072
|
await this.registry.createEntity(plan.fork);
|
|
4040
4073
|
createdEntities.push(plan.fork.url);
|
|
4041
4074
|
}
|
|
4075
|
+
if (opts.wake !== void 0) {
|
|
4076
|
+
const rootPlan = entityPlans.find((plan) => plan.source.url === rootUrl);
|
|
4077
|
+
if (rootPlan) await this.wakeRegistry.register({
|
|
4078
|
+
tenantId: this.tenantId,
|
|
4079
|
+
subscriberUrl: opts.wake.subscriberUrl,
|
|
4080
|
+
sourceUrl: rootPlan.fork.url,
|
|
4081
|
+
condition: opts.wake.condition,
|
|
4082
|
+
debounceMs: opts.wake.debounceMs,
|
|
4083
|
+
timeoutMs: opts.wake.timeoutMs,
|
|
4084
|
+
oneShot: false,
|
|
4085
|
+
includeResponse: opts.wake.includeResponse,
|
|
4086
|
+
manifestKey: opts.wake.manifestKey
|
|
4087
|
+
});
|
|
4088
|
+
}
|
|
4042
4089
|
for (const plan of entityPlans) {
|
|
4043
4090
|
const manifests = activeManifestsByEntity.get(plan.fork.url) ?? new Map();
|
|
4044
4091
|
await this.materializeForkManifestSideEffects(plan.fork.url, manifests);
|
|
@@ -4199,6 +4246,45 @@ var EntityManager = class {
|
|
|
4199
4246
|
return positionAtAnchor + pointer.subOffset;
|
|
4200
4247
|
}
|
|
4201
4248
|
/**
|
|
4249
|
+
* Find an `EventPointer` that addresses the most recent `runs` row on
|
|
4250
|
+
* the source's `main` stream with `status === 'completed'`. Mirrors the
|
|
4251
|
+
* eligibility rule the per-row "Fork from here" UI applies — only
|
|
4252
|
+
* completed runs are valid fork anchors; `started`/`failed` rows are
|
|
4253
|
+
* skipped.
|
|
4254
|
+
*
|
|
4255
|
+
* The pointer is computed in the same coordinate system the runtime
|
|
4256
|
+
* mints pointers in (see entity-stream-db's onBeforeBatch): for a row
|
|
4257
|
+
* R in log entry E, the pointer is `{ offset: P, subOffset: K }` where
|
|
4258
|
+
* `P` is the END offset of the log entry preceding E (or `null` for
|
|
4259
|
+
* stream start) and `K` is R's 1-indexed position within E.
|
|
4260
|
+
*/
|
|
4261
|
+
resolveLatestCompletedRunPointer(events, streamPath) {
|
|
4262
|
+
let priorEntryOffset = null;
|
|
4263
|
+
let currentEntryOffset = null;
|
|
4264
|
+
let positionInEntry = 0;
|
|
4265
|
+
let latest = null;
|
|
4266
|
+
for (const event of events) {
|
|
4267
|
+
const headers = isRecord(event.headers) ? event.headers : void 0;
|
|
4268
|
+
const eventOffset = typeof headers?.offset === `string` ? headers.offset : null;
|
|
4269
|
+
if (eventOffset !== currentEntryOffset) {
|
|
4270
|
+
priorEntryOffset = currentEntryOffset;
|
|
4271
|
+
currentEntryOffset = eventOffset;
|
|
4272
|
+
positionInEntry = 0;
|
|
4273
|
+
}
|
|
4274
|
+
positionInEntry++;
|
|
4275
|
+
if (event.type !== `run`) continue;
|
|
4276
|
+
if (headers?.operation === `delete`) continue;
|
|
4277
|
+
if (!isRecord(event.value)) continue;
|
|
4278
|
+
if (event.value.status !== `completed`) continue;
|
|
4279
|
+
latest = {
|
|
4280
|
+
offset: priorEntryOffset,
|
|
4281
|
+
subOffset: positionInEntry
|
|
4282
|
+
};
|
|
4283
|
+
}
|
|
4284
|
+
if (!latest) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Source ${streamPath} has no completed run to fork from`, 400);
|
|
4285
|
+
return latest;
|
|
4286
|
+
}
|
|
4287
|
+
/**
|
|
4202
4288
|
* Compute the subset of `sourceTree` that survives the manifest filter
|
|
4203
4289
|
* applied at the root. After filtering the root's manifest at the fork
|
|
4204
4290
|
* pointer, only children whose manifest entries landed at or before the
|
|
@@ -5318,7 +5404,9 @@ var EntityManager = class {
|
|
|
5318
5404
|
creation_schema: existing.creation_schema,
|
|
5319
5405
|
inbox_schemas: mergedInbox,
|
|
5320
5406
|
state_schemas: mergedState,
|
|
5407
|
+
slash_commands: existing.slash_commands,
|
|
5321
5408
|
serve_endpoint: existing.serve_endpoint,
|
|
5409
|
+
default_dispatch_policy: existing.default_dispatch_policy,
|
|
5322
5410
|
revision: nextRevision,
|
|
5323
5411
|
created_at: existing.created_at,
|
|
5324
5412
|
updated_at: now
|
|
@@ -5372,11 +5460,19 @@ var EntityManager = class {
|
|
|
5372
5460
|
throw new ElectricAgentsError(ErrCodeInvalidRequest, error instanceof Error ? error.message : `Invalid tags`, 400);
|
|
5373
5461
|
}
|
|
5374
5462
|
}
|
|
5463
|
+
validateSlashCommands(input) {
|
|
5464
|
+
const validationError = validateSlashCommandDefinitions(input);
|
|
5465
|
+
if (!validationError) return;
|
|
5466
|
+
throw new ElectricAgentsError(ErrCodeSchemaValidationFailed, validationError.message, 422, validationError.details);
|
|
5467
|
+
}
|
|
5375
5468
|
async validateSendRequest(entityUrl, req) {
|
|
5376
5469
|
const entity = await this.registry.getEntity(entityUrl);
|
|
5377
5470
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
5378
5471
|
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
5379
|
-
if (req.type
|
|
5472
|
+
if (req.type === COMPOSER_INPUT_MESSAGE_TYPE) {
|
|
5473
|
+
const valErr = validateComposerInputPayload(req.payload);
|
|
5474
|
+
if (valErr) throw new ElectricAgentsError(ErrCodeSchemaValidationFailed, valErr.message, 422, valErr.details);
|
|
5475
|
+
} else if (req.type && entity.type) {
|
|
5380
5476
|
const { inboxSchemas } = await this.getEffectiveSchemas(entity);
|
|
5381
5477
|
if (inboxSchemas) {
|
|
5382
5478
|
const schema = inboxSchemas[req.type];
|
|
@@ -5657,6 +5753,7 @@ const spawnBodySchema = Type.Object({
|
|
|
5657
5753
|
sandbox: Type.Optional(sandboxChoiceSchema),
|
|
5658
5754
|
initialMessage: Type.Optional(Type.Unknown()),
|
|
5659
5755
|
grants: Type.Optional(Type.Array(entityPermissionGrantInputSchema)),
|
|
5756
|
+
initialMessageType: Type.Optional(Type.String()),
|
|
5660
5757
|
wake: Type.Optional(Type.Object({
|
|
5661
5758
|
subscriberUrl: Type.String(),
|
|
5662
5759
|
condition: wakeConditionSchema,
|
|
@@ -5694,6 +5791,14 @@ function agentUrlPath(value) {
|
|
|
5694
5791
|
return value;
|
|
5695
5792
|
}
|
|
5696
5793
|
}
|
|
5794
|
+
async function hasValidAgentWriteToken(request, ctx, fromAgent) {
|
|
5795
|
+
const agentUrl = agentUrlPath(fromAgent);
|
|
5796
|
+
const token = writeTokenFromRequest(request);
|
|
5797
|
+
if (!token) return false;
|
|
5798
|
+
const agentEntity = await ctx.entityManager.registry.getEntity(agentUrl);
|
|
5799
|
+
if (!agentEntity) return false;
|
|
5800
|
+
return ctx.entityManager.isValidWriteToken(agentEntity, token);
|
|
5801
|
+
}
|
|
5697
5802
|
const inboxMessageBodySchema = Type.Object({
|
|
5698
5803
|
payload: Type.Optional(Type.Unknown()),
|
|
5699
5804
|
position: Type.Optional(Type.String()),
|
|
@@ -5715,7 +5820,19 @@ const forkBodySchema = Type.Object({
|
|
|
5715
5820
|
fork_pointer: Type.Optional(Type.Object({
|
|
5716
5821
|
offset: Type.Union([Type.String(), Type.Null()]),
|
|
5717
5822
|
sub_offset: Type.Number()
|
|
5718
|
-
}))
|
|
5823
|
+
})),
|
|
5824
|
+
anchor: Type.Optional(Type.Literal(`latest_completed_run`)),
|
|
5825
|
+
parent: Type.Optional(Type.String()),
|
|
5826
|
+
wake: Type.Optional(Type.Object({
|
|
5827
|
+
subscriberUrl: Type.String(),
|
|
5828
|
+
condition: wakeConditionSchema,
|
|
5829
|
+
debounceMs: Type.Optional(Type.Number()),
|
|
5830
|
+
timeoutMs: Type.Optional(Type.Number()),
|
|
5831
|
+
includeResponse: Type.Optional(Type.Boolean()),
|
|
5832
|
+
manifestKey: Type.Optional(Type.String())
|
|
5833
|
+
})),
|
|
5834
|
+
initialMessage: Type.Optional(Type.Unknown()),
|
|
5835
|
+
tags: Type.Optional(stringRecordSchema$1)
|
|
5719
5836
|
});
|
|
5720
5837
|
const setTagBodySchema = Type.Object({ value: Type.String() });
|
|
5721
5838
|
const entitySignalSchema = Type.Union([
|
|
@@ -6102,8 +6219,18 @@ async function forkEntity(request, ctx) {
|
|
|
6102
6219
|
const principalMutationError = rejectPrincipalEntityMutation(request, `forked`);
|
|
6103
6220
|
if (principalMutationError) return principalMutationError;
|
|
6104
6221
|
const parsed = routeBody(request);
|
|
6222
|
+
if (parsed.fork_pointer && parsed.anchor) return apiError(400, ErrCodeInvalidRequest, `fork_pointer and anchor are mutually exclusive`);
|
|
6105
6223
|
const { entityUrl, entity } = requireExistingEntityRoute(request);
|
|
6106
6224
|
await assertDispatchPolicyAllowed(ctx, entity.dispatch_policy);
|
|
6225
|
+
if (parsed.parent !== void 0) {
|
|
6226
|
+
const parent = await ctx.entityManager.registry.getEntity(parsed.parent);
|
|
6227
|
+
if (!parent) return apiError(404, ErrCodeNotFound, `Parent entity not found`);
|
|
6228
|
+
if (!await canAccessEntity(ctx, parent, `spawn`, request)) return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn children from ${parent.url}`);
|
|
6229
|
+
}
|
|
6230
|
+
if (parsed.wake !== void 0) {
|
|
6231
|
+
if (parsed.parent === void 0) return apiError(400, ErrCodeInvalidRequest, `wake requires parent (the fork's wake fires to its parent)`);
|
|
6232
|
+
if (parsed.wake.subscriberUrl !== parsed.parent) return apiError(401, ErrCodeUnauthorized, `wake.subscriberUrl must match parent`);
|
|
6233
|
+
}
|
|
6107
6234
|
const result = await ctx.entityManager.forkSubtree(entityUrl, {
|
|
6108
6235
|
rootInstanceId: parsed.instance_id,
|
|
6109
6236
|
waitTimeoutMs: parsed.waitTimeoutMs,
|
|
@@ -6111,9 +6238,17 @@ async function forkEntity(request, ctx) {
|
|
|
6111
6238
|
...parsed.fork_pointer && { forkPointer: {
|
|
6112
6239
|
offset: parsed.fork_pointer.offset,
|
|
6113
6240
|
subOffset: parsed.fork_pointer.sub_offset
|
|
6114
|
-
} }
|
|
6241
|
+
} },
|
|
6242
|
+
...parsed.anchor && { anchor: parsed.anchor },
|
|
6243
|
+
...parsed.parent !== void 0 && { parent: parsed.parent },
|
|
6244
|
+
...parsed.wake !== void 0 && { wake: parsed.wake },
|
|
6245
|
+
...parsed.tags !== void 0 && { tags: parsed.tags }
|
|
6115
6246
|
});
|
|
6116
6247
|
for (const forkedEntity of result.entities) await linkEntityDispatchSubscription(ctx, forkedEntity);
|
|
6248
|
+
if (parsed.initialMessage !== void 0) await ctx.entityManager.send(result.root.url, {
|
|
6249
|
+
from: parsed.parent ?? ctx.principal.url,
|
|
6250
|
+
payload: parsed.initialMessage
|
|
6251
|
+
});
|
|
6117
6252
|
return json({
|
|
6118
6253
|
root: toPublicEntity(result.root),
|
|
6119
6254
|
entities: result.entities.map((entity$1) => toPublicEntity(entity$1))
|
|
@@ -6126,7 +6261,10 @@ async function sendEntity(request, ctx) {
|
|
|
6126
6261
|
if (parsed.from_principal !== void 0 && parsed.from_principal !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from_principal must match Electric-Principal`);
|
|
6127
6262
|
if (parsed.from_agent !== void 0) {
|
|
6128
6263
|
const principalAgentUrl = agentUrlForPrincipal(principal);
|
|
6129
|
-
|
|
6264
|
+
const fromAgentUrl = agentUrlPath(parsed.from_agent);
|
|
6265
|
+
const matchesPrincipalAgent = fromAgentUrl === principalAgentUrl;
|
|
6266
|
+
const hasAgentWriteToken = await hasValidAgentWriteToken(request, ctx, parsed.from_agent);
|
|
6267
|
+
if (!matchesPrincipalAgent && !hasAgentWriteToken) return apiError(400, ErrCodeInvalidRequest, `Request from_agent must match authenticated agent principal`);
|
|
6130
6268
|
}
|
|
6131
6269
|
await ctx.entityManager.ensurePrincipal(principal);
|
|
6132
6270
|
const { entityUrl, entity } = requireExistingEntityRoute(request);
|
|
@@ -6212,6 +6350,7 @@ async function spawnEntity(request, ctx) {
|
|
|
6212
6350
|
dispatch_policy: dispatchPolicy,
|
|
6213
6351
|
sandbox: parsed.sandbox,
|
|
6214
6352
|
initialMessage: void 0,
|
|
6353
|
+
initialMessageType: void 0,
|
|
6215
6354
|
wake: parsed.wake,
|
|
6216
6355
|
created_by: principal.url
|
|
6217
6356
|
});
|
|
@@ -6230,7 +6369,8 @@ async function spawnEntity(request, ctx) {
|
|
|
6230
6369
|
if (linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
|
|
6231
6370
|
if (parsed.initialMessage !== void 0) await ctx.entityManager.send(entity.url, {
|
|
6232
6371
|
from: principal.url,
|
|
6233
|
-
payload: parsed.initialMessage
|
|
6372
|
+
payload: parsed.initialMessage,
|
|
6373
|
+
type: parsed.initialMessageType
|
|
6234
6374
|
});
|
|
6235
6375
|
if (!linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
|
|
6236
6376
|
return json({
|
|
@@ -6277,6 +6417,21 @@ async function signalEntity(request, ctx) {
|
|
|
6277
6417
|
//#region src/routing/entity-types-router.ts
|
|
6278
6418
|
const jsonObjectSchema = Type.Record(Type.String(), Type.Unknown());
|
|
6279
6419
|
const schemaMapSchema = Type.Record(Type.String(), jsonObjectSchema);
|
|
6420
|
+
const slashCommandArgumentSchema = Type.Object({
|
|
6421
|
+
name: Type.String(),
|
|
6422
|
+
type: Type.Union([
|
|
6423
|
+
Type.Literal(`string`),
|
|
6424
|
+
Type.Literal(`number`),
|
|
6425
|
+
Type.Literal(`boolean`)
|
|
6426
|
+
]),
|
|
6427
|
+
required: Type.Optional(Type.Boolean()),
|
|
6428
|
+
description: Type.Optional(Type.String())
|
|
6429
|
+
}, { additionalProperties: false });
|
|
6430
|
+
const slashCommandSchema = Type.Object({
|
|
6431
|
+
name: Type.String(),
|
|
6432
|
+
description: Type.Optional(Type.String()),
|
|
6433
|
+
arguments: Type.Optional(Type.Array(slashCommandArgumentSchema))
|
|
6434
|
+
}, { additionalProperties: false });
|
|
6280
6435
|
const typePermissionGrantInputSchema = Type.Object({
|
|
6281
6436
|
subject_kind: Type.Union([Type.Literal(`principal`), Type.Literal(`principal_kind`)]),
|
|
6282
6437
|
subject_value: Type.String(),
|
|
@@ -6289,6 +6444,7 @@ const registerEntityTypeBodySchema = Type.Object({
|
|
|
6289
6444
|
creation_schema: Type.Optional(jsonObjectSchema),
|
|
6290
6445
|
inbox_schemas: Type.Optional(schemaMapSchema),
|
|
6291
6446
|
state_schemas: Type.Optional(schemaMapSchema),
|
|
6447
|
+
slash_commands: Type.Optional(Type.Array(slashCommandSchema)),
|
|
6292
6448
|
serve_endpoint: Type.Optional(Type.String()),
|
|
6293
6449
|
default_dispatch_policy: Type.Optional(dispatchPolicySchema),
|
|
6294
6450
|
permission_grants: Type.Optional(Type.Array(typePermissionGrantInputSchema))
|
|
@@ -6430,6 +6586,7 @@ function normalizeEntityTypeRequest(parsed) {
|
|
|
6430
6586
|
creation_schema: parsed.creation_schema,
|
|
6431
6587
|
inbox_schemas: parsed.inbox_schemas,
|
|
6432
6588
|
state_schemas: parsed.state_schemas,
|
|
6589
|
+
slash_commands: parsed.slash_commands,
|
|
6433
6590
|
serve_endpoint: serveEndpoint,
|
|
6434
6591
|
default_dispatch_policy: parsed.default_dispatch_policy ?? (serveEndpoint ? { targets: [{
|
|
6435
6592
|
type: `webhook`,
|