@ankhorage/flow 0.0.0-stage → 0.4.1
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/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +17 -2
- package/dist/ai.d.ts +7 -0
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +7 -0
- package/dist/ai.js.map +1 -0
- package/dist/features/execution/application/executeFlowAsync.d.ts +4 -0
- package/dist/features/execution/application/executeFlowAsync.d.ts.map +1 -0
- package/dist/features/execution/application/executeFlowAsync.js +211 -0
- package/dist/features/execution/application/executeFlowAsync.js.map +1 -0
- package/dist/features/execution/domain/assertFlowGraphSchemasSupported.d.ts +5 -0
- package/dist/features/execution/domain/assertFlowGraphSchemasSupported.d.ts.map +1 -0
- package/dist/features/execution/domain/assertFlowGraphSchemasSupported.js +11 -0
- package/dist/features/execution/domain/assertFlowGraphSchemasSupported.js.map +1 -0
- package/dist/features/execution/domain/compileFlowGraph.d.ts +5 -0
- package/dist/features/execution/domain/compileFlowGraph.d.ts.map +1 -0
- package/dist/features/execution/domain/compileFlowGraph.js +76 -0
- package/dist/features/execution/domain/compileFlowGraph.js.map +1 -0
- package/dist/features/execution/domain/createDefaultNodeRegistry.d.ts +4 -0
- package/dist/features/execution/domain/createDefaultNodeRegistry.d.ts.map +1 -0
- package/dist/features/execution/domain/createDefaultNodeRegistry.js +15 -0
- package/dist/features/execution/domain/createDefaultNodeRegistry.js.map +1 -0
- package/dist/features/execution/domain/validateFlowDataSchema.d.ts +6 -0
- package/dist/features/execution/domain/validateFlowDataSchema.d.ts.map +1 -0
- package/dist/features/execution/domain/validateFlowDataSchema.js +144 -0
- package/dist/features/execution/domain/validateFlowDataSchema.js.map +1 -0
- package/dist/features/mcp/McpToolError.d.ts +6 -0
- package/dist/features/mcp/McpToolError.d.ts.map +1 -0
- package/dist/features/mcp/McpToolError.js +10 -0
- package/dist/features/mcp/McpToolError.js.map +1 -0
- package/dist/features/mcp/createMcpRemoteTransport.d.ts +8 -0
- package/dist/features/mcp/createMcpRemoteTransport.d.ts.map +1 -0
- package/dist/features/mcp/createMcpRemoteTransport.js +23 -0
- package/dist/features/mcp/createMcpRemoteTransport.js.map +1 -0
- package/dist/features/mcp/createMcpToolRegistry.d.ts +10 -0
- package/dist/features/mcp/createMcpToolRegistry.d.ts.map +1 -0
- package/dist/features/mcp/createMcpToolRegistry.js +185 -0
- package/dist/features/mcp/createMcpToolRegistry.js.map +1 -0
- package/dist/features/mcp/loadMcpSdkAsync.d.ts +12 -0
- package/dist/features/mcp/loadMcpSdkAsync.d.ts.map +1 -0
- package/dist/features/mcp/loadMcpSdkAsync.js +20 -0
- package/dist/features/mcp/loadMcpSdkAsync.js.map +1 -0
- package/dist/features/mcp/prepareMcpProviders.d.ts +4 -0
- package/dist/features/mcp/prepareMcpProviders.d.ts.map +1 -0
- package/dist/features/mcp/prepareMcpProviders.js +97 -0
- package/dist/features/mcp/prepareMcpProviders.js.map +1 -0
- package/dist/features/mcp/validateMcpSchema.d.ts +6 -0
- package/dist/features/mcp/validateMcpSchema.d.ts.map +1 -0
- package/dist/features/mcp/validateMcpSchema.js +220 -0
- package/dist/features/mcp/validateMcpSchema.js.map +1 -0
- package/dist/features/security/FlowSecurityError.d.ts +6 -0
- package/dist/features/security/FlowSecurityError.d.ts.map +1 -0
- package/dist/features/security/FlowSecurityError.js +10 -0
- package/dist/features/security/FlowSecurityError.js.map +1 -0
- package/dist/features/security/application/createFlowSecurityPort.d.ts +12 -0
- package/dist/features/security/application/createFlowSecurityPort.d.ts.map +1 -0
- package/dist/features/security/application/createFlowSecurityPort.js +208 -0
- package/dist/features/security/application/createFlowSecurityPort.js.map +1 -0
- package/dist/features/security/domain/validateFlowSecurityTarget.d.ts +4 -0
- package/dist/features/security/domain/validateFlowSecurityTarget.d.ts.map +1 -0
- package/dist/features/security/domain/validateFlowSecurityTarget.js +57 -0
- package/dist/features/security/domain/validateFlowSecurityTarget.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.d.ts +4 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +3 -0
- package/dist/mcp.js.map +1 -0
- package/dist/social.d.ts +7 -0
- package/dist/social.d.ts.map +1 -0
- package/dist/social.js +7 -0
- package/dist/social.js.map +1 -0
- package/dist/types/execution.d.ts +33 -0
- package/dist/types/execution.d.ts.map +1 -0
- package/dist/types/execution.js +2 -0
- package/dist/types/execution.js.map +1 -0
- package/dist/types/mcp.d.ts +61 -0
- package/dist/types/mcp.d.ts.map +1 -0
- package/dist/types/mcp.js +2 -0
- package/dist/types/mcp.js.map +1 -0
- package/dist/types/security.d.ts +123 -0
- package/dist/types/security.d.ts.map +1 -0
- package/dist/types/security.js +2 -0
- package/dist/types/security.js.map +1 -0
- package/package.json +86 -4
- package/src/ai.ts +6 -0
- package/src/features/execution/application/executeFlowAsync.ts +338 -0
- package/src/features/execution/domain/assertFlowGraphSchemasSupported.ts +17 -0
- package/src/features/execution/domain/compileFlowGraph.ts +97 -0
- package/src/features/execution/domain/createDefaultNodeRegistry.ts +26 -0
- package/src/features/execution/domain/validateFlowDataSchema.ts +218 -0
- package/src/features/mcp/McpToolError.ts +10 -0
- package/src/features/mcp/createMcpRemoteTransport.ts +34 -0
- package/src/features/mcp/createMcpToolRegistry.ts +324 -0
- package/src/features/mcp/loadMcpSdkAsync.ts +32 -0
- package/src/features/mcp/prepareMcpProviders.ts +135 -0
- package/src/features/mcp/validateMcpSchema.ts +269 -0
- package/src/features/security/FlowSecurityError.ts +17 -0
- package/src/features/security/application/createFlowSecurityPort.ts +339 -0
- package/src/features/security/domain/validateFlowSecurityTarget.ts +73 -0
- package/src/index.ts +35 -0
- package/src/mcp.ts +11 -0
- package/src/social.ts +6 -0
- package/src/types/execution.ts +41 -0
- package/src/types/mcp.ts +70 -0
- package/src/types/security.ts +148 -0
|
@@ -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,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,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
|
+
};
|