@enactprotocol/execution 2.2.4 → 2.3.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/src/provider.ts CHANGED
@@ -7,11 +7,13 @@
7
7
 
8
8
  import { basename } from "node:path";
9
9
  import { type Client, type Container, ReturnType, connect } from "@dagger.io/dagger";
10
- import type { ToolManifest } from "@enactprotocol/shared";
10
+ import type { Action, ActionsManifest, ToolManifest } from "@enactprotocol/shared";
11
11
  import {
12
12
  applyDefaults,
13
13
  detectRuntime,
14
+ getEffectiveInputSchema,
14
15
  interpolateCommand,
16
+ prepareActionCommand,
15
17
  validateInputs,
16
18
  } from "@enactprotocol/shared";
17
19
  import type {
@@ -150,26 +152,8 @@ export class DaggerExecutionProvider implements ExecutionProvider {
150
152
  );
151
153
  }
152
154
 
153
- // Validate inputs against schema
154
- if (manifest.inputSchema) {
155
- const validation = validateInputs(input.params, manifest.inputSchema);
156
- if (!validation.valid) {
157
- const errorMessages = validation.errors.map((e) => `${e.path}: ${e.message}`);
158
- return this.createErrorResult(
159
- manifest.name,
160
- containerImage,
161
- executionId,
162
- startTime,
163
- "VALIDATION_ERROR",
164
- `Input validation failed: ${errorMessages.join(", ")}`
165
- );
166
- }
167
- }
168
-
169
- // Apply defaults to inputs
170
- const params = manifest.inputSchema
171
- ? applyDefaults(input.params, manifest.inputSchema)
172
- : input.params;
155
+ // Pass params through (scripts handle their own validation via action inputSchema)
156
+ const params = input.params;
173
157
 
174
158
  // Get the command from the manifest
175
159
  const command = manifest.command;
@@ -184,15 +168,10 @@ export class DaggerExecutionProvider implements ExecutionProvider {
184
168
  );
185
169
  }
186
170
 
187
- // Interpolate command with parameters - only substitute known inputSchema params
188
- // Use onMissing: "empty" so optional params without values become empty strings
189
- // (validation already caught truly missing required params above)
190
- const knownParameters = manifest.inputSchema?.properties
191
- ? new Set(Object.keys(manifest.inputSchema.properties))
192
- : undefined;
171
+ // Interpolate command with parameters
172
+ // Without per-script schema, all ${param} patterns are substituted
193
173
  const interpolated = interpolateCommand(command, params, {
194
174
  onMissing: "empty",
195
- ...(knownParameters ? { knownParameters } : {}),
196
175
  });
197
176
 
198
177
  // Parse timeout
@@ -305,6 +284,198 @@ export class DaggerExecutionProvider implements ExecutionProvider {
305
284
  return this.execute(execManifest, input, options);
306
285
  }
307
286
 
