import { automationExecutionOutputSchema, automationExecutionSummarySchema, AUTOMATION_INVOCATION_DEFAULT_ENTRYPOINT, automationInvocationOutputSchema, automationInvocationScheduleSchema, automationOutputSchema, cursorPageOutputSchema, cursorPaginationInputSchema, deploymentOutputSchema, deploymentStatusSchema, projectOutputSchema, } from "@automate.ax/api-contract" import { encodableSchema } from "@automate.ax/codec" import { decodeCodecEnvelope, encodeCodecEnvelope, } from "@automate.ax/codec/rpc" import * as z from "zod" import { defineAction } from "../../automation/actions" import { isAutomationDescriptor, normalizeAutomationSelector, type AutomationDescriptor, } from "../../automation/automation-descriptor" import { AUTOMATE_RUNTIME_CREDENTIAL, callAutomateApi, getAutomateApi, } from "./lib/api" import { AUTOMATE_DEPLOYMENT_CANCEL_SCOPE, AUTOMATE_DEPLOYMENT_CREATE_SCOPE, AUTOMATE_DEPLOYMENT_READ_SCOPE, AUTOMATE_PROJECT_READ_SCOPE, AUTOMATE_PROJECT_UPDATE_SCOPE, } from "./lib/scopes" const PROJECT_ID_SCHEMA = z.object({ /** Automate.ax project ID. */ projectId: z.string().min(1), }) const AUTOMATION_SELECTOR_SCHEMA = z.object({ /** Automation descriptor, description, or ID. */ automation: z .union([ z.string().min(1), z.custom(isAutomationDescriptor), ]) .transform(normalizeAutomationSelector), }) const DEPLOYMENT_ID_SCHEMA = z.object({ /** Automate.ax deployment ID. */ deploymentId: z.string().min(1), }) const AUTOMATE_ERROR_BASE_SCHEMA = z.object({ code: z.string().min(1), details: z.json().optional(), message: z.string(), name: z.string().optional(), }) const AUTOMATE_ERROR_DEPTH_0_SCHEMA = AUTOMATE_ERROR_BASE_SCHEMA.extend({ cause: z.never().optional(), }) const AUTOMATE_ERROR_DEPTH_1_SCHEMA = AUTOMATE_ERROR_BASE_SCHEMA.extend({ cause: AUTOMATE_ERROR_DEPTH_0_SCHEMA.optional(), }) const AUTOMATE_ERROR_DEPTH_2_SCHEMA = AUTOMATE_ERROR_BASE_SCHEMA.extend({ cause: AUTOMATE_ERROR_DEPTH_1_SCHEMA.optional(), }) const AUTOMATE_ERROR_SCHEMA = AUTOMATE_ERROR_BASE_SCHEMA.extend({ cause: AUTOMATE_ERROR_DEPTH_2_SCHEMA.optional(), }) const AUTOMATE_EXECUTION_SUMMARY_SCHEMA = automationExecutionSummarySchema .omit({ failure: true }) .extend({ failure: AUTOMATE_ERROR_SCHEMA.nullable() }) const AUTOMATE_INVOCATION_INPUT_SCHEMA = AUTOMATION_SELECTOR_SCHEMA.extend({ /** Programmatic invocation entrypoint configured by the target trigger. */ entrypoint: z .string() .min(1) .default(AUTOMATION_INVOCATION_DEFAULT_ENTRYPOINT), /** Encodable payload delivered to the target invocation trigger. */ payload: z.unknown().pipe(encodableSchema), }).and(automationInvocationScheduleSchema) const AUTOMATE_EXECUTION_OUTPUT_SCHEMA = automationExecutionOutputSchema .omit({ actions: true, events: true, failure: true, outputs: true }) .extend({ failure: AUTOMATE_ERROR_SCHEMA.nullable(), actions: automationExecutionOutputSchema.shape.actions.element .omit({ attempts: true, failure: true, output: true }) .extend({ attempts: automationExecutionOutputSchema.shape.actions.element.shape.attempts.element .omit({ failure: true }) .extend({ failure: AUTOMATE_ERROR_SCHEMA.nullable() }) .array(), failure: AUTOMATE_ERROR_SCHEMA.nullable(), output: encodableSchema.optional(), }) .array(), events: automationExecutionOutputSchema.shape.events.element .omit({ payload: true }) .extend({ payload: encodableSchema }) .array(), outputs: automationExecutionOutputSchema.shape.outputs.element .omit({ data: true }) .extend({ data: encodableSchema }) .array(), }) /** Lists projects in the selected Automate.ax organization. */ export const listAutomateProjects = defineAction("List Automate.ax projects") .describe( "Lists a cursor-paginated page of projects in the runtime or connected organization.", ) .account( [{ serviceId: "automate", requiredScope: AUTOMATE_PROJECT_READ_SCOPE }], { default: AUTOMATE_RUNTIME_CREDENTIAL }, ) .input( cursorPaginationInputSchema.extend({ /** Search project names. */ query: z.string().min(1).optional(), }), ) .output(cursorPageOutputSchema(projectOutputSchema)) .retry({ replaySafety: "safe" }) .handler(({ account, input, runtime }) => callAutomateApi(() => getAutomateApi(account, runtime).project.list(input)), ) /** Gets one project in the selected Automate.ax organization. */ export const getAutomateProject = defineAction("Get Automate.ax project") .describe("Gets one project, its organization, and its active automations.") .account( [{ serviceId: "automate", requiredScope: AUTOMATE_PROJECT_READ_SCOPE }], { default: AUTOMATE_RUNTIME_CREDENTIAL }, ) .input(PROJECT_ID_SCHEMA) .output(projectOutputSchema) .retry({ replaySafety: "safe" }) .handler(({ account, input, runtime }) => callAutomateApi(() => getAutomateApi(account, runtime).project.get(input)), ) /** Lists automations in the selected Automate.ax organization. */ export const listAutomateAutomations = defineAction( "List Automate.ax automations", ) .describe( "Lists active automations, optionally filtered by project or state.", ) .account( [{ serviceId: "automate", requiredScope: AUTOMATE_PROJECT_READ_SCOPE }], { default: AUTOMATE_RUNTIME_CREDENTIAL }, ) .input( cursorPaginationInputSchema.extend({ /** Filter by enabled state. */ enabled: z.boolean().optional(), /** Filter by project ID. */ projectId: z.string().min(1).optional(), /** Search automation descriptions and identity keys. */ query: z.string().min(1).optional(), }), ) .output(cursorPageOutputSchema(automationOutputSchema)) .retry({ replaySafety: "safe" }) .handler(({ account, input, runtime }) => callAutomateApi(() => getAutomateApi(account, runtime).automation.list(input), ), ) /** Gets one automation in the selected Automate.ax organization. */ export const getAutomateAutomation = defineAction("Get Automate.ax automation") .describe("Gets one active automation with triggers and account bindings.") .account( [{ serviceId: "automate", requiredScope: AUTOMATE_PROJECT_READ_SCOPE }], { default: AUTOMATE_RUNTIME_CREDENTIAL }, ) .input(AUTOMATION_SELECTOR_SCHEMA) .output(automationOutputSchema) .retry({ replaySafety: "safe" }) .handler(({ account, input, runtime }) => callAutomateApi(() => getAutomateApi(account, runtime).automation.get(input), ), ) /** Lists recent executions for one Automate.ax automation. */ export const listAutomateExecutions = defineAction( "List Automate.ax executions", ) .describe("Lists recent execution summaries for one automation.") .account( [{ serviceId: "automate", requiredScope: AUTOMATE_PROJECT_READ_SCOPE }], { default: AUTOMATE_RUNTIME_CREDENTIAL }, ) .input( AUTOMATION_SELECTOR_SCHEMA.extend({ /** Maximum executions to return. */ limit: z.number().int().min(1).max(100).default(50), }), ) .output(AUTOMATE_EXECUTION_SUMMARY_SCHEMA.array()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input, runtime }) => ( await callAutomateApi(() => getAutomateApi(account, runtime).automation.listExecutions(input), ) ).map(({ failure, ...execution }) => ({ ...execution, failure: parseAutomateError(failure), })), ) /** Gets one execution with events, outputs, actions, and logical attempts. */ export const getAutomateExecution = defineAction("Get Automate.ax execution") .describe( "Gets one execution with decoded events, outputs, actions, and logical attempt history.", ) .account( [{ serviceId: "automate", requiredScope: AUTOMATE_PROJECT_READ_SCOPE }], { default: AUTOMATE_RUNTIME_CREDENTIAL }, ) .input( AUTOMATION_SELECTOR_SCHEMA.extend({ /** Durable execution ID. */ executionId: z.string().min(1), }), ) .output(AUTOMATE_EXECUTION_OUTPUT_SCHEMA, { sensitive: true }) .retry({ replaySafety: "safe" }) .handler(async ({ account, input, runtime }) => { const execution = await callAutomateApi(() => getAutomateApi(account, runtime).automation.getExecution(input), ) const { failure, ...executionOutput } = execution return { ...executionOutput, failure: parseAutomateError(failure), actions: await Promise.all( execution.actions.map( async ({ attempts, failure: actionFailure, output, ...action }) => ({ ...action, attempts: attempts.map( ({ failure: attemptFailure, ...attempt }) => ({ ...attempt, failure: parseAutomateError(attemptFailure), }), ), failure: parseAutomateError(actionFailure), ...(output === undefined ? {} : { output: await decodeCodecEnvelope(output) }), }), ), ), events: await Promise.all( execution.events.map(async ({ payload, ...event }) => ({ ...event, payload: await decodeCodecEnvelope(payload), })), ), outputs: await Promise.all( execution.outputs.map(async ({ data, ...output }) => ({ ...output, data: await decodeCodecEnvelope(data), })), ), } }) /** * Parses one normalized execution failure into an encodable plain object. * * @param error - Normalized failure returned by the Automate.ax API. */ function parseAutomateError(error: unknown) { return error === null ? null : AUTOMATE_ERROR_SCHEMA.parse(error) } /** Enables or disables one Automate.ax automation. */ export const setAutomateAutomationEnabled = defineAction( "Set Automate.ax automation enabled", ) .describe("Sets the enabled state of one automation.") .account( [{ serviceId: "automate", requiredScope: AUTOMATE_PROJECT_UPDATE_SCOPE }], { default: AUTOMATE_RUNTIME_CREDENTIAL }, ) .input( AUTOMATION_SELECTOR_SCHEMA.extend({ /** Desired enabled state. */ enabled: z.boolean(), }), ) .output(z.object({ txid: z.number().int().nonnegative() })) .retry({ replaySafety: "safe" }) .handler(({ account, input, runtime }) => callAutomateApi(() => getAutomateApi(account, runtime).automation.update(input), ), ) /** Lists deployments in the selected Automate.ax organization. */ export const listAutomateDeployments = defineAction( "List Automate.ax deployments", ) .describe( "Lists deployments with project, status, and creation-time filters.", ) .account( [ { serviceId: "automate", requiredScope: AUTOMATE_DEPLOYMENT_READ_SCOPE, }, ], { default: AUTOMATE_RUNTIME_CREDENTIAL }, ) .input( cursorPaginationInputSchema .extend({ /** Filter by project ID. */ projectId: z.string().min(1).optional(), /** Include deployments created at or after this instant. */ since: z.date().optional(), /** Filter by normalized deployment status. */ status: deploymentStatusSchema.optional(), /** Include deployments created at or before this instant. */ until: z.date().optional(), }) .superRefine(({ since, until }, context) => { if (since !== undefined && until !== undefined && since > until) { context.addIssue({ code: "custom", message: "Since must be before or equal to until.", path: ["since"], }) } }), ) .output(cursorPageOutputSchema(deploymentOutputSchema)) .retry({ replaySafety: "safe" }) .handler(({ account, input, runtime }) => callAutomateApi(() => getAutomateApi(account, runtime).deployment.list(input), ), ) /** Gets one deployment in the selected Automate.ax organization. */ export const getAutomateDeployment = defineAction("Get Automate.ax deployment") .describe( "Gets one deployment with authorization and planned automation details.", ) .account( [ { serviceId: "automate", requiredScope: AUTOMATE_DEPLOYMENT_READ_SCOPE, }, ], { default: AUTOMATE_RUNTIME_CREDENTIAL }, ) .input(DEPLOYMENT_ID_SCHEMA) .output(deploymentOutputSchema) .retry({ replaySafety: "safe" }) .handler(({ account, input, runtime }) => callAutomateApi(() => getAutomateApi(account, runtime).deployment.get(input), ), ) /** Cancels an Automate.ax deployment before publishing begins. */ export const cancelAutomateDeployment = defineAction( "Cancel Automate.ax deployment", ) .describe( "Cancels a planning, awaiting-authorization, or configuring deployment.", ) .account( [ { serviceId: "automate", requiredScope: AUTOMATE_DEPLOYMENT_CANCEL_SCOPE, }, ], { default: AUTOMATE_RUNTIME_CREDENTIAL }, ) .input(DEPLOYMENT_ID_SCHEMA) .retry({ replaySafety: "safe" }) .handler(({ account, input, runtime }) => callAutomateApi(() => getAutomateApi(account, runtime).deployment.cancel(input), ), ) /** Redeploys a project from its active artifact with explicit bindings. */ export const redeployAutomateProject = defineAction( "Redeploy Automate.ax project", ) .describe( "Creates a deployment from a project's active artifact and explicit integration account bindings.", ) .account( [ { serviceId: "automate", requiredScope: AUTOMATE_DEPLOYMENT_CREATE_SCOPE, }, ], { default: AUTOMATE_RUNTIME_CREDENTIAL }, ) .input( PROJECT_ID_SCHEMA.extend({ /** Account bindings for the new deployment. */ bindings: z .object({ accountId: z.string().min(1), binding: z.string().min(1), serviceId: z.string().min(1), }) .array(), }).superRefine(({ bindings }, context) => { const keys = new Set() for (const [index, binding] of bindings.entries()) { const key = JSON.stringify([binding.serviceId, binding.binding]) if (keys.has(key)) { context.addIssue({ code: "custom", message: "Deployment bindings must be unique.", path: ["bindings", index], }) } keys.add(key) } }), ) .output(z.object({ id: z.string(), txid: z.number().int().nonnegative() })) .retry({ replaySafety: "unsafe" }) .handler(({ account, input, runtime }) => callAutomateApi(() => getAutomateApi(account, runtime).deployment.redeploy(input), ), ) /** Invokes an active automation through the management API. */ export const invokeAutomateAutomation = defineAction( "Invoke Automate.ax automation", ) .describe( "Schedules an immediate or delayed invocation of an active automation.", ) .account( [{ serviceId: "automate", requiredScope: AUTOMATE_PROJECT_UPDATE_SCOPE }], { default: AUTOMATE_RUNTIME_CREDENTIAL }, ) .input(AUTOMATE_INVOCATION_INPUT_SCHEMA) .output(automationInvocationOutputSchema) .retry({ replaySafety: "safe" }) .handler( async ({ account, input, runtime }) => await callAutomateApi(async () => getAutomateApi(account, runtime).automation.invoke({ ...input, payload: await encodeCodecEnvelope(input.payload), }), ), )