@ankhorage/flow 0.0.0-stage → 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.
Files changed (164) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/LICENSE +21 -0
  3. package/README.md +19 -2
  4. package/dist/ai.d.ts +7 -0
  5. package/dist/ai.d.ts.map +1 -0
  6. package/dist/ai.js +7 -0
  7. package/dist/ai.js.map +1 -0
  8. package/dist/execution.d.ts +5 -0
  9. package/dist/execution.d.ts.map +1 -0
  10. package/dist/execution.js +5 -0
  11. package/dist/execution.js.map +1 -0
  12. package/dist/features/execution/adapters/FileFlowPersistenceAdapter.d.ts +4 -0
  13. package/dist/features/execution/adapters/FileFlowPersistenceAdapter.d.ts.map +1 -0
  14. package/dist/features/execution/adapters/FileFlowPersistenceAdapter.js +219 -0
  15. package/dist/features/execution/adapters/FileFlowPersistenceAdapter.js.map +1 -0
  16. package/dist/features/execution/application/cancelDurableRunAsync.d.ts +8 -0
  17. package/dist/features/execution/application/cancelDurableRunAsync.d.ts.map +1 -0
  18. package/dist/features/execution/application/cancelDurableRunAsync.js +22 -0
  19. package/dist/features/execution/application/cancelDurableRunAsync.js.map +1 -0
  20. package/dist/features/execution/application/createDurableRunAsync.d.ts +12 -0
  21. package/dist/features/execution/application/createDurableRunAsync.d.ts.map +1 -0
  22. package/dist/features/execution/application/createDurableRunAsync.js +8 -0
  23. package/dist/features/execution/application/createDurableRunAsync.js.map +1 -0
  24. package/dist/features/execution/application/executeFlowAsync.d.ts +4 -0
  25. package/dist/features/execution/application/executeFlowAsync.d.ts.map +1 -0
  26. package/dist/features/execution/application/executeFlowAsync.js +211 -0
  27. package/dist/features/execution/application/executeFlowAsync.js.map +1 -0
  28. package/dist/features/execution/application/runDurableWorkerAsync.d.ts +52 -0
  29. package/dist/features/execution/application/runDurableWorkerAsync.d.ts.map +1 -0
  30. package/dist/features/execution/application/runDurableWorkerAsync.js +195 -0
  31. package/dist/features/execution/application/runDurableWorkerAsync.js.map +1 -0
  32. package/dist/features/execution/domain/assertFlowGraphSchemasSupported.d.ts +5 -0
  33. package/dist/features/execution/domain/assertFlowGraphSchemasSupported.d.ts.map +1 -0
  34. package/dist/features/execution/domain/assertFlowGraphSchemasSupported.js +11 -0
  35. package/dist/features/execution/domain/assertFlowGraphSchemasSupported.js.map +1 -0
  36. package/dist/features/execution/domain/calculateRetryDueAt.d.ts +4 -0
  37. package/dist/features/execution/domain/calculateRetryDueAt.d.ts.map +1 -0
  38. package/dist/features/execution/domain/calculateRetryDueAt.js +14 -0
  39. package/dist/features/execution/domain/calculateRetryDueAt.js.map +1 -0
  40. package/dist/features/execution/domain/compileFlowGraph.d.ts +5 -0
  41. package/dist/features/execution/domain/compileFlowGraph.d.ts.map +1 -0
  42. package/dist/features/execution/domain/compileFlowGraph.js +76 -0
  43. package/dist/features/execution/domain/compileFlowGraph.js.map +1 -0
  44. package/dist/features/execution/domain/createDefaultNodeRegistry.d.ts +4 -0
  45. package/dist/features/execution/domain/createDefaultNodeRegistry.d.ts.map +1 -0
  46. package/dist/features/execution/domain/createDefaultNodeRegistry.js +15 -0
  47. package/dist/features/execution/domain/createDefaultNodeRegistry.js.map +1 -0
  48. package/dist/features/execution/domain/createDurableRun.d.ts +7 -0
  49. package/dist/features/execution/domain/createDurableRun.d.ts.map +1 -0
  50. package/dist/features/execution/domain/createDurableRun.js +10 -0
  51. package/dist/features/execution/domain/createDurableRun.js.map +1 -0
  52. package/dist/features/execution/domain/createRetryWork.d.ts +4 -0
  53. package/dist/features/execution/domain/createRetryWork.d.ts.map +1 -0
  54. package/dist/features/execution/domain/createRetryWork.js +17 -0
  55. package/dist/features/execution/domain/createRetryWork.js.map +1 -0
  56. package/dist/features/execution/domain/isCronDueAt.d.ts +7 -0
  57. package/dist/features/execution/domain/isCronDueAt.d.ts.map +1 -0
  58. package/dist/features/execution/domain/isCronDueAt.js +114 -0
  59. package/dist/features/execution/domain/isCronDueAt.js.map +1 -0
  60. package/dist/features/execution/domain/validateFlowDataSchema.d.ts +6 -0
  61. package/dist/features/execution/domain/validateFlowDataSchema.d.ts.map +1 -0
  62. package/dist/features/execution/domain/validateFlowDataSchema.js +144 -0
  63. package/dist/features/execution/domain/validateFlowDataSchema.js.map +1 -0
  64. package/dist/features/execution/ports/FlowPersistencePort.d.ts +34 -0
  65. package/dist/features/execution/ports/FlowPersistencePort.d.ts.map +1 -0
  66. package/dist/features/execution/ports/FlowPersistencePort.js +2 -0
  67. package/dist/features/execution/ports/FlowPersistencePort.js.map +1 -0
  68. package/dist/features/mcp/McpToolError.d.ts +6 -0
  69. package/dist/features/mcp/McpToolError.d.ts.map +1 -0
  70. package/dist/features/mcp/McpToolError.js +10 -0
  71. package/dist/features/mcp/McpToolError.js.map +1 -0
  72. package/dist/features/mcp/createMcpRemoteTransport.d.ts +8 -0
  73. package/dist/features/mcp/createMcpRemoteTransport.d.ts.map +1 -0
  74. package/dist/features/mcp/createMcpRemoteTransport.js +23 -0
  75. package/dist/features/mcp/createMcpRemoteTransport.js.map +1 -0
  76. package/dist/features/mcp/createMcpToolRegistry.d.ts +10 -0
  77. package/dist/features/mcp/createMcpToolRegistry.d.ts.map +1 -0
  78. package/dist/features/mcp/createMcpToolRegistry.js +185 -0
  79. package/dist/features/mcp/createMcpToolRegistry.js.map +1 -0
  80. package/dist/features/mcp/loadMcpSdkAsync.d.ts +12 -0
  81. package/dist/features/mcp/loadMcpSdkAsync.d.ts.map +1 -0
  82. package/dist/features/mcp/loadMcpSdkAsync.js +20 -0
  83. package/dist/features/mcp/loadMcpSdkAsync.js.map +1 -0
  84. package/dist/features/mcp/prepareMcpProviders.d.ts +4 -0
  85. package/dist/features/mcp/prepareMcpProviders.d.ts.map +1 -0
  86. package/dist/features/mcp/prepareMcpProviders.js +97 -0
  87. package/dist/features/mcp/prepareMcpProviders.js.map +1 -0
  88. package/dist/features/mcp/validateMcpSchema.d.ts +6 -0
  89. package/dist/features/mcp/validateMcpSchema.d.ts.map +1 -0
  90. package/dist/features/mcp/validateMcpSchema.js +220 -0
  91. package/dist/features/mcp/validateMcpSchema.js.map +1 -0
  92. package/dist/features/security/FlowSecurityError.d.ts +6 -0
  93. package/dist/features/security/FlowSecurityError.d.ts.map +1 -0
  94. package/dist/features/security/FlowSecurityError.js +10 -0
  95. package/dist/features/security/FlowSecurityError.js.map +1 -0
  96. package/dist/features/security/application/createFlowSecurityPort.d.ts +12 -0
  97. package/dist/features/security/application/createFlowSecurityPort.d.ts.map +1 -0
  98. package/dist/features/security/application/createFlowSecurityPort.js +208 -0
  99. package/dist/features/security/application/createFlowSecurityPort.js.map +1 -0
  100. package/dist/features/security/domain/validateFlowSecurityTarget.d.ts +4 -0
  101. package/dist/features/security/domain/validateFlowSecurityTarget.d.ts.map +1 -0
  102. package/dist/features/security/domain/validateFlowSecurityTarget.js +57 -0
  103. package/dist/features/security/domain/validateFlowSecurityTarget.js.map +1 -0
  104. package/dist/index.d.ts +16 -0
  105. package/dist/index.d.ts.map +1 -0
  106. package/dist/index.js +12 -0
  107. package/dist/index.js.map +1 -0
  108. package/dist/mcp.d.ts +4 -0
  109. package/dist/mcp.d.ts.map +1 -0
  110. package/dist/mcp.js +3 -0
  111. package/dist/mcp.js.map +1 -0
  112. package/dist/social.d.ts +7 -0
  113. package/dist/social.d.ts.map +1 -0
  114. package/dist/social.js +7 -0
  115. package/dist/social.js.map +1 -0
  116. package/dist/types/durableExecution.d.ts +55 -0
  117. package/dist/types/durableExecution.d.ts.map +1 -0
  118. package/dist/types/durableExecution.js +2 -0
  119. package/dist/types/durableExecution.js.map +1 -0
  120. package/dist/types/execution.d.ts +33 -0
  121. package/dist/types/execution.d.ts.map +1 -0
  122. package/dist/types/execution.js +2 -0
  123. package/dist/types/execution.js.map +1 -0
  124. package/dist/types/mcp.d.ts +61 -0
  125. package/dist/types/mcp.d.ts.map +1 -0
  126. package/dist/types/mcp.js +2 -0
  127. package/dist/types/mcp.js.map +1 -0
  128. package/dist/types/security.d.ts +123 -0
  129. package/dist/types/security.d.ts.map +1 -0
  130. package/dist/types/security.js +2 -0
  131. package/dist/types/security.js.map +1 -0
  132. package/package.json +90 -4
  133. package/src/ai.ts +6 -0
  134. package/src/execution.ts +9 -0
  135. package/src/features/execution/adapters/FileFlowPersistenceAdapter.ts +333 -0
  136. package/src/features/execution/application/cancelDurableRunAsync.ts +28 -0
  137. package/src/features/execution/application/createDurableRunAsync.ts +14 -0
  138. package/src/features/execution/application/executeFlowAsync.ts +338 -0
  139. package/src/features/execution/application/runDurableWorkerAsync.ts +337 -0
  140. package/src/features/execution/domain/assertFlowGraphSchemasSupported.ts +17 -0
  141. package/src/features/execution/domain/calculateRetryDueAt.ts +21 -0
  142. package/src/features/execution/domain/compileFlowGraph.ts +97 -0
  143. package/src/features/execution/domain/createDefaultNodeRegistry.ts +26 -0
  144. package/src/features/execution/domain/createDurableRun.ts +14 -0
  145. package/src/features/execution/domain/createRetryWork.ts +22 -0
  146. package/src/features/execution/domain/isCronDueAt.ts +131 -0
  147. package/src/features/execution/domain/validateFlowDataSchema.ts +218 -0
  148. package/src/features/execution/ports/FlowPersistencePort.ts +38 -0
  149. package/src/features/mcp/McpToolError.ts +10 -0
  150. package/src/features/mcp/createMcpRemoteTransport.ts +34 -0
  151. package/src/features/mcp/createMcpToolRegistry.ts +324 -0
  152. package/src/features/mcp/loadMcpSdkAsync.ts +32 -0
  153. package/src/features/mcp/prepareMcpProviders.ts +135 -0
  154. package/src/features/mcp/validateMcpSchema.ts +269 -0
  155. package/src/features/security/FlowSecurityError.ts +17 -0
  156. package/src/features/security/application/createFlowSecurityPort.ts +339 -0
  157. package/src/features/security/domain/validateFlowSecurityTarget.ts +73 -0
  158. package/src/index.ts +49 -0
  159. package/src/mcp.ts +11 -0
  160. package/src/social.ts +6 -0
  161. package/src/types/durableExecution.ts +61 -0
  162. package/src/types/execution.ts +41 -0
  163. package/src/types/mcp.ts +70 -0
  164. package/src/types/security.ts +148 -0