287
+ /**
288
+ * Execute an action from ACTIONS.yaml
289
+ *
290
+ * This method uses the {{param}} template system which:
291
+ * - Passes commands directly to execve() (no shell interpolation)
292
+ * - Each template becomes a single argument regardless of content
293
+ * - Omits arguments for optional params without values
294
+ *
295
+ * @param manifest - The skill manifest (SKILL.md)
296
+ * @param actionsManifest - The actions manifest (ACTIONS.yaml)
297
+ * @param actionName - The name of the action to execute
298
+ * @param action - The specific action definition to execute
299
+ * @param input - Execution input with params
300
+ * @param options - Execution options
301
+ */
302
+ async executeAction(
303
+ manifest: ToolManifest,
304
+ actionsManifest: ActionsManifest,
305
+ actionName: string,
306
+ action: Action,
307
+ input: ExecutionInput,
308
+ options: ExecutionOptions = {}
309
+ ): Promise<ExecutionResult> {
310
+ const startTime = new Date();
311
+ const executionId = this.generateExecutionId();
312
+
313
+ // Get container image from manifest
314
+ const containerImage = manifest.from ?? "alpine:latest";
315
+
316
+ // Ensure initialized
317
+ if (!this.initialized) {
318
+ await this.initialize();
319
+ }
320
+
321
+ // Check runtime availability
322
+ if (!this.detectedRuntime) {
323
+ return this.createErrorResult(
324
+ manifest.name,
325
+ containerImage,
326
+ executionId,
327
+ startTime,
328
+ "RUNTIME_NOT_FOUND",
329
+ "No container runtime available (docker, podman, or nerdctl required)"
330
+ );
331
+ }
332
+
333
+ // Get effective inputSchema (defaults to empty if not provided)
334
+ const effectiveSchema = getEffectiveInputSchema(action);
335
+
336
+ // Validate inputs against action's inputSchema
337
+ const validation = validateInputs(input.params, effectiveSchema);
338
+ if (!validation.valid) {
339
+ const errorMessages = validation.errors.map((e) => `${e.path}: ${e.message}`);
340
+ return this.createErrorResult(
341
+ manifest.name,
342
+ containerImage,
343
+ executionId,
344
+ startTime,
345
+ "VALIDATION_ERROR",
346
+ `Input validation failed: ${errorMessages.join(", ")}`
347
+ );
348
+ }
349
+
350
+ // Apply defaults to inputs
351
+ const params = applyDefaults(input.params, effectiveSchema);
352
+
353
+ // Prepare the command using {{param}} template system
354
+ // This returns an array suitable for execve() - no shell interpolation
355
+ let commandArray: string[];
356
+ try {
357
+ const actionCommand = action.command;
358
+ if (typeof actionCommand === "string") {
359
+ // String-form command (no templates allowed - validation ensures this)
360
+ // Split into args for execve
361
+ commandArray = actionCommand.split(/\s+/).filter((s) => s.length > 0);
362
+ } else {
363
+ // Array-form command with {{param}} templates
364
+ commandArray = prepareActionCommand(actionCommand, params, effectiveSchema);
365
+ }
366
+ } catch (error) {
367
+ const message = error instanceof Error ? error.message : String(error);
368
+ return this.createErrorResult(
369
+ manifest.name,
370
+ containerImage,
371
+ executionId,
372
+ startTime,
373
+ "COMMAND_ERROR",
374
+ `Failed to prepare action command: ${message}`
375
+ );
376
+ }
377
+
378
+ if (commandArray.length === 0) {
379
+ return this.createErrorResult(
380
+ manifest.name,
381
+ containerImage,
382
+ executionId,
383
+ startTime,
384
+ "COMMAND_ERROR",
385
+ "Action command is empty"
386
+ );
387
+ }
388
+
389
+ // Parse timeout
390
+ const timeoutMs = this.parseTimeout(options.timeout ?? manifest.timeout);
391
+
392
+ // Execute with Dagger using direct exec (no shell wrapper)
393
+ try {
394
+ const output = await this.runActionContainer(
395
+ manifest,
396
+ actionsManifest,
397
+ containerImage,
398
+ commandArray,
399
+ input,
400
+ options,
401
+ timeoutMs
402
+ );
403
+
404
+ const endTime = new Date();
405
+ this.consecutiveFailures = 0;
406
+ this.lastHealthCheck = endTime;
407
+
408
+ const metadata: ExecutionMetadata = {
409
+ toolName: `${manifest.name}:${actionName}`,
410
+ containerImage,
411
+ startTime,
412
+ endTime,
413
+ durationMs: endTime.getTime() - startTime.getTime(),
414
+ cached: false,
415
+ executionId,
416
+ };
417
+
418
+ if (manifest.version) {
419
+ metadata.toolVersion = manifest.version;
420
+ }
421
+
422
+ if (output.exitCode !== 0) {
423
+ return {
424
+ success: false,
425
+ output,
426
+ metadata,
427
+ error: {
428
+ code: "COMMAND_ERROR",
429
+ message: `Action "${actionName}" exited with code ${output.exitCode}`,
430
+ ...(output.stderr && { details: { stderr: output.stderr } }),
431
+ },
432
+ };
433
+ }
434
+
435
+ // Validate output against outputSchema if defined
436
+ if (action.outputSchema && output.stdout) {
437
+ try {
438
+ const parsed = JSON.parse(output.stdout);
439
+ output.parsed = parsed;
440
+ // TODO: Validate parsed output against outputSchema
441
+ } catch {
442
+ // Output is not JSON - that's OK, leave as string
443
+ }
444
+ }
445
+
446
+ return {
447
+ success: true,
448
+ output,
449
+ metadata,
450
+ };
451
+ } catch (error) {
452
+ this.consecutiveFailures++;
453
+ const errorMessage = error instanceof Error ? error.message : String(error);
454
+
455
+ let code: ExecutionErrorCode = "CONTAINER_ERROR";
456
+ let displayMessage = errorMessage;
457
+
458
+ if (errorMessage.startsWith("BUILD_ERROR:")) {
459
+ code = "BUILD_ERROR";
460
+ displayMessage = errorMessage.slice("BUILD_ERROR:".length).trim();
461
+ } else if (errorMessage.includes("timeout") || errorMessage === "TIMEOUT") {
462
+ code = "TIMEOUT";
463
+ displayMessage = `Action execution timed out: ${errorMessage}`;
464
+ } else {
465
+ displayMessage = `Action execution failed: ${errorMessage}`;
466
+ }
467
+
468
+ return this.createErrorResult(
469
+ manifest.name,
470
+ containerImage,
471
+ executionId,
472
+ startTime,
473
+ code,
474
+ displayMessage
475
+ );
476
+ }
477
+ }
478
+
308
479
  /**
309
480
  * Shutdown the provider
310
481
  */
