@electric-ax/agents-server 0.4.5 → 0.4.7
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 +404 -68
- package/dist/index.cjs +421 -67
- package/dist/index.d.cts +97 -11
- package/dist/index.d.ts +97 -11
- package/dist/index.js +414 -69
- package/drizzle/0009_entity_signal_statuses.sql +3 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +6 -6
- package/src/db/schema.ts +1 -1
- package/src/electric-agents-types.ts +76 -1
- package/src/entity-manager.ts +256 -33
- package/src/entity-registry.ts +57 -20
- package/src/entrypoint-lib.ts +5 -0
- package/src/index.ts +33 -0
- package/src/routing/context.ts +6 -0
- package/src/routing/dispatch-policy.ts +6 -0
- package/src/routing/durable-streams-router.ts +62 -13
- package/src/routing/entities-router.ts +57 -1
- package/src/routing/internal-router.ts +147 -23
- package/src/routing/runners-router.ts +1 -1
- package/src/runtime.ts +5 -1
- package/src/server.ts +18 -0
- package/src/stream-client.ts +10 -4
- package/src/webhook-signing.ts +173 -0
package/src/entity-registry.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
assertEntityStatus,
|
|
16
16
|
assertRunnerAdminStatus,
|
|
17
17
|
assertRunnerKind,
|
|
18
|
+
isTerminalEntityStatus,
|
|
18
19
|
} from './electric-agents-types.js'
|
|
19
20
|
import { DEFAULT_TENANT_ID } from './tenant.js'
|
|
20
21
|
import type { DrizzleDB } from './db/index.js'
|
|
@@ -360,11 +361,17 @@ export class PostgresRegistry {
|
|
|
360
361
|
input: MaterializeHeartbeatClaimInput
|
|
361
362
|
): Promise<void> {
|
|
362
363
|
const heartbeatAt = input.heartbeatAt ?? new Date()
|
|
364
|
+
// Only touch leaseExpiresAt when the caller explicitly provides one.
|
|
365
|
+
// The lease was set at materializeActiveClaim time from the upstream
|
|
366
|
+
// lease_ttl_ms and remains the authoritative expiry; heartbeats are
|
|
367
|
+
// alive-pings, not lease extensions.
|
|
363
368
|
await this.db
|
|
364
369
|
.update(consumerClaims)
|
|
365
370
|
.set({
|
|
366
371
|
lastHeartbeatAt: heartbeatAt,
|
|
367
|
-
|
|
372
|
+
...(input.leaseExpiresAt !== undefined
|
|
373
|
+
? { leaseExpiresAt: input.leaseExpiresAt }
|
|
374
|
+
: {}),
|
|
368
375
|
updatedAt: heartbeatAt,
|
|
369
376
|
})
|
|
370
377
|
.where(
|
|
@@ -378,7 +385,7 @@ export class PostgresRegistry {
|
|
|
378
385
|
|
|
379
386
|
async materializeReleasedClaim(
|
|
380
387
|
input: MaterializeReleasedClaimInput
|
|
381
|
-
): Promise<ConsumerClaim | null> {
|
|
388
|
+
): Promise<{ claim: ConsumerClaim | null; entityCleared: boolean }> {
|
|
382
389
|
const releasedAt = input.releasedAt ?? new Date()
|
|
383
390
|
const rows = await this.db
|
|
384
391
|
.update(consumerClaims)
|
|
@@ -398,8 +405,13 @@ export class PostgresRegistry {
|
|
|
398
405
|
.returning()
|
|
399
406
|
|
|
400
407
|
const claim = rows[0] ? this.rowToConsumerClaim(rows[0]) : null
|
|
408
|
+
let entityCleared = false
|
|
401
409
|
if (claim) {
|
|
402
|
-
|
|
410
|
+
// entityCleared distinguishes "we were the active dispatch and now it's
|
|
411
|
+
// empty" from "a newer claim was already active for this entity." The
|
|
412
|
+
// WHERE clause matches our (consumerId, epoch) so an evicted-by-newer
|
|
413
|
+
// case correctly returns zero rows.
|
|
414
|
+
const cleared = await this.db
|
|
403
415
|
.update(entityDispatchState)
|
|
404
416
|
.set({
|
|
405
417
|
activeConsumerId: null,
|
|
@@ -419,8 +431,10 @@ export class PostgresRegistry {
|
|
|
419
431
|
eq(entityDispatchState.activeEpoch, input.epoch)
|
|
420
432
|
)
|
|
421
433
|
)
|
|
434
|
+
.returning({ entityUrl: entityDispatchState.entityUrl })
|
|
435
|
+
entityCleared = cleared.length > 0
|
|
422
436
|
}
|
|
423
|
-
return claim
|
|
437
|
+
return { claim, entityCleared }
|
|
424
438
|
}
|
|
425
439
|
|
|
426
440
|
async getActiveClaimsForRunner(
|
|
@@ -713,10 +727,13 @@ export class PostgresRegistry {
|
|
|
713
727
|
}
|
|
714
728
|
|
|
715
729
|
async updateStatus(entityUrl: string, status: EntityStatus): Promise<void> {
|
|
716
|
-
const whereClause =
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
730
|
+
const whereClause = isTerminalEntityStatus(status)
|
|
731
|
+
? this.entityWhere(entityUrl)
|
|
732
|
+
: and(
|
|
733
|
+
this.entityWhere(entityUrl),
|
|
734
|
+
ne(entities.status, `stopped`),
|
|
735
|
+
ne(entities.status, `killed`)
|
|
736
|
+
)
|
|
720
737
|
|
|
721
738
|
await this.db
|
|
722
739
|
.update(entities)
|
|
@@ -727,21 +744,41 @@ export class PostgresRegistry {
|
|
|
727
744
|
async updateStatusWithTxid(
|
|
728
745
|
entityUrl: string,
|
|
729
746
|
status: EntityStatus
|
|
730
|
-
): Promise<number> {
|
|
747
|
+
): Promise<number | null> {
|
|
731
748
|
return await this.db.transaction(async (tx) => {
|
|
732
|
-
const
|
|
733
|
-
status === `stopped`
|
|
734
|
-
? this.entityWhere(entityUrl)
|
|
735
|
-
: and(this.entityWhere(entityUrl), ne(entities.status, `stopped`))
|
|
736
|
-
|
|
737
|
-
await tx
|
|
749
|
+
const rows = await tx
|
|
738
750
|
.update(entities)
|
|
739
751
|
.set({ status, updatedAt: Date.now() })
|
|
740
|
-
.where(
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
752
|
+
.where(
|
|
753
|
+
and(
|
|
754
|
+
this.entityWhere(entityUrl),
|
|
755
|
+
ne(entities.status, `stopped`),
|
|
756
|
+
ne(entities.status, `killed`)
|
|
757
|
+
)
|
|
758
|
+
)
|
|
759
|
+
.returning({
|
|
760
|
+
txid: sql<string>`pg_current_xact_id()::xid::text`,
|
|
761
|
+
})
|
|
762
|
+
return rows[0] ? parseInt(rows[0].txid) : null
|
|
763
|
+
})
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
async touchEntityWithTxid(entityUrl: string): Promise<number | null> {
|
|
767
|
+
return await this.db.transaction(async (tx) => {
|
|
768
|
+
const rows = await tx
|
|
769
|
+
.update(entities)
|
|
770
|
+
.set({ updatedAt: Date.now() })
|
|
771
|
+
.where(
|
|
772
|
+
and(
|
|
773
|
+
eq(entities.url, entityUrl),
|
|
774
|
+
ne(entities.status, `stopped`),
|
|
775
|
+
ne(entities.status, `killed`)
|
|
776
|
+
)
|
|
777
|
+
)
|
|
778
|
+
.returning({
|
|
779
|
+
txid: sql<string>`pg_current_xact_id()::xid::text`,
|
|
780
|
+
})
|
|
781
|
+
return rows[0] ? parseInt(rows[0].txid) : null
|
|
745
782
|
})
|
|
746
783
|
}
|
|
747
784
|
|
package/src/entrypoint-lib.ts
CHANGED
|
@@ -139,6 +139,10 @@ export function resolveElectricAgentsEntrypointOptions(
|
|
|
139
139
|
`ELECTRIC_URL`,
|
|
140
140
|
])
|
|
141
141
|
const electricSecret = readEnv(env, [`ELECTRIC_AGENTS_ELECTRIC_SECRET`])
|
|
142
|
+
const webhookSigningKey = readEnv(env, [
|
|
143
|
+
`ELECTRIC_AGENTS_WEBHOOK_SIGNING_PRIVATE_KEY`,
|
|
144
|
+
`WEBHOOK_SIGNING_PRIVATE_KEY`,
|
|
145
|
+
])
|
|
142
146
|
const baseUrl = readEnv(env, [`ELECTRIC_AGENTS_BASE_URL`, `BASE_URL`])
|
|
143
147
|
return {
|
|
144
148
|
service: readEnv(env, [`ELECTRIC_AGENTS_SERVICE`, `SERVICE`]),
|
|
@@ -153,6 +157,7 @@ export function resolveElectricAgentsEntrypointOptions(
|
|
|
153
157
|
? validateUrl(`Electric URL`, electricUrl)
|
|
154
158
|
: undefined,
|
|
155
159
|
electricSecret,
|
|
160
|
+
webhookSigningKey,
|
|
156
161
|
host: readEnv(env, [`ELECTRIC_AGENTS_HOST`, `HOST`]) ?? DEFAULT_HOST,
|
|
157
162
|
port: readPort(env),
|
|
158
163
|
workingDirectory:
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,14 @@ export type {
|
|
|
15
15
|
SubscriptionResponse,
|
|
16
16
|
SubscriptionStreamInfo,
|
|
17
17
|
} from './stream-client.js'
|
|
18
|
+
export {
|
|
19
|
+
assertEntitySignal,
|
|
20
|
+
assertEntityStatus,
|
|
21
|
+
expectedSignalStatus,
|
|
22
|
+
isTerminalEntityStatus,
|
|
23
|
+
rejectsNormalWrites,
|
|
24
|
+
toPublicEntity,
|
|
25
|
+
} from './electric-agents-types.js'
|
|
18
26
|
export type {
|
|
19
27
|
AuthenticateRequest,
|
|
20
28
|
ConsumerClaim,
|
|
@@ -32,6 +40,18 @@ export type {
|
|
|
32
40
|
RunnerLiveness,
|
|
33
41
|
SourceStreamOffset,
|
|
34
42
|
WakeNotificationRow,
|
|
43
|
+
ElectricAgentsEntity,
|
|
44
|
+
ElectricAgentsEntityRow,
|
|
45
|
+
ElectricAgentsEntityType,
|
|
46
|
+
EntityStatus,
|
|
47
|
+
EntitySignal,
|
|
48
|
+
PublicElectricAgentsEntity,
|
|
49
|
+
EntityListFilter,
|
|
50
|
+
RegisterEntityTypeRequest,
|
|
51
|
+
SendRequest,
|
|
52
|
+
SignalRequest,
|
|
53
|
+
SignalResponse,
|
|
54
|
+
TypedSpawnRequest,
|
|
35
55
|
} from './electric-agents-types.js'
|
|
36
56
|
export type { Principal, PrincipalKind } from './principal.js'
|
|
37
57
|
export { globalRouter } from './routing/global-router.js'
|
|
@@ -46,6 +66,19 @@ export type {
|
|
|
46
66
|
DurableStreamsRoutingAdapter,
|
|
47
67
|
DurableStreamsRoutingInput,
|
|
48
68
|
} from './routing/durable-streams-routing-adapter.js'
|
|
69
|
+
export {
|
|
70
|
+
createEd25519WebhookSigner,
|
|
71
|
+
getDefaultWebhookSigner,
|
|
72
|
+
webhookSigningMetadata,
|
|
73
|
+
} from './webhook-signing.js'
|
|
74
|
+
export type {
|
|
75
|
+
Ed25519WebhookSignerOptions,
|
|
76
|
+
WebhookJwks,
|
|
77
|
+
WebhookPublicJwk,
|
|
78
|
+
WebhookSigner,
|
|
79
|
+
WebhookSigningKeyInput,
|
|
80
|
+
WebhookSigningMetadata,
|
|
81
|
+
} from './webhook-signing.js'
|
|
49
82
|
export type { EntityBridgeCoordinator } from './entity-bridge-manager.js'
|
|
50
83
|
export {
|
|
51
84
|
DEFAULT_TENANT_ID,
|
package/src/routing/context.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Agent } from 'undici'
|
|
2
|
+
import type { WebhookSignatureVerifierConfig } from '@electric-ax/agents-runtime'
|
|
2
3
|
import type { DrizzleDB } from '../db/index.js'
|
|
3
4
|
import type { EntityBridgeCoordinator } from '../entity-bridge-manager.js'
|
|
4
5
|
import type { EntityManager } from '../entity-manager.js'
|
|
@@ -7,6 +8,7 @@ import type { StreamClient } from '../stream-client.js'
|
|
|
7
8
|
import type { DurableStreamsRoutingAdapter } from './durable-streams-routing-adapter.js'
|
|
8
9
|
import type { Principal } from '../principal.js'
|
|
9
10
|
import type { DurableStreamsBearerProvider } from '../stream-client.js'
|
|
11
|
+
import type { WebhookSigner } from '../webhook-signing.js'
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Per-request tenant context passed through every router and handler.
|
|
@@ -24,6 +26,10 @@ export interface TenantContext {
|
|
|
24
26
|
durableStreamsBearer?: DurableStreamsBearerProvider
|
|
25
27
|
durableStreamsRouting?: DurableStreamsRoutingAdapter
|
|
26
28
|
durableStreamsDispatcher: Agent
|
|
29
|
+
durableStreamsWebhookSignature?:
|
|
30
|
+
| false
|
|
31
|
+
| Partial<WebhookSignatureVerifierConfig>
|
|
32
|
+
webhookSigner?: WebhookSigner
|
|
27
33
|
electricUrl?: string
|
|
28
34
|
electricSecret?: string
|
|
29
35
|
ownAgentHandlerPaths?: ReadonlyArray<string>
|
|
@@ -217,6 +217,12 @@ export async function assertDispatchPolicyAllowed(
|
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
export function shouldLinkDispatchBeforeInitialMessage(
|
|
221
|
+
policy: DispatchPolicy | undefined
|
|
222
|
+
): boolean {
|
|
223
|
+
return policy?.targets[0] !== undefined
|
|
224
|
+
}
|
|
225
|
+
|
|
220
226
|
export async function linkEntityDispatchSubscription(
|
|
221
227
|
ctx: TenantContext,
|
|
222
228
|
entity: ElectricAgentsEntity
|
|
@@ -15,10 +15,15 @@ import {
|
|
|
15
15
|
import { validateBody } from './schema.js'
|
|
16
16
|
import { rewriteLoopbackWebhookUrl } from '../utils/webhook-url.js'
|
|
17
17
|
import { forwardFetchRequest } from '../utils/server-utils.js'
|
|
18
|
+
import {
|
|
19
|
+
getDefaultWebhookSigner,
|
|
20
|
+
webhookSigningMetadata,
|
|
21
|
+
} from '../webhook-signing.js'
|
|
18
22
|
import { resolveDurableStreamsRoutingAdapter } from './durable-streams-routing-adapter.js'
|
|
19
23
|
import type { IRequest, RouterType } from 'itty-router'
|
|
20
24
|
import type { TenantContext } from './context.js'
|
|
21
25
|
import type { DurableStreamsRoutingAdapter } from './durable-streams-routing-adapter.js'
|
|
26
|
+
import type { WebhookSigner } from '../webhook-signing.js'
|
|
22
27
|
|
|
23
28
|
const subscriptionProxyBodySchema = Type.Object(
|
|
24
29
|
{
|
|
@@ -81,6 +86,7 @@ for (const action of subscriptionControlActions) {
|
|
|
81
86
|
subscriptionAction(action)
|
|
82
87
|
)
|
|
83
88
|
}
|
|
89
|
+
durableStreamsRouter.get(`/__ds/jwks.json`, webhookJwks)
|
|
84
90
|
durableStreamsRouter.all(`/__ds`, controlPassThrough)
|
|
85
91
|
durableStreamsRouter.all(`/__ds/*`, controlPassThrough)
|
|
86
92
|
durableStreamsRouter.post(`*`, streamAppend)
|
|
@@ -94,13 +100,22 @@ function bodyFromBytes(body: Uint8Array): ArrayBuffer {
|
|
|
94
100
|
}
|
|
95
101
|
|
|
96
102
|
function responseFromUpstream(response: Response, body?: Uint8Array): Response {
|
|
97
|
-
|
|
103
|
+
const responseBody = forbidsResponseBody(response.status)
|
|
104
|
+
? null
|
|
105
|
+
: body !== undefined
|
|
106
|
+
? bodyFromBytes(body)
|
|
107
|
+
: response.body
|
|
108
|
+
return new Response(responseBody, {
|
|
98
109
|
status: response.status,
|
|
99
110
|
statusText: response.statusText,
|
|
100
111
|
headers: responseHeaders(response),
|
|
101
112
|
})
|
|
102
113
|
}
|
|
103
114
|
|
|
115
|
+
function forbidsResponseBody(status: number): boolean {
|
|
116
|
+
return status === 204 || status === 205 || status === 304
|
|
117
|
+
}
|
|
118
|
+
|
|
104
119
|
async function forwardToDurableStreams(
|
|
105
120
|
ctx: TenantContext,
|
|
106
121
|
request: IRequest,
|
|
@@ -178,12 +193,12 @@ function rewriteSubscriptionBodyForBackend(
|
|
|
178
193
|
}
|
|
179
194
|
}
|
|
180
195
|
|
|
181
|
-
function rewriteSubscriptionResponseForClient(
|
|
196
|
+
async function rewriteSubscriptionResponseForClient(
|
|
182
197
|
bytes: Uint8Array,
|
|
183
198
|
response: Response,
|
|
184
|
-
|
|
199
|
+
ctx: TenantContext,
|
|
185
200
|
routingAdapter: DurableStreamsRoutingAdapter
|
|
186
|
-
): Uint8Array {
|
|
201
|
+
): Promise<Uint8Array> {
|
|
187
202
|
if (!response.headers.get(`content-type`)?.includes(`application/json`)) {
|
|
188
203
|
return bytes
|
|
189
204
|
}
|
|
@@ -192,14 +207,14 @@ function rewriteSubscriptionResponseForClient(
|
|
|
192
207
|
|
|
193
208
|
if (typeof payload.pattern === `string`) {
|
|
194
209
|
payload.pattern = routingAdapter.toRuntimeStreamPath(
|
|
195
|
-
service,
|
|
210
|
+
ctx.service,
|
|
196
211
|
payload.pattern
|
|
197
212
|
)
|
|
198
213
|
}
|
|
199
214
|
if (Array.isArray(payload.streams)) {
|
|
200
215
|
payload.streams = payload.streams.map((stream) => {
|
|
201
216
|
if (typeof stream === `string`) {
|
|
202
|
-
return routingAdapter.toRuntimeStreamPath(service, stream)
|
|
217
|
+
return routingAdapter.toRuntimeStreamPath(ctx.service, stream)
|
|
203
218
|
}
|
|
204
219
|
if (
|
|
205
220
|
stream &&
|
|
@@ -209,7 +224,7 @@ function rewriteSubscriptionResponseForClient(
|
|
|
209
224
|
return {
|
|
210
225
|
...(stream as Record<string, unknown>),
|
|
211
226
|
path: routingAdapter.toRuntimeStreamPath(
|
|
212
|
-
service,
|
|
227
|
+
ctx.service,
|
|
213
228
|
(stream as Record<string, string>).path
|
|
214
229
|
),
|
|
215
230
|
}
|
|
@@ -219,26 +234,43 @@ function rewriteSubscriptionResponseForClient(
|
|
|
219
234
|
}
|
|
220
235
|
if (typeof payload.wake_stream === `string`) {
|
|
221
236
|
payload.wake_stream = routingAdapter.toRuntimeStreamPath(
|
|
222
|
-
service,
|
|
237
|
+
ctx.service,
|
|
223
238
|
payload.wake_stream
|
|
224
239
|
)
|
|
225
240
|
}
|
|
226
241
|
if (typeof payload.stream === `string`) {
|
|
227
|
-
payload.stream = routingAdapter.toRuntimeStreamPath(
|
|
242
|
+
payload.stream = routingAdapter.toRuntimeStreamPath(
|
|
243
|
+
ctx.service,
|
|
244
|
+
payload.stream
|
|
245
|
+
)
|
|
228
246
|
}
|
|
229
247
|
if (Array.isArray(payload.acks)) {
|
|
230
248
|
payload.acks = payload.acks.map((ack) => {
|
|
231
249
|
if (!ack || typeof ack !== `object`) return ack
|
|
232
250
|
const next = { ...(ack as Record<string, unknown>) }
|
|
233
251
|
if (typeof next.stream === `string`) {
|
|
234
|
-
next.stream = routingAdapter.toRuntimeStreamPath(
|
|
252
|
+
next.stream = routingAdapter.toRuntimeStreamPath(
|
|
253
|
+
ctx.service,
|
|
254
|
+
next.stream
|
|
255
|
+
)
|
|
235
256
|
}
|
|
236
257
|
if (typeof next.path === `string`) {
|
|
237
|
-
next.path = routingAdapter.toRuntimeStreamPath(service, next.path)
|
|
258
|
+
next.path = routingAdapter.toRuntimeStreamPath(ctx.service, next.path)
|
|
238
259
|
}
|
|
239
260
|
return next
|
|
240
261
|
})
|
|
241
262
|
}
|
|
263
|
+
if (
|
|
264
|
+
payload.webhook &&
|
|
265
|
+
typeof payload.webhook === `object` &&
|
|
266
|
+
!Array.isArray(payload.webhook)
|
|
267
|
+
) {
|
|
268
|
+
const webhook = payload.webhook as Record<string, unknown>
|
|
269
|
+
webhook.signing = await webhookSigningMetadata(
|
|
270
|
+
resolveWebhookSigner(ctx),
|
|
271
|
+
ctx.publicUrl
|
|
272
|
+
)
|
|
273
|
+
}
|
|
242
274
|
|
|
243
275
|
return new TextEncoder().encode(JSON.stringify(payload))
|
|
244
276
|
}
|
|
@@ -269,6 +301,10 @@ function subscriptionRoutingAdapter(
|
|
|
269
301
|
)
|
|
270
302
|
}
|
|
271
303
|
|
|
304
|
+
function resolveWebhookSigner(ctx: TenantContext): WebhookSigner {
|
|
305
|
+
return ctx.webhookSigner ?? getDefaultWebhookSigner()
|
|
306
|
+
}
|
|
307
|
+
|
|
272
308
|
async function rewriteSubscriptionRequestBody(
|
|
273
309
|
request: IRequest,
|
|
274
310
|
ctx: TenantContext,
|
|
@@ -334,10 +370,10 @@ async function forwardSubscriptionRequest(
|
|
|
334
370
|
let responseBytes: Uint8Array = upstream.body
|
|
335
371
|
? new Uint8Array(await upstream.arrayBuffer())
|
|
336
372
|
: new Uint8Array()
|
|
337
|
-
responseBytes = rewriteSubscriptionResponseForClient(
|
|
373
|
+
responseBytes = await rewriteSubscriptionResponseForClient(
|
|
338
374
|
responseBytes,
|
|
339
375
|
upstream,
|
|
340
|
-
ctx
|
|
376
|
+
ctx,
|
|
341
377
|
routingAdapter
|
|
342
378
|
)
|
|
343
379
|
return {
|
|
@@ -530,6 +566,19 @@ async function controlPassThrough(
|
|
|
530
566
|
return responseFromUpstream(upstream)
|
|
531
567
|
}
|
|
532
568
|
|
|
569
|
+
async function webhookJwks(
|
|
570
|
+
_request: IRequest,
|
|
571
|
+
ctx: TenantContext
|
|
572
|
+
): Promise<Response> {
|
|
573
|
+
return new Response(JSON.stringify(await resolveWebhookSigner(ctx).jwks()), {
|
|
574
|
+
status: 200,
|
|
575
|
+
headers: {
|
|
576
|
+
'content-type': `application/jwk-set+json`,
|
|
577
|
+
'cache-control': `public, max-age=300`,
|
|
578
|
+
},
|
|
579
|
+
})
|
|
580
|
+
}
|
|
581
|
+
|
|
533
582
|
async function streamAppend(
|
|
534
583
|
request: IRequest,
|
|
535
584
|
ctx: TenantContext
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
backfillEntityDispatchPolicy,
|
|
19
19
|
linkEntityDispatchSubscription,
|
|
20
20
|
resolveEffectiveDispatchPolicyForSpawn,
|
|
21
|
+
shouldLinkDispatchBeforeInitialMessage,
|
|
21
22
|
unlinkEntityDispatchSubscription,
|
|
22
23
|
} from './dispatch-policy.js'
|
|
23
24
|
import { routeBody, withSchema } from './schema.js'
|
|
@@ -133,6 +134,22 @@ const setTagBodySchema = Type.Object({
|
|
|
133
134
|
value: Type.String(),
|
|
134
135
|
})
|
|
135
136
|
|
|
137
|
+
const entitySignalSchema = Type.Union([
|
|
138
|
+
Type.Literal(`SIGINT`),
|
|
139
|
+
Type.Literal(`SIGHUP`),
|
|
140
|
+
Type.Literal(`SIGTERM`),
|
|
141
|
+
Type.Literal(`SIGKILL`),
|
|
142
|
+
Type.Literal(`SIGSTOP`),
|
|
143
|
+
Type.Literal(`SIGCONT`),
|
|
144
|
+
Type.Literal(`SIGUSR`),
|
|
145
|
+
])
|
|
146
|
+
|
|
147
|
+
const signalBodySchema = Type.Object({
|
|
148
|
+
signal: entitySignalSchema,
|
|
149
|
+
reason: Type.Optional(Type.String()),
|
|
150
|
+
payload: Type.Optional(Type.Unknown()),
|
|
151
|
+
})
|
|
152
|
+
|
|
136
153
|
const scheduleBodySchema = Type.Union([
|
|
137
154
|
Type.Object({
|
|
138
155
|
scheduleType: Type.Literal(`cron`),
|
|
@@ -161,6 +178,7 @@ type SendBody = Static<typeof sendBodySchema>
|
|
|
161
178
|
type InboxMessageBody = Static<typeof inboxMessageBodySchema>
|
|
162
179
|
type ForkBody = Static<typeof forkBodySchema>
|
|
163
180
|
type SetTagBody = Static<typeof setTagBodySchema>
|
|
181
|
+
type SignalBody = Static<typeof signalBodySchema>
|
|
164
182
|
type ScheduleBody = Static<typeof scheduleBodySchema>
|
|
165
183
|
type EntitiesRegisterBody = Static<typeof entitiesRegisterBodySchema>
|
|
166
184
|
|
|
@@ -187,6 +205,12 @@ entitiesRouter.put(
|
|
|
187
205
|
entitiesRouter.get(`/:type/:instanceId`, withExistingEntity, getEntity)
|
|
188
206
|
entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, headEntity)
|
|
189
207
|
entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, killEntity)
|
|
208
|
+
entitiesRouter.post(
|
|
209
|
+
`/:type/:instanceId/signal`,
|
|
210
|
+
withExistingEntity,
|
|
211
|
+
withSchema(signalBodySchema),
|
|
212
|
+
signalEntity
|
|
213
|
+
)
|
|
190
214
|
entitiesRouter.post(
|
|
191
215
|
`/:type/:instanceId/send`,
|
|
192
216
|
withExistingEntity,
|
|
@@ -614,13 +638,21 @@ async function spawnEntity(
|
|
|
614
638
|
wake: parsed.wake,
|
|
615
639
|
created_by: principal.url,
|
|
616
640
|
})
|
|
617
|
-
|
|
641
|
+
const linkBeforeInitialMessage =
|
|
642
|
+
parsed.initialMessage !== undefined &&
|
|
643
|
+
shouldLinkDispatchBeforeInitialMessage(dispatchPolicy)
|
|
644
|
+
if (linkBeforeInitialMessage) {
|
|
645
|
+
await linkEntityDispatchSubscription(ctx, entity)
|
|
646
|
+
}
|
|
618
647
|
if (parsed.initialMessage !== undefined) {
|
|
619
648
|
await ctx.entityManager.send(entity.url, {
|
|
620
649
|
from: principal.url,
|
|
621
650
|
payload: parsed.initialMessage,
|
|
622
651
|
})
|
|
623
652
|
}
|
|
653
|
+
if (!linkBeforeInitialMessage) {
|
|
654
|
+
await linkEntityDispatchSubscription(ctx, entity)
|
|
655
|
+
}
|
|
624
656
|
|
|
625
657
|
return json(
|
|
626
658
|
{ ...toPublicEntity(entity), txid: entity.txid },
|
|
@@ -655,3 +687,27 @@ async function killEntity(
|
|
|
655
687
|
ctx.runtime.claimWriteTokens.clearStream(ctx.service, entity.streams.main)
|
|
656
688
|
return json(result)
|
|
657
689
|
}
|
|
690
|
+
|
|
691
|
+
async function signalEntity(
|
|
692
|
+
request: AgentsRouteRequest,
|
|
693
|
+
ctx: TenantContext
|
|
694
|
+
): Promise<Response> {
|
|
695
|
+
const principalMutationError = rejectPrincipalEntityMutation(
|
|
696
|
+
request,
|
|
697
|
+
`signaled`
|
|
698
|
+
)
|
|
699
|
+
if (principalMutationError) return principalMutationError
|
|
700
|
+
|
|
701
|
+
const parsed = routeBody<SignalBody>(request)
|
|
702
|
+
const { entityUrl, entity } = requireExistingEntityRoute(request)
|
|
703
|
+
const result = await ctx.entityManager.signal(entityUrl, {
|
|
704
|
+
signal: parsed.signal,
|
|
705
|
+
reason: parsed.reason,
|
|
706
|
+
payload: parsed.payload,
|
|
707
|
+
})
|
|
708
|
+
if (result.new_state === `stopped` || result.new_state === `killed`) {
|
|
709
|
+
await unlinkEntityDispatchSubscription(ctx, entity)
|
|
710
|
+
ctx.runtime.claimWriteTokens.clearStream(ctx.service, entity.streams.main)
|
|
711
|
+
}
|
|
712
|
+
return json(result)
|
|
713
|
+
}
|