@@ -0,0 +1,21 @@
1
+ import type { FlowRetryPolicy } from '../../../types/durableExecution.js';
2
+
3
+ /*** Calculate an exponential retry time from persisted attempt state without ambient time. */
4
+ export const calculateRetryDueAt = (
5
+ now: Date,
6
+ completedAttempt: number,
7
+ policy: FlowRetryPolicy,
8
+ ): string => {
9
+ if (!Number.isSafeInteger(completedAttempt) || completedAttempt < 1)
10
+ throw new Error('FLOW_RETRY_ATTEMPT_INVALID');
11
+ if (!Number.isSafeInteger(policy.baseDelayMs) || policy.baseDelayMs < 0)
12
+ throw new Error('FLOW_RETRY_BASE_DELAY_INVALID');
13
+ if (
14
+ !Number.isSafeInteger(policy.maxDelayMs) ||
15
+ policy.maxDelayMs < policy.baseDelayMs ||
16
+ policy.maxDelayMs < 0
17
+ )
18
+ throw new Error('FLOW_RETRY_MAX_DELAY_INVALID');
19
+ const delay = Math.min(policy.maxDelayMs, policy.baseDelayMs * 2 ** (completedAttempt - 1));
20
+ return new Date(now.getTime() + delay).toISOString();
21
+ };
@@ -0,0 +1,97 @@
1
+ import type { FlowDefinition, FlowEdge, FlowNode } from '@ankhorage/contracts/flow';
2
+
3
+ import type { FlowCompiledGraph, FlowNodeRegistry } from '../../../types/execution.js';
4
+
5
+ /*** Compile a valid acyclic graph into deterministic execution order. */
6
+ export const compileFlowGraph = (
7
+ definition: FlowDefinition,
8
+ registry: FlowNodeRegistry,
9
+ ): FlowCompiledGraph => {
10
+ const nodesById = indexNodes(definition.nodes);
11
+ const edgesBySourceNodeId = indexEdges(definition.edges, nodesById);
12
+ for (const node of definition.nodes) assertNodeIsExecutable(node, registry);
13
+ return {
14
+ definition,
15
+ edgesBySourceNodeId,
16
+ topologicalNodeIds: orderNodes(
17
+ definition.nodes.map(({ id }) => id),
18
+ definition.edges,
19
+ ),
20
+ };
21
+ };
22
+
23
+ /*** Index nodes while rejecting duplicate graph identifiers. */
24
+ const indexNodes = (nodes: readonly FlowNode[]): Readonly<Record<string, FlowNode>> => {
25
+ const entries = nodes.map((node) => [node.id, node] as const);
26
+ if (new Set(entries.map(([id]) => id)).size !== entries.length)
27
+ throw new Error('FLOW_GRAPH_DUPLICATE_NODE_ID');
28
+ return Object.fromEntries(entries);
29
+ };
30
+
31
+ /*** Validate and group graph edges by their producing node. */
32
+ const indexEdges = (
33
+ edges: readonly FlowEdge[],
34
+ nodesById: Readonly<Record<string, FlowNode>>,
35
+ ): Readonly<Record<string, readonly FlowEdge[]>> => {
36
+ if (new Set(edges.map(({ id }) => id)).size !== edges.length)
37
+ throw new Error('FLOW_GRAPH_DUPLICATE_EDGE_ID');
38
+ for (const edge of edges) {
39
+ const source = nodesById[edge.source.nodeId];
40
+ const target = nodesById[edge.target.nodeId];
41
+ if (!source || !target) throw new Error(`FLOW_GRAPH_UNKNOWN_EDGE_NODE:${edge.id}`);
42
+ assertPort(source, edge.source.portId, 'output', edge.id);
43
+ assertPort(target, edge.target.portId, 'input', edge.id);
44
+ }
45
+ return edges.reduce<Readonly<Record<string, readonly FlowEdge[]>>>(
46
+ (grouped, edge) => ({
47
+ ...grouped,
48
+ [edge.source.nodeId]: [...(grouped[edge.source.nodeId] ?? []), edge],
49
+ }),
50
+ {},
51
+ );
52
+ };
53
+
54
+ /*** Ensure a graph port exists exactly once with the expected direction. */
55
+ const assertPort = (
56
+ node: FlowNode,
57
+ portId: string,
58
+ direction: 'input' | 'output',
59
+ edgeId: string,
60
+ ): void => {
61
+ if (node.ports.filter((port) => port.id === portId && port.direction === direction).length !== 1)
62
+ throw new Error(`FLOW_GRAPH_INVALID_EDGE_PORT:${edgeId}`);
63
+ };
64
+
65
+ /*** Reject unknown operations and code capabilities before execution can begin. */
66
+ const assertNodeIsExecutable = (node: FlowNode, registry: FlowNodeRegistry): void => {
67
+ const { capabilityId, providerId } = node.capability;
68
+ const operationId = `${providerId}:${capabilityId}`;
69
+ if (capabilityId === 'code') throw new Error('FLOW_CODE_EXECUTION_DISABLED');
70
+ if (!Object.hasOwn(registry, operationId))
71
+ throw new Error(`FLOW_OPERATION_UNREGISTERED:${operationId}`);
72
+ if (
73
+ new Set(node.ports.map(({ direction, id }) => `${direction}:${id}`)).size !== node.ports.length
74
+ )
75
+ throw new Error(`FLOW_GRAPH_DUPLICATE_PORT_ID:${node.id}`);
76
+ };
77
+
78
+ /*** Produce stable topological order and reject cycles without an explicit bounded-loop contract. */
79
+ const orderNodes = (nodeIds: readonly string[], edges: readonly FlowEdge[]): readonly string[] => {
80
+ const incomingCounts = new Map(nodeIds.map((nodeId) => [nodeId, 0]));
81
+ for (const edge of edges)
82
+ incomingCounts.set(edge.target.nodeId, (incomingCounts.get(edge.target.nodeId) ?? 0) + 1);
83
+ const pending = nodeIds.filter((nodeId) => incomingCounts.get(nodeId) === 0);
84
+ const ordered: string[] = [];
85
+ while (pending.length > 0) {
86
+ const nodeId = pending.shift();
87
+ if (!nodeId) continue;
88
+ ordered.push(nodeId);
89
+ for (const edge of edges.filter((candidate) => candidate.source.nodeId === nodeId)) {
90
+ const nextCount = (incomingCounts.get(edge.target.nodeId) ?? 0) - 1;
91
+ incomingCounts.set(edge.target.nodeId, nextCount);
92
+ if (nextCount === 0) pending.push(edge.target.nodeId);
93
+ }
94
+ }
95
+ if (ordered.length !== nodeIds.length) throw new Error('FLOW_GRAPH_CYCLE');
96
+ return ordered;
97
+ };
@@ -0,0 +1,26 @@
1
+ import type { FlowDataRef, FlowNodeConfigValue } from '@ankhorage/contracts/flow';
2
+
3
+ import type { FlowNodeOperation, FlowNodeRegistry } from '../../../types/execution.js';
4
+
5
+ /*** Create the built-in operations that have no provider or host dependency. */
6
+ export const createDefaultNodeRegistry = (): FlowNodeRegistry => ({
7
+ 'flow:condition': { executeAsync: () => Promise.resolve({}) },
8
+ 'flow:noop': { executeAsync: () => Promise.resolve({}) },
9
+ 'flow:transform': { executeAsync: transformAsync },
10
+ });
11
+
12
+ /*** Copy configured inline or artifact values to matching output ports without evaluating code. */
13
+ const transformAsync: FlowNodeOperation['executeAsync'] = ({ config, node }) => {
14
+ const outputIds = new Set(
15
+ node.ports.filter((port) => port.direction === 'output').map(({ id }) => id),
16
+ );
17
+ const output = Object.fromEntries(
18
+ Object.entries(config ?? {}).filter(
19
+ (entry): entry is [string, FlowDataRef] => outputIds.has(entry[0]) && isDataRef(entry[1]),
20
+ ),
21
+ );
22
+ return Promise.resolve({ output });
23
+ };
24
+
25
+ /*** Narrow a configuration value to data that may safely leave a node. */
26
+ const isDataRef = (value: FlowNodeConfigValue): value is FlowDataRef => value.kind !== 'secret';
@@ -0,0 +1,14 @@
1
+ import type { FlowDefinitionRef, FlowDurableRun } from '../../../types/durableExecution.js';
2
+
3
+ /*** Create a revision-zero run pinned permanently to one published flow definition. */
4
+ export const createDurableRun = (
5
+ input: { readonly flow: FlowDefinitionRef; readonly id: string },
6
+ now: Date,
7
+ ): FlowDurableRun => ({
8
+ createdAt: now.toISOString(),
9
+ flow: { ...input.flow },
10
+ id: input.id,
11
+ revision: 0,
12
+ status: 'queued',
13
+ updatedAt: now.toISOString(),
14
+ });
@@ -0,0 +1,22 @@
1
+ import type { FlowDurableWork, FlowRetryPolicy } from '../../../types/durableExecution.js';
2
+ import { calculateRetryDueAt } from './calculateRetryDueAt.js';
3
+
4
+ /*** Create the next immutable work attempt from a completed attempt and controlled clock. */
5
+ export const createRetryWork = (
6
+ completed: FlowDurableWork,
7
+ now: Date,
8
+ policy: FlowRetryPolicy,
9
+ ): FlowDurableWork => ({
10
+ ...(completed.idempotencyKey === undefined ? {} : { idempotencyKey: completed.idempotencyKey }),
11
+ attempt: completed.attempt + 1,
12
+ dueAt: calculateRetryDueAt(now, completed.attempt, policy),
13
+ effectKind: completed.effectKind,
14
+ flow: { ...completed.flow },
15
+ id: `${completed.runId}:attempt:${completed.attempt + 1}`,
16
+ runId: completed.runId,
17
+ ...(completed.securityOperation === undefined
18
+ ? {}
19
+ : { securityOperation: completed.securityOperation }),
20
+ ...(completed.stepId === undefined ? {} : { stepId: completed.stepId }),
21
+ status: 'ready',
22
+ });
@@ -0,0 +1,131 @@
1
+ /** Determine whether one UTC instant matches a five-field POSIX cron expression in an IANA zone. */
2
+ export const isCronDueAt = (input: {
3
+ readonly cron: string;
4
+ readonly now: Date;
5
+ readonly timezone?: string;
6
+ }): boolean => {
7
+ const fields = input.cron.trim().split(/\s+/);
8
+ if (fields.length !== 5) throw new Error('FLOW_CRON_INVALID');
9
+ const local = localParts(input.now, input.timezone ?? 'Etc/UTC');
10
+ const [minute, hour, dayOfMonth, month, dayOfWeek] = fields;
11
+ if (
12
+ minute === undefined ||
13
+ hour === undefined ||
14
+ dayOfMonth === undefined ||
15
+ month === undefined ||
16
+ dayOfWeek === undefined
17
+ )
18
+ throw new Error('FLOW_CRON_INVALID');
19
+ const minuteMatches = matchesField(minute, local.minute, 0, 59);
20
+ const hourMatches = matchesField(hour, local.hour, 0, 23);
21
+ const monthMatches = matchesField(month, local.month, 1, 12);
22
+ const dayOfMonthMatches = matchesField(dayOfMonth, local.day, 1, 31);
23
+ const dayOfWeekMatches = matchesField(dayOfWeek, local.dayOfWeek, 0, 7);
24
+ const dayMatches =
25
+ dayOfMonth === '*' || dayOfWeek === '*'
26
+ ? dayOfMonthMatches && dayOfWeekMatches
27
+ : dayOfMonthMatches || dayOfWeekMatches;
28
+ return minuteMatches && hourMatches && monthMatches && dayMatches;
29
+ };
30
+
31
+ /** Format a UTC instant into its real local wall-clock fields; duplicate fall-back instants remain distinct instants. */
32
+ const localParts = (
33
+ now: Date,
34
+ timezone: string,
35
+ ): {
36
+ readonly day: number;
37
+ readonly dayOfWeek: number;
38
+ readonly hour: number;
39
+ readonly minute: number;
40
+ readonly month: number;
41
+ } => {
42
+ try {
43
+ const parts = new Intl.DateTimeFormat('en-US', {
44
+ day: 'numeric',
45
+ hour: 'numeric',
46
+ hourCycle: 'h23',
47
+ minute: 'numeric',
48
+ month: 'numeric',
49
+ timeZone: timezone,
50
+ weekday: 'short',
51
+ }).formatToParts(now);
52
+ const read = (type: Intl.DateTimeFormatPartTypes): string =>
53
+ parts.find((part) => part.type === type)?.value ?? invalidTimezone();
54
+ return {
55
+ day: Number(read('day')),
56
+ dayOfWeek: weekday(read('weekday')),
57
+ hour: Number(read('hour')),
58
+ minute: Number(read('minute')),
59
+ month: Number(read('month')),
60
+ };
61
+ } catch {
62
+ throw new Error('FLOW_CRON_TIMEZONE_INVALID');
63
+ }
64
+ };
65
+
66
+ /** Match one POSIX cron field including lists, ranges, and positive step values. */
67
+ const matchesField = (field: string, value: number, minimum: number, maximum: number): boolean =>
68
+ field.split(',').some((part) => matchesPart(part, value, minimum, maximum));
69
+
70
+ /** Match one validated cron list member. */
71
+ const matchesPart = (part: string, value: number, minimum: number, maximum: number): boolean => {
72
+ const [rangePart, stepPart] = part.split('/');
73
+ if (part.split('/').length > 2 || rangePart === undefined) throw new Error('FLOW_CRON_INVALID');
74
+ const step = stepPart === undefined ? 1 : parseValue(stepPart, 1, maximum - minimum + 1);
75
+ const [start, end] = parseRange(rangePart, minimum, maximum);
76
+ return value >= start && value <= end && (value - start) % step === 0;
77
+ };
78
+
79
+ /** Parse a wildcard, number, or inclusive numeric range. */
80
+ const parseRange = (range: string, minimum: number, maximum: number): readonly [number, number] => {
81
+ if (range === '*') return [minimum, maximum];
82
+ const values = range.split('-');
83
+ if (values.length === 1) {
84
+ const [single] = values;
85
+ if (single === undefined) throw new Error('FLOW_CRON_INVALID');
86
+ const value = parseValue(single, minimum, maximum);
87
+ return [value, value];
88
+ }
89
+ if (values.length !== 2 || values[0] === undefined || values[1] === undefined)
90
+ throw new Error('FLOW_CRON_INVALID');
91
+ const start = parseValue(values[0], minimum, maximum);
92
+ const end = parseValue(values[1], minimum, maximum);
93
+ if (end < start) throw new Error('FLOW_CRON_INVALID');
94
+ return [start, end];
95
+ };
96
+
97
+ /** Parse one strict POSIX cron integer. */
98
+ const parseValue = (value: string, minimum: number, maximum: number): number => {
99
+ if (!/^\d+$/.test(value)) throw new Error('FLOW_CRON_INVALID');
100
+ const parsed = Number(value);
101
+ if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum)
102
+ throw new Error('FLOW_CRON_INVALID');
103
+ return parsed;
104
+ };
105
+
106
+ /** Map English short weekday names produced by the fixed formatter to POSIX weekday values. */
107
+ const weekday = (value: string): number => {
108
+ switch (value) {
109
+ case 'Sun':
110
+ return 0;
111
+ case 'Mon':
112
+ return 1;
113
+ case 'Tue':
114
+ return 2;
115
+ case 'Wed':
116
+ return 3;
117
+ case 'Thu':
118
+ return 4;
119
+ case 'Fri':
120
+ return 5;
121
+ case 'Sat':
122
+ return 6;
123
+ default:
124
+ return invalidTimezone();
125
+ }
126
+ };
127
+
128
+ /** Keep an impossible formatter shape distinct from a cron syntax error. */
129
+ const invalidTimezone = (): never => {
130
+ throw new Error('FLOW_CRON_TIMEZONE_INVALID');
131
+ };
@@ -0,0 +1,218 @@
1
+ import type { DataContractValue, DataSchema, DataSchemaRegistry } from '@ankhorage/contracts/data';
2
+
3
+ const maximumSchemaDepth = 64;
4
+
5
+ /*** Validate serializable data against the supported portable schema vocabulary. */
6
+ export const validateFlowDataSchema = (
7
+ value: DataContractValue,
8
+ schema: DataSchema,
9
+ schemas: DataSchemaRegistry | undefined,
10
+ ): boolean => {
11
+ assertFlowDataSchemaSupported(schema, schemas);
12
+ return matchesSchema(value, schema, schemas, createTraversalState());
13
+ };
14
+
15
+ /*** Reject unsupported, unresolved, cyclic, or excessively nested portable schemas before use. */
16
+ export const assertFlowDataSchemaSupported = (
17
+ schema: DataSchema,
18
+ schemas: DataSchemaRegistry | undefined,
19
+ ): void => assertSupportedSchema(schema, schemas, createTraversalState());
20
+
21
+ /*** Resolve a schema reference only from the explicit execution registry. */
22
+ const resolveSchema = (
23
+ schema: DataSchema,
24
+ schemas: DataSchemaRegistry | undefined,
25
+ state: SchemaTraversalState,
26
+ ): DataSchema => {
27
+ if (!schema.ref) return schema;
28
+ if (state.referenceIds.has(schema.ref.id))
29
+ throw new Error(`FLOW_SCHEMA_REFERENCE_CYCLE:${schema.ref.id}`);
30
+ const resolved = schemas?.[schema.ref.id];
31
+ if (!resolved) throw new Error(`FLOW_SCHEMA_REFERENCE_UNRESOLVED:${schema.ref.id}`);
32
+ return resolved;
33
+ };
34
+
35
+ /*** Apply portable schema constraints without coercing a JSON-compatible value. */
36
+ const matchesSchema = (
37
+ value: DataContractValue,
38
+ schema: DataSchema,
39
+ schemas: DataSchemaRegistry | undefined,
40
+ state: SchemaTraversalState,
41
+ ): boolean => {
42
+ const resolved = resolveSchema(schema, schemas, state);
43
+ const nextState = advanceTraversalState(resolved, schema.ref?.id, state);
44
+ return (
45
+ matchesTypes(value, resolved) &&
46
+ matchesValues(value, resolved) &&
47
+ matchesComposition(value, resolved, schemas, nextState) &&
48
+ matchesStructure(value, resolved, schemas, nextState)
49
+ );
50
+ };
51
+
52
+ /*** Reject portable annotations that require a validator capability this engine does not own. */
53
+ const assertSupportedSchema = (
54
+ schema: DataSchema,
55
+ schemas: DataSchemaRegistry | undefined,
56
+ state: SchemaTraversalState,
57
+ ): void => {
58
+ if (schema.format !== undefined) throw new Error(`FLOW_SCHEMA_UNSUPPORTED_CONSTRAINT:format`);
59
+ const nextState = advanceTraversalState(schema, undefined, state);
60
+ visitChildSchemas(schema, schemas, nextState);
61
+ if (!schema.ref) return;
62
+ const resolved = resolveSchema(schema, schemas, state);
63
+ assertSupportedSchema(resolved, schemas, {
64
+ ...nextState,
65
+ referenceIds: new Set([...state.referenceIds, schema.ref.id]),
66
+ });
67
+ };
68
+
69
+ /*** Visit every schema child so unsupported declarations fail before value matching begins. */
70
+ const visitChildSchemas = (
71
+ schema: DataSchema,
72
+ schemas: DataSchemaRegistry | undefined,
73
+ state: SchemaTraversalState,
74
+ ): void => {
75
+ const children = [
76
+ ...(schema.items ? [schema.items] : []),
77
+ ...(typeof schema.additionalProperties === 'object' ? [schema.additionalProperties] : []),
78
+ ...Object.values(schema.properties ?? {}),
79
+ ...(schema.allOf ?? []),
80
+ ...(schema.anyOf ?? []),
81
+ ...(schema.oneOf ?? []),
82
+ ];
83
+ for (const child of children) assertSupportedSchema(child, schemas, state);
84
+ };
85
+
86
+ /*** Match the declared primitive type union and explicit nullability. */
87
+ const matchesTypes = (value: DataContractValue, schema: DataSchema): boolean => {
88
+ if (value === null && schema.nullable) return true;
89
+ if (schema.type === undefined) return true;
90
+ if (typeof schema.type === 'string') return matchesType(value, schema.type);
91
+ return schema.type.some((type) => matchesType(value, type));
92
+ };
93
+
94
+ /*** Match one JSON primitive category without coercion. */
95
+ const matchesType = (value: DataContractValue, type: string): boolean => {
96
+ if (type === 'array') return isDataArray(value);
97
+ if (type === 'integer') return typeof value === 'number' && Number.isInteger(value);
98
+ if (type === 'null') return value === null;
99
+ if (type === 'object') return isDataRecord(value);
100
+ return typeof value === type;
101
+ };
102
+
103
+ /*** Apply constant and enumeration constraints using structural JSON equality. */
104
+ const matchesValues = (value: DataContractValue, schema: DataSchema): boolean =>
105
+ (schema.const === undefined || structurallyEqual(value, schema.const)) &&
106
+ (!schema.enum || schema.enum.some((candidate) => structurallyEqual(value, candidate)));
107
+
108
+ /*** Apply logical schema composition with its standard cardinality semantics. */
109
+ const matchesComposition = (
110
+ value: DataContractValue,
111
+ schema: DataSchema,
112
+ schemas: DataSchemaRegistry | undefined,
113
+ state: SchemaTraversalState,
114
+ ): boolean =>
115
+ (!schema.allOf ||
116
+ schema.allOf.every((candidate) => matchesSchema(value, candidate, schemas, state))) &&
117
+ (!schema.anyOf ||
118
+ schema.anyOf.some((candidate) => matchesSchema(value, candidate, schemas, state))) &&
119
+ (!schema.oneOf ||
120
+ schema.oneOf.filter((candidate) => matchesSchema(value, candidate, schemas, state)).length ===
121
+ 1);
122
+
123
+ /*** Recursively apply object and array member constraints. */
124
+ const matchesStructure = (
125
+ value: DataContractValue,
126
+ schema: DataSchema,
127
+ schemas: DataSchemaRegistry | undefined,
128
+ state: SchemaTraversalState,
129
+ ): boolean =>
130
+ matchesItems(value, schema.items, schemas, state) &&
131
+ (!isDataRecord(value) || matchesProperties(value, schema, schemas, state));
132
+
133
+ /*** Validate array elements only when an item schema is declared. */
134
+ const matchesItems = (
135
+ value: DataContractValue,
136
+ items: DataSchema | undefined,
137
+ schemas: DataSchemaRegistry | undefined,
138
+ state: SchemaTraversalState,
139
+ ): boolean =>
140
+ !items ||
141
+ !isDataArray(value) ||
142
+ value.every((item) => matchesSchema(item, items, schemas, state));
143
+
144
+ /*** Validate required object members and declared or additional property schemas. */
145
+ const matchesProperties = (
146
+ value: Readonly<Record<string, DataContractValue>>,
147
+ schema: DataSchema,
148
+ schemas: DataSchemaRegistry | undefined,
149
+ state: SchemaTraversalState,
150
+ ): boolean => {
151
+ const properties = schema.properties ?? {};
152
+ return (
153
+ (schema.required ?? []).every((key) => Object.entries(value).some(([name]) => name === key)) &&
154
+ Object.entries(value).every(([key, item]) => {
155
+ const property = Object.entries(properties).find(([name]) => name === key)?.[1];
156
+ if (property) return matchesSchema(item, property, schemas, state);
157
+ if (schema.additionalProperties === false) return false;
158
+ return (
159
+ typeof schema.additionalProperties !== 'object' ||
160
+ matchesSchema(item, schema.additionalProperties, schemas, state)
161
+ );
162
+ })
163
+ );
164
+ };
165
+
166
+ interface SchemaTraversalState {
167
+ readonly depth: number;
168
+ readonly referenceIds: ReadonlySet<string>;
169
+ readonly schemas: ReadonlySet<DataSchema>;
170
+ }
171
+
172
+ /*** Start bounded schema traversal without retaining mutable validation state. */
173
+ const createTraversalState = (): SchemaTraversalState => ({
174
+ depth: 0,
175
+ referenceIds: new Set(),
176
+ schemas: new Set(),
177
+ });
178
+
179
+ /*** Advance a schema traversal while rejecting direct cycles and excessive nesting deterministically. */
180
+ const advanceTraversalState = (
181
+ schema: DataSchema,
182
+ referenceId: string | undefined,
183
+ state: SchemaTraversalState,
184
+ ): SchemaTraversalState => {
185
+ if (state.depth >= maximumSchemaDepth) throw new Error('FLOW_SCHEMA_MAX_DEPTH');
186
+ if (state.schemas.has(schema)) throw new Error('FLOW_SCHEMA_CYCLE');
187
+ return {
188
+ depth: state.depth + 1,
189
+ referenceIds: referenceId ? new Set([...state.referenceIds, referenceId]) : state.referenceIds,
190
+ schemas: new Set([...state.schemas, schema]),
191
+ };
192
+ };
193
+
194
+ /*** Compare JSON-compatible values independently of object key order. */
195
+ const structurallyEqual = (left: DataContractValue, right: DataContractValue): boolean =>
196
+ JSON.stringify(normalizeValue(left)) === JSON.stringify(normalizeValue(right));
197
+
198
+ /*** Canonicalize object member ordering for structural comparison. */
199
+ const normalizeValue = (value: DataContractValue): DataContractValue => {
200
+ if (isDataArray(value)) return value.map(normalizeValue);
201
+ if (isDataRecord(value))
202
+ return Object.fromEntries(
203
+ Object.entries(value)
204
+ .sort(([left], [right]) => left.localeCompare(right))
205
+ .map(([key, item]) => [key, normalizeValue(item)]),
206
+ );
207
+ return value;
208
+ };
209
+
210
+ /*** Narrow a JSON-compatible object to a non-array member record. */
211
+ const isDataRecord = (
212
+ value: DataContractValue,
213
+ ): value is Readonly<Record<string, DataContractValue>> =>
214
+ value !== null && typeof value === 'object' && !isDataArray(value);
215
+
216
+ /*** Narrow a JSON-compatible array without losing its serializable member type. */
217
+ const isDataArray = (value: DataContractValue): value is readonly DataContractValue[] =>
218
+ Array.isArray(value);
@@ -0,0 +1,38 @@
1
+ import type {
2
+ FlowDurableRun,
3
+ FlowDurableWork,
4
+ FlowOutboxRecord,
5
+ } from '../../../types/durableExecution.js';
6
+
7
+ export interface FlowPersistencePort {
8
+ readonly cancelRunWorkAsync: (runId: string) => Promise<readonly string[]>;
9
+ readonly claimDueWorkAsync: (input: {
10
+ readonly leaseDurationMs: number;
11
+ readonly limit: number;
12
+ readonly now: Date;
13
+ readonly workerId: string;
14
+ }) => Promise<readonly FlowDurableWork[]>;
15
+ readonly completeWorkAsync: (input: {
16
+ readonly id: string;
17
+ readonly leaseFencingToken: number;
18
+ readonly now: Date;
19
+ readonly workerId: string;
20
+ }) => Promise<boolean>;
21
+ readonly createOutboxAsync: (record: FlowOutboxRecord) => Promise<void>;
22
+ readonly createRunAsync: (run: FlowDurableRun) => Promise<void>;
23
+ readonly enqueueWorkAsync: (work: FlowDurableWork) => Promise<void>;
24
+ readonly markOutboxDeliveredAsync: (id: string) => Promise<boolean>;
25
+ readonly readOutboxAsync: (id: string) => Promise<FlowOutboxRecord | undefined>;
26
+ readonly readRunAsync: (id: string) => Promise<FlowDurableRun | undefined>;
27
+ readonly recoverExpiredClaimsAsync: (now: Date) => Promise<readonly string[]>;
28
+ readonly releaseWorkAsync: (input: {
29
+ readonly dueAt: string;
30
+ readonly id: string;
31
+ readonly leaseFencingToken: number;
32
+ readonly workerId: string;
33
+ }) => Promise<boolean>;
34
+ readonly transitionRunAsync: (input: {
35
+ readonly expectedRevision: number;
36
+ readonly run: FlowDurableRun;
37
+ }) => Promise<boolean>;
38
+ }
@@ -0,0 +1,10 @@
1
+ /*** Represents a stable, redaction-safe MCP failure. */
2
+ export class McpToolError extends Error {
3
+ constructor(
4
+ readonly code: string,
5
+ message: string,
6
+ ) {
7
+ super(message);
8
+ this.name = 'McpToolError';
9
+ }
10
+ }
@@ -0,0 +1,34 @@
1
+ import type { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
2
+
3
+ import type { McpProviderDefinition } from '../../types/mcp.js';
4
+ import type { FlowOutboundTransportPort } from '../../types/security.js';
5
+ import { loadMcpSdkAsync } from './loadMcpSdkAsync.js';
6
+ import { McpToolError } from './McpToolError.js';
7
+
8
+ /*** Opens remote MCP only through the host-owned transport that controls every peer connection. */
9
+ export const createMcpRemoteTransportAsync = async (
10
+ provider: Extract<McpProviderDefinition, { readonly transport: 'remote' }>,
11
+ outboundTransport: FlowOutboundTransportPort | undefined,
12
+ ): Promise<StreamableHTTPClientTransport> => {
13
+ if (!outboundTransport)
14
+ throw new McpToolError(
15
+ 'remote_security_boundary_unavailable',
16
+ 'Remote MCP requires a host-owned Flow security transport boundary.',
17
+ );
18
+ const { StreamableHTTPClientTransport } = await loadMcpSdkAsync();
19
+ return new StreamableHTTPClientTransport(new URL(provider.endpoint), {
20
+ fetch: async (url, init) =>
21
+ outboundTransport.fetchAsync({
22
+ allowedHosts: provider.allowedHosts,
23
+ ...(init === undefined ? {} : { init }),
24
+ url: url.toString(),
25
+ }),
26
+ redirectPolicy: 'same-origin',
27
+ reconnectionOptions: {
28
+ initialReconnectionDelay: 100,
29
+ maxReconnectionDelay: 1_000,
30
+ reconnectionDelayGrowFactor: 1,
31
+ maxRetries: 0,
32
+ },
33
+ });
34
+ };