@@ -553,6 +724,216 @@ export class DaggerExecutionProvider implements ExecutionProvider {
553
724
  });
554
725
  }
555
726
 
727
+ /**
728
+ * Run action container with direct exec (no shell wrapper)
729
+ *
730
+ * This is the key difference from runContainer - commands are passed
731
+ * directly to execve() without shell interpolation, preventing injection.
732
+ */
733
+ private async runActionContainer(
734
+ manifest: ToolManifest,
735
+ actionsManifest: ActionsManifest,
736
+ containerImage: string,
737
+ commandArray: string[],
738
+ input: ExecutionInput,
739
+ options: ExecutionOptions,
740
+ timeoutMs: number
741
+ ): Promise<ExecutionOutput> {
742
+ return new Promise<ExecutionOutput>((resolve, reject) => {
743
+ connect(
744
+ async (client: Client) => {
745
+ try {
746
+ // Create container from image
747
+ let container: Container = client.container().from(containerImage);
748
+
749
+ // Force the image to be pulled before timeout
750
+ await container.platform();
751
+
752
+ // Set working directory
753
+ const workdir = options.workdir ?? this.config.workdir ?? "/workspace";
754
+ container = container.withWorkdir(workdir);
755
+
756
+ // Add environment variables from ACTIONS.yaml env block
757
+ if (actionsManifest.env) {
758
+ for (const [key, envVar] of Object.entries(actionsManifest.env)) {
759
+ if (!envVar.secret && envVar.default) {
760
+ container = container.withEnvVariable(key, envVar.default);
761
+ }
762
+ }
763
+ }
764
+
765
+ // Add environment variables from manifest (SKILL.md)
766
+ if (manifest.env) {
767
+ for (const [key, envVar] of Object.entries(manifest.env)) {
768
+ if (!envVar.secret && envVar.default) {
769
+ container = container.withEnvVariable(key, envVar.default);
770
+ }
771
+ }
772
+ }
773
+
774
+ // Add additional environment variables from options
775
+ if (options.additionalEnv) {
776
+ for (const [key, value] of Object.entries(options.additionalEnv)) {
777
+ container = container.withEnvVariable(key, String(value));
778
+ }
779
+ }
780
+
781
+ // Add environment variable overrides from input
782
+ if (input.envOverrides) {
783
+ for (const [key, value] of Object.entries(input.envOverrides)) {
784
+ container = container.withEnvVariable(key, value);
785
+ }
786
+ }
787
+
788
+ // Add secrets from input overrides
789
+ if (input.secretOverrides) {
790
+ for (const [key, secretUri] of Object.entries(input.secretOverrides)) {
791
+ const secret = client.secret(secretUri);
792
+ container = container.withSecretVariable(key, secret);
793
+ }
794
+ }
795
+
796
+ // Mount directories
797
+ if (options.mountDirs) {
798
+ for (const [source, target] of Object.entries(options.mountDirs)) {
799
+ container = container.withDirectory(target, client.host().directory(source));
800
+ }
801
+ }
802
+
803
+ // Mount input paths
804
+ if (options.inputPaths) {
805
+ for (const inputPath of options.inputPaths) {
806
+ if (inputPath.name) {
807
+ const target = `/inputs/${inputPath.name}`;
808
+ if (inputPath.type === "file") {
809
+ container = container.withFile(target, client.host().file(inputPath.path));
810
+ } else {
811
+ container = container.withDirectory(
812
+ target,
813
+ client.host().directory(inputPath.path)
814
+ );
815
+ }
816
+ } else if (inputPath.type === "file") {
817
+ const filename = basename(inputPath.path);
818
+ container = container.withFile(
819
+ `/input/${filename}`,
820
+ client.host().file(inputPath.path)
821
+ );
822
+ } else {
823
+ container = container.withDirectory(
824
+ "/input",
825
+ client.host().directory(inputPath.path)
826
+ );
827
+ }
828
+ }
829
+ }
830
+
831
+ // Run build commands from ACTIONS.yaml (cached)
832
+ const buildCommands = actionsManifest.build
833
+ ? Array.isArray(actionsManifest.build)
834
+ ? actionsManifest.build
835
+ : [actionsManifest.build]
836
+ : manifest.build
837
+ ? Array.isArray(manifest.build)
838
+ ? manifest.build
839
+ : [manifest.build]
840
+ : [];
841
+
842
+ for (let i = 0; i < buildCommands.length; i++) {
843
+ const buildCmd = buildCommands[i] as string;
844
+ container = container.withExec(["sh", "-c", buildCmd], { expect: ReturnType.Any });
845
+ const buildContainer = await container.sync();
846
+
847
+ const buildExitCode = await buildContainer.exitCode();
848
+ const buildStdout = await buildContainer.stdout();
849
+ const buildStderr = await buildContainer.stderr();
850
+
851
+ if (buildExitCode !== 0) {
852
+ const stepInfo =
853
+ buildCommands.length > 1
854
+ ? `Build failed at step ${i + 1} of ${buildCommands.length}`
855
+ : "Build failed";
856
+
857
+ const details = [
858
+ stepInfo,
859
+ `Command: ${buildCmd}`,
860
+ `Exit code: ${buildExitCode}`,
861
+ buildStderr ? `\nstderr:\n${buildStderr}` : "",
862
+ buildStdout ? `\nstdout:\n${buildStdout}` : "",
863
+ ]
864
+ .filter(Boolean)
865
+ .join("\n");
866
+
867
+ throw new Error(`BUILD_ERROR: ${details}`);
868
+ }
869
+ }
870
+
871
+ // Start timeout for action execution
872
+ const timeoutId = setTimeout(() => {
873
+ reject(new Error("TIMEOUT"));
874
+ }, timeoutMs);
875
+
876
+ try {
877
+ // Execute the action command DIRECTLY (no shell wrapper!)
878
+ // This is the key security difference from runContainer
879
+ container = container.withExec(commandArray, { expect: ReturnType.Any });
880
+
881
+ const finalContainer = await container.sync();
882
+ const [stdout, stderr, exitCode] = await Promise.all([
883
+ finalContainer.stdout(),
884
+ finalContainer.stderr(),
885
+ finalContainer.exitCode(),
886
+ ]);
887
+
888
+ clearTimeout(timeoutId);
889
+
890
+ const output: ExecutionOutput = {
891
+ stdout,
892
+ stderr,
893
+ exitCode,
894
+ };
895
+
896
+ // Export /output directory if requested
897
+ if (options.outputPath) {
898
+ try {
899
+ await finalContainer.directory("/output").export(options.outputPath);
900
+ } catch {
901
+ // /output may not exist
902
+ }
903
+ }
904
+
905
+ // Extract output files if requested
906
+ if (options.outputFiles && options.outputFiles.length > 0) {
907
+ const extractedFiles: Record<string, Buffer> = {};
908
+ for (const filePath of options.outputFiles) {
909
+ try {
910
+ const content = await finalContainer.file(filePath).contents();
911
+ extractedFiles[filePath] = Buffer.from(content);
912
+ } catch {
913
+ // File doesn't exist - skip
914
+ }
915
+ }
916
+ if (Object.keys(extractedFiles).length > 0) {
917
+ output.files = extractedFiles;
918
+ }
919
+ }
920
+
921
+ resolve(output);
922
+ } catch (error) {
923
+ clearTimeout(timeoutId);
924
+ throw error;
925
+ }
926
+ } catch (error) {
927
+ reject(error);
928
+ }
929
+ },
930
+ this.config.verbose ? { LogOutput: process.stderr } : {}
931
+ ).catch((error) => {
932
+ reject(error);
933
+ });
934
+ });
935
+ }
936
+
556
937
  /**
557
938
  * Generate a unique execution ID
558
939
  */
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Remote Execution Provider
3
+ *
4
+ * Delegates tool execution to a remote endpoint.
5
+ * Useful when no local container runtime is available.
6
+ */
7
+
8
+ import type { Action, ActionsManifest, ToolManifest } from "@enactprotocol/shared";
9
+ import { applyDefaults, getEffectiveInputSchema, validateInputs } from "@enactprotocol/shared";
10
+ import type {
11
+ EngineHealth,
12
+ ExecutionErrorCode,
13
+ ExecutionInput,
14
+ ExecutionOptions,
15
+ ExecutionProvider,
16
+ ExecutionResult,
17
+ } from "@enactprotocol/shared";
18
+
19
+ /**
20
+ * Configuration for the remote execution provider
21
+ */
22
+ export interface RemoteProviderConfig {
23
+ /** Remote execution endpoint (e.g., "https://run.enact.tools" or "http://localhost:3000") */
24
+ endpoint: string;
25
+ /** Authentication token */
26
+ authToken?: string;
27
+ /** Default timeout in milliseconds */
28
+ defaultTimeout?: number;
29
+ }
30
+
31
+ /**
32
+ * Provider that delegates execution to a remote server.
33
+ */
34
+ export class RemoteExecutionProvider implements ExecutionProvider {
35
+ readonly name = "remote";
36
+ private endpoint: string;
37
+ private authToken: string | undefined;
38
+ private defaultTimeout: number;
39
+
40
+ constructor(config: RemoteProviderConfig) {
41
+ this.endpoint = config.endpoint.replace(/\/$/, "");
42
+ this.authToken = config.authToken;
43
+ this.defaultTimeout = config.defaultTimeout ?? 300000;
44
+ }
45
+
46
+ async initialize(): Promise<void> {
47
+ // No initialization needed
48
+ }
49
+
50
+ async isAvailable(): Promise<boolean> {
51
+ try {
52
+ const controller = new AbortController();
53
+ const timeout = setTimeout(() => controller.abort(), 5000);
54
+ const res = await fetch(`${this.endpoint}/health`, {
55
+ signal: controller.signal,
56
+ });
57
+ clearTimeout(timeout);
58
+ return res.ok;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ async getHealth(): Promise<EngineHealth> {
65
+ const available = await this.isAvailable();
66
+ return {
67
+ healthy: available,
68
+ runtime: "docker",
69
+ consecutiveFailures: available ? 0 : 1,
70
+ };
71
+ }
72
+
73
+ async shutdown(): Promise<void> {
74
+ // Nothing to clean up
75
+ }
76
+
77
+ async execute(
78
+ manifest: ToolManifest,
79
+ input: ExecutionInput,
80
+ options: ExecutionOptions = {}
81
+ ): Promise<ExecutionResult> {
82
+ return this.remoteExecute({
83
+ type: "execute",
84
+ manifest,
85
+ input,
86
+ options,
87
+ });
88
+ }
89
+
90
+ async exec(
91
+ manifest: ToolManifest,
92
+ command: string,
93
+ options: ExecutionOptions = {}
94
+ ): Promise<ExecutionResult> {
95
+ return this.remoteExecute({
96
+ type: "exec",
97
+ manifest,
98
+ command,
99
+ input: { params: {} },
100
+ options,
101
+ });
102
+ }
103
+
104
+ async executeAction(
105
+ manifest: ToolManifest,
106
+ actionsManifest: ActionsManifest,
107
+ actionName: string,
108
+ action: Action,
109
+ input: ExecutionInput,
110
+ options: ExecutionOptions = {}
111
+ ): Promise<ExecutionResult> {
112
+ const startTime = new Date();
113
+ const executionId = `remote-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
114
+
115
+ // Validate inputs locally before sending to remote
116
+ const effectiveSchema = getEffectiveInputSchema(action);
117
+ const validation = validateInputs(input.params, effectiveSchema);
118
+ if (!validation.valid) {
119
+ const errorMessages = validation.errors.map((e) => `${e.path}: ${e.message}`);
120
+ return this.createErrorResult(
121
+ `${manifest.name}:${actionName}`,
122
+ executionId,
123
+ startTime,
124
+ "VALIDATION_ERROR",
125
+ `Input validation failed: ${errorMessages.join(", ")}`
126
+ );
127
+ }
128
+
129
+ const params = applyDefaults(input.params, effectiveSchema);
130
+
131
+ return this.remoteExecute({
132
+ type: "executeAction",
133
+ manifest,
134
+ actionsManifest,
135
+ actionName,
136
+ action,
137
+ input: { ...input, params },
138
+ options,
139
+ });
140
+ }
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // Private helpers
144
+ // ---------------------------------------------------------------------------
145
+
146
+ private async remoteExecute(payload: Record<string, unknown>): Promise<ExecutionResult> {
147
+ const startTime = new Date();
148
+ const executionId = `remote-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
149
+ const toolName = (payload.manifest as ToolManifest)?.name ?? "unknown";
150
+
151
+ try {
152
+ const headers: Record<string, string> = {
153
+ "Content-Type": "application/json",
154
+ };
155
+ if (this.authToken) {
156
+ headers.Authorization = `Bearer ${this.authToken}`;
157
+ }
158
+
159
+ const controller = new AbortController();
160
+ const timeout = setTimeout(() => controller.abort(), this.defaultTimeout);
161
+
162
+ const response = await fetch(`${this.endpoint}/execute`, {
163
+ method: "POST",
164
+ headers,
165
+ body: JSON.stringify(payload),
166
+ signal: controller.signal,
167
+ });
168
+
169
+ clearTimeout(timeout);
170
+
171
+ if (!response.ok) {
172
+ const errorBody = await response.text();
173
+ return this.createErrorResult(
174
+ toolName,
175
+ executionId,
176
+ startTime,
177
+ "COMMAND_ERROR",
178
+ `Remote execution failed (${response.status}): ${errorBody}`
179
+ );
180
+ }
181
+
182
+ const result = (await response.json()) as ExecutionResult;
183
+
184
+ // Ensure metadata has correct timing from our perspective
185
+ const endTime = new Date();
186
+ if (result.metadata) {
187
+ result.metadata.startTime = startTime;
188
+ result.metadata.endTime = endTime;
189
+ result.metadata.durationMs = endTime.getTime() - startTime.getTime();
190
+ }
191
+
192
+ return result;
193
+ } catch (error) {
194
+ const message = error instanceof Error ? error.message : String(error);
195
+ const code: ExecutionErrorCode = message.includes("abort") ? "TIMEOUT" : "COMMAND_ERROR";
196
+
197
+ return this.createErrorResult(toolName, executionId, startTime, code, message);
198
+ }
199
+ }
200
+
201
+ private createErrorResult(
202
+ toolName: string,
203
+ executionId: string,
204
+ startTime: Date,
205
+ code: ExecutionErrorCode,
206
+ message: string
207
+ ): ExecutionResult {
208
+ const endTime = new Date();
209
+ return {
210
+ success: false,
211
+ output: { stdout: "", stderr: message, exitCode: 1 },
212
+ metadata: {
213
+ toolName,
214
+ containerImage: "remote",
215
+ startTime,
216
+ endTime,
217
+ durationMs: endTime.getTime() - startTime.getTime(),
218
+ cached: false,
219
+ executionId,
220
+ },
221
+ error: { code, message },
222
+ };
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Create a remote execution provider
228
+ */
229
+ export function createRemoteProvider(config: RemoteProviderConfig): RemoteExecutionProvider {
230
+ return new RemoteExecutionProvider(config);
231
+ }