@nuxtagentic/automations 0.0.0-stage → 0.1.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.
Files changed (38) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +173 -2
  3. package/client.ts +24 -0
  4. package/module.ts +55 -0
  5. package/package.json +59 -3
  6. package/runtime/components/AutomationCanvas.vue +488 -0
  7. package/runtime/components/AutomationInspector.vue +436 -0
  8. package/runtime/components/AutomationNodeIcon.vue +27 -0
  9. package/runtime/components/AutomationRunsPanel.vue +475 -0
  10. package/runtime/components/AutomationSidebar.vue +253 -0
  11. package/runtime/components/AutomationStatusIcon.vue +44 -0
  12. package/runtime/pages/automations.vue +1181 -0
  13. package/runtime/server/api/automations/[id]/endpoint.get.ts +12 -0
  14. package/runtime/server/api/automations/[id]/run.post.ts +30 -0
  15. package/runtime/server/api/automations/events.post.ts +37 -0
  16. package/runtime/server/api/automations/runs/[id]/resume.post.ts +23 -0
  17. package/runtime/server/handlers/automation-hook.post.ts +51 -0
  18. package/runtime/server/handlers/connect-hook.post.ts +54 -0
  19. package/runtime/server/handlers/integration-hook.post.ts +86 -0
  20. package/runtime/server/plugins/events.ts +79 -0
  21. package/runtime/server/plugins/maintenance.ts +20 -0
  22. package/runtime/server/utils/agent-runner.ts +15 -0
  23. package/runtime/server/utils/automation-context.ts +77 -0
  24. package/runtime/server/utils/automation-engine.ts +697 -0
  25. package/runtime/server/utils/automation-launch.ts +205 -0
  26. package/runtime/server/utils/automation-schedule.ts +31 -0
  27. package/runtime/server/utils/connection-events.ts +114 -0
  28. package/runtime/server/utils/maintenance.ts +255 -0
  29. package/runtime/server/utils/schedule-sync.ts +32 -0
  30. package/runtime/server/utils/transient-retry.ts +34 -0
  31. package/runtime/server/workflows/runner.ts +39 -0
  32. package/runtime/shared/agent.ts +5 -0
  33. package/runtime/shared/automation-run.ts +61 -0
  34. package/runtime/shared/automation.ts +202 -0
  35. package/runtime/workspace.ts +56 -0
  36. package/schema.ts +222 -0
  37. package/server.ts +2 -0
  38. package/workflow.ts +2 -0
@@ -0,0 +1,12 @@
1
+ import { defineEventHandler, getRequestURL, getRouterParam } from 'h3'
2
+ import { getAutomationContext } from '../../../utils/automation-context'
3
+ export default defineEventHandler(async (event) => {
4
+ const context = await getAutomationContext(event)
5
+ const automation = await context.api.automation.get(Number(getRouterParam(event, 'id')))
6
+ if (automation.workspace !== context.workspace) {
7
+ throw new Error('Choose an automation in this workspace.')
8
+ }
9
+ return {
10
+ url: `${getRequestURL(event).origin}/api/automation-hooks/${context.ownerId}/${automation.id}/${automation.webhookToken}/${(automation.trigger as { slug: string }).slug}`,
11
+ }
12
+ })
@@ -0,0 +1,30 @@
1
+ import { readCommandBody } from '@nuxtagentic/integrations/runtime/server/utils/request-body'
2
+ import { defineEventHandler, getRouterParam, setResponseStatus } from 'h3'
3
+ import { assertGoogleMutation } from '@nuxtagentic/google/runtime/server/utils/google'
4
+ import { toActionError } from '@nuxtagentic/integrations/runtime/server/utils/integration-context'
5
+ import { getAutomationContext } from '../../../utils/automation-context'
6
+ import { launchAutomation } from '../../../utils/automation-launch'
7
+ export default defineEventHandler(async (event) => {
8
+ assertGoogleMutation(event)
9
+ try {
10
+ const context = await getAutomationContext(event)
11
+ const automation = await context.api.automation.get(Number(getRouterParam(event, 'id')))
12
+ const body = await readCommandBody<{ payload: unknown; errorPath?: boolean; eventId?: string }>(
13
+ event,
14
+ 100000,
15
+ )
16
+ if (body.payload === undefined || JSON.stringify(body.payload).length > 100000) {
17
+ throw new Error('Provide a sample payload up to 100 KB.')
18
+ }
19
+ const result = await launchAutomation(
20
+ event,
21
+ context,
22
+ { automation, payload: body.payload, eventId: `test:${body.eventId || crypto.randomUUID()}` },
23
+ { test: true, forceError: !!body.errorPath },
24
+ )
25
+ setResponseStatus(event, 202)
26
+ return result
27
+ } catch (cause) {
28
+ throw toActionError(cause)
29
+ }
30
+ })
@@ -0,0 +1,37 @@
1
+ import { readCommandBody } from '@nuxtagentic/integrations/runtime/server/utils/request-body'
2
+ import { defineEventHandler, setResponseStatus } from 'h3'
3
+ import { assertGoogleMutation } from '@nuxtagentic/google/runtime/server/utils/google'
4
+ import { toActionError } from '@nuxtagentic/integrations/runtime/server/utils/integration-context'
5
+ import { getAutomationContext } from '../../utils/automation-context'
6
+ import { emitAutomationEvent } from '../../utils/automation-launch'
7
+ import { LOCAL_EVENTS } from '../../../shared/automation'
8
+ import { INTEGRATION_CATALOG } from '@nuxtagentic/integrations/runtime/shared/catalog'
9
+ export default defineEventHandler(async (event) => {
10
+ assertGoogleMutation(event)
11
+ try {
12
+ const context = await getAutomationContext(event)
13
+ const body = await readCommandBody<{ event: string; payload: unknown; id: string }>(
14
+ event,
15
+ 100000,
16
+ )
17
+ if (
18
+ ![
19
+ ...LOCAL_EVENTS.map((entry) => entry.id),
20
+ ...INTEGRATION_CATALOG.flatMap((entry) => entry.triggers.map((trigger) => trigger.id)),
21
+ ].includes(body.event) ||
22
+ !body.id ||
23
+ body.id.length > 200 ||
24
+ JSON.stringify(body.payload ?? null).length > 100000
25
+ ) {
26
+ throw new Error('Provide a supported event, unique id and payload up to 100 KB.')
27
+ }
28
+ setResponseStatus(event, 202)
29
+ return await emitAutomationEvent(event, context, {
30
+ name: body.event,
31
+ payload: body.payload ?? {},
32
+ id: body.id,
33
+ })
34
+ } catch (cause) {
35
+ throw toActionError(cause)
36
+ }
37
+ })
@@ -0,0 +1,23 @@
1
+ import { defineEventHandler, getRouterParam } from 'h3'
2
+ import { assertGoogleMutation } from '@nuxtagentic/google/runtime/server/utils/google'
3
+ import { getAutomationContext } from '../../../../utils/automation-context'
4
+ import { executeAutomation } from '../../../../utils/automation-engine'
5
+
6
+ export default defineEventHandler(async (event) => {
7
+ assertGoogleMutation(event)
8
+ const context = await getAutomationContext(event)
9
+ const run = await context.api.automationRun.get(Number(getRouterParam(event, 'id')))
10
+ if (run.workspace !== context.workspace || run.status !== 'waiting') {
11
+ throw new Error('This run is not waiting in this workspace.')
12
+ }
13
+ if (context.env?.AUTOMATION_RUNNER) {
14
+ throw new Error('Cloudflare resumes this run through its Workflow approval event.')
15
+ }
16
+ event.waitUntil(
17
+ executeAutomation(
18
+ { runId: run.id, ownerId: context.ownerId, workspace: context.workspace },
19
+ context,
20
+ ),
21
+ )
22
+ return { status: 'queued', runId: run.id }
23
+ })
@@ -0,0 +1,51 @@
1
+ import {
2
+ createError,
3
+ defineEventHandler,
4
+ getHeader,
5
+ getRouterParam,
6
+ setHeader,
7
+ setResponseStatus,
8
+ } from 'h3'
9
+ import { tokenMatches } from '@nuxtagentic/integrations/runtime/server/utils/integration-crypto'
10
+ import { toActionError } from '@nuxtagentic/integrations/runtime/server/utils/integration-context'
11
+ import { getWebhookContext } from '../utils/automation-context'
12
+ import { launchAutomation } from '../utils/automation-launch'
13
+ import { readCommandBody } from '@nuxtagentic/integrations/runtime/server/utils/request-body'
14
+ export default defineEventHandler(async (event) => {
15
+ setHeader(event, 'Cache-Control', 'no-store')
16
+ const ownerId = Number(getRouterParam(event, 'owner'))
17
+ const id = Number(getRouterParam(event, 'id'))
18
+ if (!Number.isSafeInteger(ownerId) || ownerId <= 0 || !Number.isSafeInteger(id) || id <= 0) {
19
+ throw createError({ statusCode: 404, statusMessage: 'Unknown webhook endpoint.' })
20
+ }
21
+ const context = await getWebhookContext(event, ownerId)
22
+ const automation = await context.api.automation.get(id).catch(() => null)
23
+ const trigger = automation?.trigger as { kind: string; slug: string } | undefined
24
+ if (
25
+ !automation ||
26
+ trigger?.kind !== 'webhook' ||
27
+ trigger.slug !== getRouterParam(event, 'slug') ||
28
+ !(await tokenMatches(getRouterParam(event, 'token') ?? '', automation.webhookToken))
29
+ ) {
30
+ throw createError({ statusCode: 404, statusMessage: 'Unknown webhook endpoint.' })
31
+ }
32
+ context.workspace = automation.workspace
33
+ try {
34
+ const payload = await readCommandBody<Record<string, unknown>>(event)
35
+ const eventId = getHeader(event, 'Idempotency-Key')
36
+ if (!eventId || eventId.length > 200) {
37
+ throw new Error(
38
+ 'Send an Idempotency-Key header with a unique event ID (up to 200 characters).',
39
+ )
40
+ }
41
+ const result = await launchAutomation(event, context, {
42
+ automation,
43
+ payload,
44
+ eventId: `webhook:${eventId}`,
45
+ })
46
+ setResponseStatus(event, 202)
47
+ return result
48
+ } catch (cause) {
49
+ throw toActionError(cause)
50
+ }
51
+ })
@@ -0,0 +1,54 @@
1
+ import { defineEventHandler, getRouterParam, setResponseStatus, toWebRequest } from 'h3'
2
+ import { ConnectError } from '@cloudconfig/core/connect'
3
+ import {
4
+ connectOf,
5
+ getDatabaseProvider,
6
+ } from '@nuxtagentic/integrations/runtime/server/utils/integration-context'
7
+ import { useRuntimeConfig } from '#imports'
8
+ import { catalogTrigger, emitConnectionEvent } from '../utils/connection-events'
9
+
10
+ /**
11
+ * App-level vendor webhooks (GitHub App, Slack, Shopify, …) at the URL
12
+ * `cloudconfig connect add` prints. Connect verifies the signature and finds
13
+ * the connections for the vendor account; this starts their automations.
14
+ */
15
+ export default defineEventHandler(async (event) => {
16
+ const provider = getRouterParam(event, 'provider') ?? ''
17
+ const env = event.context.cloudflare?.env as Record<string, unknown> | undefined
18
+ const settings = useRuntimeConfig(event).cloudconfig as { d1Binding?: string; localDb?: string }
19
+ const database = await getDatabaseProvider(env, settings)
20
+ const connect = await connectOf({
21
+ database,
22
+ env,
23
+ request: new Request('https://events.internal'),
24
+ })
25
+ try {
26
+ const result = await connect.receiveWebhook(provider, toWebRequest(event))
27
+ if (result.response) {
28
+ return result.response
29
+ }
30
+ for (const delivery of result.deliveries) {
31
+ const name = catalogTrigger(provider, delivery.event, delivery.payload)
32
+ if (!name) {
33
+ continue
34
+ }
35
+ await emitConnectionEvent(event, connect, {
36
+ connection: delivery.connection,
37
+ context: { database, env },
38
+ delivery: {
39
+ name,
40
+ payload: delivery.payload,
41
+ id: delivery.deliveryId || crypto.randomUUID(),
42
+ },
43
+ })
44
+ }
45
+ setResponseStatus(event, 202)
46
+ return { received: result.deliveries.length }
47
+ } catch (cause) {
48
+ if (cause instanceof ConnectError) {
49
+ setResponseStatus(event, cause.status)
50
+ return { error: cause.message }
51
+ }
52
+ throw cause
53
+ }
54
+ })
@@ -0,0 +1,86 @@
1
+ import {
2
+ createError,
3
+ defineEventHandler,
4
+ getHeader,
5
+ getRouterParam,
6
+ setHeader,
7
+ setResponseStatus,
8
+ toWebRequest,
9
+ } from 'h3'
10
+ import { ConnectError } from '@cloudconfig/core/connect'
11
+ import {
12
+ connectOf,
13
+ getDatabaseProvider,
14
+ toActionError,
15
+ } from '@nuxtagentic/integrations/runtime/server/utils/integration-context'
16
+ import { getIntegrationDefinition } from '@nuxtagentic/integrations/runtime/shared/catalog'
17
+ import { useRuntimeConfig } from '#imports'
18
+ import { catalogTrigger, emitConnectionEvent } from '../utils/connection-events'
19
+
20
+ /**
21
+ * Events for one connection, authenticated with that connection's own webhook
22
+ * secret: Connect's signature, a GitHub repository webhook signed with it, or
23
+ * the secret as a bearer token. The secret never appears in the URL.
24
+ */
25
+ export default defineEventHandler(async (event) => {
26
+ setHeader(event, 'Cache-Control', 'no-store')
27
+ const uid = getRouterParam(event, 'uid') ?? ''
28
+ const env = event.context.cloudflare?.env as Record<string, unknown> | undefined
29
+ const settings = useRuntimeConfig(event).cloudconfig as { d1Binding?: string; localDb?: string }
30
+ const database = await getDatabaseProvider(env, settings)
31
+ const connect = await connectOf({
32
+ database,
33
+ env,
34
+ request: new Request('https://events.internal'),
35
+ })
36
+ let received
37
+ try {
38
+ received = await connect.receiveConnectionEvent(uid, toWebRequest(event))
39
+ } catch (cause) {
40
+ if (cause instanceof ConnectError) {
41
+ throw createError({
42
+ statusCode: cause.status === 401 ? 404 : cause.status,
43
+ statusMessage: 'Unknown event endpoint.',
44
+ })
45
+ }
46
+ throw cause
47
+ }
48
+ try {
49
+ const body = (received.payload ?? {}) as Record<string, unknown>
50
+ const githubEvent = getHeader(event, 'X-GitHub-Event')
51
+ const name = githubEvent
52
+ ? catalogTrigger(
53
+ 'github',
54
+ body.action ? `${githubEvent}.${String(body.action)}` : githubEvent,
55
+ body,
56
+ )
57
+ : String(body.event ?? '')
58
+ // GitHub sends every subscribed event; ignore the ones no trigger listens for.
59
+ if (githubEvent && !name) {
60
+ return { runs: [] }
61
+ }
62
+ const provider = name.split('.')[0] ?? ''
63
+ if (!getIntegrationDefinition(provider)?.triggers.some((entry) => entry.id === name)) {
64
+ throw new Error('Choose an event supported by this integration.')
65
+ }
66
+ const delivery =
67
+ getHeader(event, 'X-GitHub-Delivery') ??
68
+ getHeader(event, 'Idempotency-Key') ??
69
+ String(body.id ?? '')
70
+ if (!delivery || delivery.length > 200) {
71
+ throw new Error('Send a unique event id or Idempotency-Key header, up to 200 characters.')
72
+ }
73
+ setResponseStatus(event, 202)
74
+ return await emitConnectionEvent(event, connect, {
75
+ connection: received.connection,
76
+ context: { database, env },
77
+ delivery: {
78
+ name,
79
+ payload: githubEvent ? body : (body.payload ?? {}),
80
+ id: delivery,
81
+ },
82
+ })
83
+ } catch (cause) {
84
+ throw toActionError(cause)
85
+ }
86
+ })
@@ -0,0 +1,79 @@
1
+ import { defineNitroPlugin } from 'nitropack/runtime'
2
+ import { getRequestURL, getResponseStatus, readBody } from 'h3'
3
+ import { getAutomationContext } from '../utils/automation-context'
4
+ import { emitAutomationEvent } from '../utils/automation-launch'
5
+ import { readJson } from '@nuxtagentic/integrations/runtime/server/utils/integration-adapters'
6
+
7
+ /** Successful API calls that raise a built-in automation event. */
8
+ const automationEventRoutes = [
9
+ { method: 'POST', path: /\/auth\/manage\/users$/, event: 'user.created' },
10
+ { method: 'POST', path: /\/auth\/ban-user$/, event: 'user.deactivated' },
11
+ { method: 'POST', path: /\/(?:files|file-content)$/, event: 'file.uploaded' },
12
+ { method: 'PATCH', path: /\/kanbanCards\/\d+$/, event: 'task.moved' },
13
+ ]
14
+
15
+ async function readResponseBody(body: unknown) {
16
+ if (!(body instanceof Response)) {
17
+ return body
18
+ }
19
+ return body.ok ? readJson(body.clone()) : null
20
+ }
21
+
22
+ function eventPayload(name: string, row: Record<string, unknown>) {
23
+ if (name.startsWith('user.')) {
24
+ return { user: row.user ?? row }
25
+ }
26
+ return {
27
+ record: { model: name === 'task.moved' ? 'kanbanCard' : 'file', id: row.id },
28
+ data: row,
29
+ }
30
+ }
31
+
32
+ export default defineNitroPlugin((nitro) => {
33
+ nitro.hooks.hook('beforeResponse', async (event, response) => {
34
+ const path = getRequestURL(event).pathname
35
+ const name = automationEventRoutes.find(
36
+ (route) => route.method === event.method && route.path.test(path),
37
+ )?.event
38
+ if (!name) {
39
+ return
40
+ }
41
+ const body = await readResponseBody(response.body)
42
+ if (!body || getResponseStatus(event) >= 400) {
43
+ return
44
+ }
45
+ const row = body as Record<string, unknown>
46
+ if (name === 'task.moved') {
47
+ const input = await readBody<Record<string, unknown>>(event)
48
+ if (!Object.hasOwn(input ?? {}, 'status')) {
49
+ return
50
+ }
51
+ }
52
+ event.context.automationEvent = {
53
+ name,
54
+ payload: eventPayload(name, row),
55
+ id: crypto.randomUUID(),
56
+ }
57
+ })
58
+ nitro.hooks.hook('afterResponse', (event) => {
59
+ const candidate = event.context.automationEvent as
60
+ { name: string; payload: unknown; id: string } | undefined
61
+ if (!candidate) {
62
+ return
63
+ }
64
+ event.waitUntil(
65
+ (async () => {
66
+ const context = await getAutomationContext(event)
67
+ await emitAutomationEvent(event, context, {
68
+ name: candidate.name,
69
+ payload: candidate.payload,
70
+ id: candidate.id,
71
+ })
72
+ })().catch(() => {
73
+ console.warn(
74
+ '[automations] Could not dispatch a workspace event. Check the automation configuration.',
75
+ )
76
+ }),
77
+ )
78
+ })
79
+ })
@@ -0,0 +1,20 @@
1
+ import { defineNitroPlugin, useRuntimeConfig } from 'nitropack/runtime'
2
+ import { runAutomationMaintenance, type MaintenanceDatabase } from '../utils/maintenance'
3
+
4
+ /** Runs on the Worker's cron trigger; see `triggers.crons` in wrangler.jsonc. */
5
+ export default defineNitroPlugin((nitro) => {
6
+ nitro.hooks.hook('cloudflare:scheduled', async ({ env }) => {
7
+ const bindings = env as Record<string, unknown>
8
+ const settings = useRuntimeConfig().cloudconfig as { d1Binding?: string } | undefined
9
+ const database = bindings[settings?.d1Binding ?? 'DB'] as MaintenanceDatabase | undefined
10
+ if (!database) {
11
+ console.error('[automations] Maintenance needs the D1 binding; it was skipped.')
12
+ return
13
+ }
14
+ const retentionDays = Number(bindings.NUXT_AUTOMATIONS_RUN_RETENTION_DAYS ?? 0)
15
+ const report = await runAutomationMaintenance(bindings, database, { retentionDays })
16
+ if (Object.values(report).some(Boolean)) {
17
+ console.log('[automations] Maintenance', JSON.stringify(report))
18
+ }
19
+ })
20
+ })
@@ -0,0 +1,15 @@
1
+ import {
2
+ runSavedAgent as runApiAgent,
3
+ type AgentInvocation,
4
+ } from '@cloudconfig/core/agents/runtime'
5
+ import { createRetryingModelFetch } from '@nuxtagentic/agents/runtime/server/utils/model-fetch'
6
+ export type { RunnableTool, AgentProviderKeys } from '@cloudconfig/core/agents/runtime'
7
+
8
+ export async function runSavedAgent(options: AgentInvocation) {
9
+ if (import.meta.dev && options.agent.runtime && options.agent.runtime !== 'api') {
10
+ return (
11
+ await import('@nuxtagentic/agents/runtime/server/utils/run-local-agent')
12
+ ).runLocalAgent(options)
13
+ }
14
+ return runApiAgent({ ...options, fetch: createRetryingModelFetch(options.fetch) })
15
+ }
@@ -0,0 +1,77 @@
1
+ import { useCloudConfigServer } from '#cloudconfig/server'
2
+ import { useRuntimeConfig } from '#imports'
3
+ import type { H3Event } from 'h3'
4
+ import { createServerClient } from '@cloudconfig/core/server'
5
+ import type { AgentsConfig } from '@cloudconfig/core/agents'
6
+ import {
7
+ getAgentProviderKeys,
8
+ getAgentProviderKeysForEvent,
9
+ } from '@nuxtagentic/agents/runtime/server/utils/agent-provider-keys'
10
+ import {
11
+ backendConfig,
12
+ getDatabaseProvider,
13
+ getIntegrationContext,
14
+ } from '@nuxtagentic/integrations/runtime/server/utils/integration-context'
15
+ import type { IntegrationsConfig } from '@nuxtagentic/integrations/schema'
16
+ import type { WorkspaceConfig } from '@nuxtagentic/workspace/schema'
17
+ import type { AutomationsConfig } from '../../../schema'
18
+ import type { AgentProviderKeys } from './agent-runner'
19
+
20
+ export type AutomationBackend = {
21
+ models: AutomationsConfig['models'] &
22
+ IntegrationsConfig['models'] &
23
+ AgentsConfig['models'] &
24
+ WorkspaceConfig['models']
25
+ }
26
+ export interface AutomationJob {
27
+ ownerId: number
28
+ runId: number
29
+ workspace: string
30
+ forceError?: boolean
31
+ }
32
+ /** Who an automation acts as: its owner (their and the workspace's connections) or only the workspace. */
33
+ export type AutomationRunAs = 'owner' | 'workspace'
34
+ export function providerKeys(values: Record<string, unknown>): AgentProviderKeys {
35
+ return getAgentProviderKeys(values)
36
+ }
37
+ export async function getAutomationContext(event: H3Event) {
38
+ const context = await getIntegrationContext(event)
39
+ const api = await useCloudConfigServer<AutomationBackend>(event)
40
+ const keys = getAgentProviderKeysForEvent(event)
41
+ return { ...context, api, keys }
42
+ }
43
+ export async function createJobContext(
44
+ job: AutomationJob,
45
+ env: Record<string, unknown> | undefined,
46
+ options: {
47
+ keys?: AgentProviderKeys
48
+ localDb?: string
49
+ d1Binding?: string
50
+ runAs?: AutomationRunAs
51
+ } = {},
52
+ ) {
53
+ const database = await getDatabaseProvider(env, options)
54
+ const request = new Request('https://automation.internal')
55
+ const api = createServerClient(
56
+ backendConfig as AutomationBackend,
57
+ { request, env },
58
+ { db: database, getUser: () => ({ id: job.ownerId }) },
59
+ )
60
+ return {
61
+ api,
62
+ database,
63
+ request,
64
+ env,
65
+ ownerId: job.ownerId,
66
+ workspace: job.workspace,
67
+ ...(options.runAs ? { runAs: options.runAs } : {}),
68
+ keys: options.keys ?? providerKeys({ ...process.env, ...env }),
69
+ }
70
+ }
71
+ export async function getWebhookContext(event: H3Event, ownerId: number) {
72
+ const runtime = useRuntimeConfig(event)
73
+ const env = event.context.cloudflare?.env as Record<string, unknown> | undefined
74
+ const settings = runtime.cloudconfig as { d1Binding?: string; localDb?: string }
75
+ const keys = getAgentProviderKeysForEvent(event)
76
+ return createJobContext({ ownerId, workspace: '', runId: 0 }, env, { ...settings, keys })
77
+ }