keystone-cli 0.8.0 → 1.0.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/README.md +486 -54
- package/package.json +8 -2
- package/src/__fixtures__/index.ts +100 -0
- package/src/cli.ts +809 -90
- package/src/db/memory-db.ts +35 -1
- package/src/db/workflow-db.test.ts +24 -0
- package/src/db/workflow-db.ts +469 -14
- package/src/expression/evaluator.ts +68 -4
- package/src/parser/agent-parser.ts +6 -3
- package/src/parser/config-schema.ts +38 -2
- package/src/parser/schema.ts +192 -7
- package/src/parser/test-schema.ts +29 -0
- package/src/parser/workflow-parser.test.ts +54 -0
- package/src/parser/workflow-parser.ts +153 -7
- package/src/runner/aggregate-error.test.ts +57 -0
- package/src/runner/aggregate-error.ts +46 -0
- package/src/runner/audit-verification.test.ts +2 -2
- package/src/runner/auto-heal.test.ts +1 -1
- package/src/runner/blueprint-executor.test.ts +63 -0
- package/src/runner/blueprint-executor.ts +157 -0
- package/src/runner/concurrency-limit.test.ts +82 -0
- package/src/runner/debug-repl.ts +18 -3
- package/src/runner/durable-timers.test.ts +200 -0
- package/src/runner/engine-executor.test.ts +464 -0
- package/src/runner/engine-executor.ts +489 -0
- package/src/runner/foreach-executor.ts +30 -12
- package/src/runner/llm-adapter.test.ts +282 -5
- package/src/runner/llm-adapter.ts +581 -8
- package/src/runner/llm-clarification.test.ts +79 -21
- package/src/runner/llm-errors.ts +83 -0
- package/src/runner/llm-executor.test.ts +258 -219
- package/src/runner/llm-executor.ts +226 -29
- package/src/runner/mcp-client.ts +70 -3
- package/src/runner/mcp-manager.test.ts +52 -52
- package/src/runner/mcp-manager.ts +12 -5
- package/src/runner/mcp-server.test.ts +117 -78
- package/src/runner/mcp-server.ts +13 -4
- package/src/runner/optimization-runner.ts +48 -31
- package/src/runner/reflexion.test.ts +1 -1
- package/src/runner/resource-pool.test.ts +113 -0
- package/src/runner/resource-pool.ts +164 -0
- package/src/runner/shell-executor.ts +130 -32
- package/src/runner/standard-tools-integration.test.ts +36 -36
- package/src/runner/standard-tools.test.ts +18 -0
- package/src/runner/standard-tools.ts +110 -37
- package/src/runner/step-executor.test.ts +176 -16
- package/src/runner/step-executor.ts +530 -86
- package/src/runner/stream-utils.test.ts +14 -0
- package/src/runner/subflow-outputs.test.ts +103 -0
- package/src/runner/test-harness.ts +161 -0
- package/src/runner/tool-integration.test.ts +73 -79
- package/src/runner/workflow-runner.test.ts +492 -15
- package/src/runner/workflow-runner.ts +1438 -79
- package/src/runner/workflow-subflows.test.ts +255 -0
- package/src/templates/agents/keystone-architect.md +19 -14
- package/src/templates/agents/tester.md +21 -0
- package/src/templates/batch-processor.yaml +1 -1
- package/src/templates/child-rollback.yaml +11 -0
- package/src/templates/decompose-implement.yaml +53 -0
- package/src/templates/decompose-problem.yaml +159 -0
- package/src/templates/decompose-research.yaml +52 -0
- package/src/templates/decompose-review.yaml +51 -0
- package/src/templates/dev.yaml +134 -0
- package/src/templates/engine-example.yaml +33 -0
- package/src/templates/fan-out-fan-in.yaml +61 -0
- package/src/templates/loop-parallel.yaml +1 -1
- package/src/templates/memory-service.yaml +1 -1
- package/src/templates/parent-rollback.yaml +16 -0
- package/src/templates/robust-automation.yaml +1 -1
- package/src/templates/scaffold-feature.yaml +29 -27
- package/src/templates/scaffold-generate.yaml +41 -0
- package/src/templates/scaffold-plan.yaml +53 -0
- package/src/types/status.ts +3 -0
- package/src/ui/dashboard.tsx +4 -3
- package/src/utils/assets.macro.ts +36 -0
- package/src/utils/auth-manager.ts +585 -8
- package/src/utils/blueprint-utils.test.ts +49 -0
- package/src/utils/blueprint-utils.ts +80 -0
- package/src/utils/circuit-breaker.test.ts +177 -0
- package/src/utils/circuit-breaker.ts +160 -0
- package/src/utils/config-loader.test.ts +100 -13
- package/src/utils/config-loader.ts +44 -17
- package/src/utils/constants.ts +62 -0
- package/src/utils/error-renderer.test.ts +267 -0
- package/src/utils/error-renderer.ts +320 -0
- package/src/utils/json-parser.test.ts +4 -0
- package/src/utils/json-parser.ts +18 -1
- package/src/utils/mermaid.ts +4 -0
- package/src/utils/paths.test.ts +46 -0
- package/src/utils/paths.ts +70 -0
- package/src/utils/process-sandbox.test.ts +128 -0
- package/src/utils/process-sandbox.ts +293 -0
- package/src/utils/rate-limiter.test.ts +143 -0
- package/src/utils/rate-limiter.ts +221 -0
- package/src/utils/redactor.test.ts +23 -15
- package/src/utils/redactor.ts +65 -25
- package/src/utils/resource-loader.test.ts +54 -0
- package/src/utils/resource-loader.ts +158 -0
- package/src/utils/sandbox.test.ts +69 -4
- package/src/utils/sandbox.ts +69 -6
- package/src/utils/schema-validator.ts +65 -0
- package/src/utils/workflow-registry.test.ts +57 -0
- package/src/utils/workflow-registry.ts +45 -25
- /package/src/expression/{evaluator.audit.test.ts → evaluator-audit.test.ts} +0 -0
- /package/src/runner/{mcp-client.audit.test.ts → mcp-client-audit.test.ts} +0 -0
|
@@ -27,7 +27,11 @@ jsep.plugins.register(jsepObject);
|
|
|
27
27
|
export interface ExpressionContext {
|
|
28
28
|
inputs?: Record<string, unknown>;
|
|
29
29
|
secrets?: Record<string, string>;
|
|
30
|
-
|
|
30
|
+
secretValues?: string[];
|
|
31
|
+
steps?: Record<
|
|
32
|
+
string,
|
|
33
|
+
{ output?: unknown; outputs?: Record<string, unknown>; status?: string; error?: string }
|
|
34
|
+
>;
|
|
31
35
|
item?: unknown;
|
|
32
36
|
args?: unknown;
|
|
33
37
|
index?: number;
|
|
@@ -35,6 +39,8 @@ export interface ExpressionContext {
|
|
|
35
39
|
output?: unknown;
|
|
36
40
|
autoHealAttempts?: number;
|
|
37
41
|
reflexionAttempts?: number;
|
|
42
|
+
outputRepairAttempts?: number;
|
|
43
|
+
last_failed_step?: { id: string; error: string };
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
type ASTNode = jsep.Expression;
|
|
@@ -76,6 +82,14 @@ export class ExpressionEvaluator {
|
|
|
76
82
|
private static readonly MAX_TEMPLATE_LENGTH = 10_000;
|
|
77
83
|
// Maximum length for plain strings without expressions (1MB)
|
|
78
84
|
private static readonly MAX_PLAIN_STRING_LENGTH = 1_000_000;
|
|
85
|
+
// Maximum nesting depth for expressions (prevents stack overflow)
|
|
86
|
+
private static readonly MAX_NESTING_DEPTH = 50;
|
|
87
|
+
// Maximum array literal size (prevents memory exhaustion)
|
|
88
|
+
private static readonly MAX_ARRAY_SIZE = 1000;
|
|
89
|
+
// Maximum total nodes to evaluate (prevents DoS via complex expressions)
|
|
90
|
+
private static readonly MAX_TOTAL_NODES = 10000;
|
|
91
|
+
// Maximum arrow function nesting depth
|
|
92
|
+
private static readonly MAX_ARROW_DEPTH = 3;
|
|
79
93
|
|
|
80
94
|
/**
|
|
81
95
|
* Helper to scan string for matches of ${{ ... }} handling nested braces manually
|
|
@@ -237,7 +251,9 @@ export class ExpressionEvaluator {
|
|
|
237
251
|
static evaluateExpression(expr: string, context: ExpressionContext): unknown {
|
|
238
252
|
try {
|
|
239
253
|
const ast = jsep(expr);
|
|
240
|
-
|
|
254
|
+
// Track total nodes evaluated to prevent DoS
|
|
255
|
+
const nodeCounter = { count: 0 };
|
|
256
|
+
return ExpressionEvaluator.evaluateNode(ast, context, 0, nodeCounter);
|
|
241
257
|
} catch (error) {
|
|
242
258
|
throw new Error(
|
|
243
259
|
`Failed to evaluate expression "${expr}": ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -248,7 +264,25 @@ export class ExpressionEvaluator {
|
|
|
248
264
|
/**
|
|
249
265
|
* Evaluate an AST node recursively
|
|
250
266
|
*/
|
|
251
|
-
private static evaluateNode(
|
|
267
|
+
private static evaluateNode(
|
|
268
|
+
node: ASTNode,
|
|
269
|
+
context: ExpressionContext,
|
|
270
|
+
depth = 0,
|
|
271
|
+
nodeCounter: { count: number } = { count: 0 },
|
|
272
|
+
arrowDepth = 0
|
|
273
|
+
): unknown {
|
|
274
|
+
// Increment node counter for DoS protection
|
|
275
|
+
nodeCounter.count++;
|
|
276
|
+
if (nodeCounter.count > ExpressionEvaluator.MAX_TOTAL_NODES) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
`Expression exceeds maximum complexity of ${ExpressionEvaluator.MAX_TOTAL_NODES} nodes`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
if (depth > ExpressionEvaluator.MAX_NESTING_DEPTH) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
`Expression nesting exceeds maximum depth of ${ExpressionEvaluator.MAX_NESTING_DEPTH}`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
252
286
|
switch (node.type) {
|
|
253
287
|
case 'Literal':
|
|
254
288
|
return (node as jsep.Literal).value;
|
|
@@ -256,6 +290,30 @@ export class ExpressionEvaluator {
|
|
|
256
290
|
case 'Identifier': {
|
|
257
291
|
const name = (node as jsep.Identifier).name;
|
|
258
292
|
|
|
293
|
+
// Security: Block dangerous global identifiers that could enable code execution
|
|
294
|
+
const FORBIDDEN_IDENTIFIERS = new Set([
|
|
295
|
+
'eval',
|
|
296
|
+
'Function',
|
|
297
|
+
'AsyncFunction',
|
|
298
|
+
'GeneratorFunction',
|
|
299
|
+
'globalThis',
|
|
300
|
+
'global',
|
|
301
|
+
'self',
|
|
302
|
+
'window',
|
|
303
|
+
'top',
|
|
304
|
+
'parent',
|
|
305
|
+
'frames',
|
|
306
|
+
'Reflect',
|
|
307
|
+
'Proxy',
|
|
308
|
+
'require',
|
|
309
|
+
'import',
|
|
310
|
+
'module',
|
|
311
|
+
'exports',
|
|
312
|
+
]);
|
|
313
|
+
if (FORBIDDEN_IDENTIFIERS.has(name)) {
|
|
314
|
+
throw new Error(`Access to "${name}" is forbidden for security reasons`);
|
|
315
|
+
}
|
|
316
|
+
|
|
259
317
|
// Safe global functions and values
|
|
260
318
|
const safeGlobals: Record<string, unknown> = {
|
|
261
319
|
Boolean: Boolean,
|
|
@@ -300,6 +358,7 @@ export class ExpressionEvaluator {
|
|
|
300
358
|
index: context.index,
|
|
301
359
|
env: context.env || {},
|
|
302
360
|
stdout: contextAsRecord.stdout, // For transform expressions
|
|
361
|
+
last_failed_step: context.last_failed_step,
|
|
303
362
|
};
|
|
304
363
|
|
|
305
364
|
if (name in rootContext && rootContext[name] !== undefined) {
|
|
@@ -441,8 +500,13 @@ export class ExpressionEvaluator {
|
|
|
441
500
|
|
|
442
501
|
case 'ArrayExpression': {
|
|
443
502
|
const arrayNode = node as jsep.ArrayExpression;
|
|
503
|
+
if (arrayNode.elements.length > ExpressionEvaluator.MAX_ARRAY_SIZE) {
|
|
504
|
+
throw new Error(
|
|
505
|
+
`Array literal exceeds maximum size of ${ExpressionEvaluator.MAX_ARRAY_SIZE} elements`
|
|
506
|
+
);
|
|
507
|
+
}
|
|
444
508
|
return arrayNode.elements.map((elem) =>
|
|
445
|
-
elem ? ExpressionEvaluator.evaluateNode(elem, context) : null
|
|
509
|
+
elem ? ExpressionEvaluator.evaluateNode(elem, context, depth + 1) : null
|
|
446
510
|
);
|
|
447
511
|
}
|
|
448
512
|
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
1
|
import { homedir } from 'node:os';
|
|
3
2
|
import { join } from 'node:path';
|
|
4
3
|
import yaml from 'js-yaml';
|
|
4
|
+
import { ResourceLoader } from '../utils/resource-loader';
|
|
5
5
|
import { type Agent, AgentSchema } from './schema';
|
|
6
6
|
|
|
7
7
|
export function parseAgent(filePath: string): Agent {
|
|
8
|
-
const content =
|
|
8
|
+
const content = ResourceLoader.readFile(filePath);
|
|
9
|
+
if (content === null) {
|
|
10
|
+
throw new Error(`Agent file not found at ${filePath}`);
|
|
11
|
+
}
|
|
9
12
|
// Flexible regex to handle both standard and single-line frontmatter
|
|
10
13
|
const match = content.match(/^---[\r\n]*([\s\S]*?)[\r\n]*---(?:\r?\n?([\s\S]*))?$/);
|
|
11
14
|
|
|
@@ -58,7 +61,7 @@ export function resolveAgentPath(agentName: string, baseDir?: string): string {
|
|
|
58
61
|
);
|
|
59
62
|
|
|
60
63
|
for (const path of possiblePaths) {
|
|
61
|
-
if (
|
|
64
|
+
if (ResourceLoader.exists(path)) {
|
|
62
65
|
return path;
|
|
63
66
|
}
|
|
64
67
|
}
|
|
@@ -6,10 +6,20 @@ export const ConfigSchema = z.object({
|
|
|
6
6
|
providers: z
|
|
7
7
|
.record(
|
|
8
8
|
z.object({
|
|
9
|
-
type: z
|
|
9
|
+
type: z
|
|
10
|
+
.enum([
|
|
11
|
+
'openai',
|
|
12
|
+
'anthropic',
|
|
13
|
+
'anthropic-claude',
|
|
14
|
+
'copilot',
|
|
15
|
+
'openai-chatgpt',
|
|
16
|
+
'google-gemini',
|
|
17
|
+
])
|
|
18
|
+
.default('openai'),
|
|
10
19
|
base_url: z.string().optional(),
|
|
11
20
|
api_key_env: z.string().optional(),
|
|
12
21
|
default_model: z.string().optional(),
|
|
22
|
+
project_id: z.string().optional(),
|
|
13
23
|
})
|
|
14
24
|
)
|
|
15
25
|
.default({
|
|
@@ -37,9 +47,9 @@ export const ConfigSchema = z.object({
|
|
|
37
47
|
storage: z
|
|
38
48
|
.object({
|
|
39
49
|
retention_days: z.number().default(30),
|
|
50
|
+
redact_secrets_at_rest: z.boolean().default(true),
|
|
40
51
|
})
|
|
41
52
|
.default({}),
|
|
42
|
-
workflows_directory: z.string().default('workflows'),
|
|
43
53
|
mcp_servers: z
|
|
44
54
|
.record(
|
|
45
55
|
z.union([
|
|
@@ -64,6 +74,32 @@ export const ConfigSchema = z.object({
|
|
|
64
74
|
])
|
|
65
75
|
)
|
|
66
76
|
.default({}),
|
|
77
|
+
engines: z
|
|
78
|
+
.object({
|
|
79
|
+
allowlist: z
|
|
80
|
+
.record(
|
|
81
|
+
z.object({
|
|
82
|
+
command: z.string(),
|
|
83
|
+
args: z.array(z.string()).optional(),
|
|
84
|
+
version: z.string(),
|
|
85
|
+
versionArgs: z.array(z.string()).optional().default(['--version']),
|
|
86
|
+
})
|
|
87
|
+
)
|
|
88
|
+
.default({}),
|
|
89
|
+
denylist: z.array(z.string()).default([]),
|
|
90
|
+
})
|
|
91
|
+
.default({}),
|
|
92
|
+
concurrency: z
|
|
93
|
+
.object({
|
|
94
|
+
default: z.number().int().positive().default(10),
|
|
95
|
+
pools: z.record(z.number().int().positive()).default({
|
|
96
|
+
llm: 2,
|
|
97
|
+
shell: 5,
|
|
98
|
+
http: 10,
|
|
99
|
+
engine: 2,
|
|
100
|
+
}),
|
|
101
|
+
})
|
|
102
|
+
.default({}),
|
|
67
103
|
});
|
|
68
104
|
|
|
69
105
|
export type Config = z.infer<typeof ConfigSchema>;
|
package/src/parser/schema.ts
CHANGED
|
@@ -2,11 +2,84 @@ import { z } from 'zod';
|
|
|
2
2
|
|
|
3
3
|
// ===== Input/Output Schema =====
|
|
4
4
|
|
|
5
|
-
const InputSchema = z
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
const InputSchema = z
|
|
6
|
+
.object({
|
|
7
|
+
type: z.enum(['string', 'number', 'boolean', 'array', 'object']),
|
|
8
|
+
default: z.any().optional(),
|
|
9
|
+
values: z.array(z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
10
|
+
secret: z.boolean().optional(),
|
|
11
|
+
description: z.string().optional(),
|
|
12
|
+
})
|
|
13
|
+
.superRefine((value, ctx) => {
|
|
14
|
+
const type = value.type;
|
|
15
|
+
const defaultValue = value.default;
|
|
16
|
+
|
|
17
|
+
if (defaultValue !== undefined) {
|
|
18
|
+
if (type === 'string' && typeof defaultValue !== 'string') {
|
|
19
|
+
ctx.addIssue({
|
|
20
|
+
code: z.ZodIssueCode.custom,
|
|
21
|
+
message: `default must be a string for type "${type}"`,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
if (type === 'number' && typeof defaultValue !== 'number') {
|
|
25
|
+
ctx.addIssue({
|
|
26
|
+
code: z.ZodIssueCode.custom,
|
|
27
|
+
message: `default must be a number for type "${type}"`,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (type === 'boolean' && typeof defaultValue !== 'boolean') {
|
|
31
|
+
ctx.addIssue({
|
|
32
|
+
code: z.ZodIssueCode.custom,
|
|
33
|
+
message: `default must be a boolean for type "${type}"`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (type === 'array' && !Array.isArray(defaultValue)) {
|
|
37
|
+
ctx.addIssue({
|
|
38
|
+
code: z.ZodIssueCode.custom,
|
|
39
|
+
message: `default must be an array for type "${type}"`,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (
|
|
43
|
+
type === 'object' &&
|
|
44
|
+
(typeof defaultValue !== 'object' || defaultValue === null || Array.isArray(defaultValue))
|
|
45
|
+
) {
|
|
46
|
+
ctx.addIssue({
|
|
47
|
+
code: z.ZodIssueCode.custom,
|
|
48
|
+
message: `default must be an object for type "${type}"`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (value.values) {
|
|
54
|
+
if (type !== 'string' && type !== 'number' && type !== 'boolean') {
|
|
55
|
+
ctx.addIssue({
|
|
56
|
+
code: z.ZodIssueCode.custom,
|
|
57
|
+
message: `values cannot be used with type "${type}"`,
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const allowed of value.values) {
|
|
63
|
+
const matchesType =
|
|
64
|
+
(type === 'string' && typeof allowed === 'string') ||
|
|
65
|
+
(type === 'number' && typeof allowed === 'number') ||
|
|
66
|
+
(type === 'boolean' && typeof allowed === 'boolean');
|
|
67
|
+
if (!matchesType) {
|
|
68
|
+
ctx.addIssue({
|
|
69
|
+
code: z.ZodIssueCode.custom,
|
|
70
|
+
message: `enum value ${JSON.stringify(allowed)} must be a ${type}`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (defaultValue !== undefined && !value.values.includes(defaultValue as never)) {
|
|
76
|
+
ctx.addIssue({
|
|
77
|
+
code: z.ZodIssueCode.custom,
|
|
78
|
+
message: `default must be one of: ${value.values.map((v) => JSON.stringify(v)).join(', ')}`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
10
83
|
|
|
11
84
|
// ===== Retry Schema =====
|
|
12
85
|
|
|
@@ -42,11 +115,21 @@ const BaseStepSchema = z.object({
|
|
|
42
115
|
retry: RetrySchema.optional(),
|
|
43
116
|
auto_heal: AutoHealSchema.optional(),
|
|
44
117
|
reflexion: ReflexionSchema.optional(),
|
|
118
|
+
allowFailure: z.boolean().optional(),
|
|
119
|
+
idempotencyKey: z.string().optional(), // Expression for dedup key (evaluated at runtime)
|
|
120
|
+
idempotencyScope: z.enum(['run', 'global']).optional(), // Default: run
|
|
121
|
+
idempotencyTtlSeconds: z.number().int().positive().optional(),
|
|
45
122
|
foreach: z.string().optional(),
|
|
46
123
|
// Accept both number and string (for expressions or YAML number-as-string)
|
|
47
124
|
concurrency: z.union([z.number().int().positive(), z.string()]).optional(),
|
|
125
|
+
pool: z.string().optional(), // Resource pool to use for this step
|
|
48
126
|
transform: z.string().optional(),
|
|
49
127
|
learn: z.boolean().optional(),
|
|
128
|
+
inputSchema: z.any().optional(),
|
|
129
|
+
outputSchema: z.any().optional(),
|
|
130
|
+
outputRetries: z.number().int().min(0).optional(), // Max retries for output validation failures
|
|
131
|
+
repairStrategy: z.enum(['reask', 'repair', 'hybrid']).optional(), // Strategy for output repair
|
|
132
|
+
compensate: z.lazy(() => StepSchema).optional(), // Compensation step to run on rollback
|
|
50
133
|
});
|
|
51
134
|
|
|
52
135
|
// ===== Step Type Schemas =====
|
|
@@ -67,15 +150,41 @@ const AgentToolSchema = z.object({
|
|
|
67
150
|
execution: z.lazy(() => StepSchema), // Tools are essentially steps
|
|
68
151
|
});
|
|
69
152
|
|
|
153
|
+
const JoinStepSchema = BaseStepSchema.extend({
|
|
154
|
+
type: z.literal('join'),
|
|
155
|
+
target: z.enum(['steps', 'branches']).optional().default('steps'),
|
|
156
|
+
condition: z
|
|
157
|
+
.union([z.literal('all'), z.literal('any'), z.number().int().positive()])
|
|
158
|
+
.default('all'),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const EngineConfigSchema = z.object({
|
|
162
|
+
command: z.string(),
|
|
163
|
+
args: z.array(z.string()).optional(),
|
|
164
|
+
input: z.any().optional(),
|
|
165
|
+
env: z.record(z.string()),
|
|
166
|
+
cwd: z.string(),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const EngineHandoffSchema = z.object({
|
|
170
|
+
name: z.string().optional(),
|
|
171
|
+
description: z.string().optional(),
|
|
172
|
+
inputSchema: z.any().optional(),
|
|
173
|
+
engine: EngineConfigSchema.extend({
|
|
174
|
+
timeout: z.number().int().positive().optional(),
|
|
175
|
+
outputSchema: z.any().optional(),
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
178
|
+
|
|
70
179
|
const LlmStepSchema = BaseStepSchema.extend({
|
|
71
180
|
type: z.literal('llm'),
|
|
72
181
|
agent: z.string(),
|
|
73
182
|
provider: z.string().optional(),
|
|
74
183
|
model: z.string().optional(),
|
|
75
184
|
prompt: z.string(),
|
|
76
|
-
schema: z.any().optional(),
|
|
77
185
|
tools: z.array(AgentToolSchema).optional(),
|
|
78
186
|
maxIterations: z.number().int().positive().default(10),
|
|
187
|
+
maxMessageHistory: z.number().int().positive().optional(), // Max messages to keep in conversation history
|
|
79
188
|
useGlobalMcp: z.boolean().optional(),
|
|
80
189
|
allowClarification: z.boolean().optional(),
|
|
81
190
|
mcpServers: z
|
|
@@ -98,12 +207,22 @@ const LlmStepSchema = BaseStepSchema.extend({
|
|
|
98
207
|
useStandardTools: z.boolean().optional(),
|
|
99
208
|
allowOutsideCwd: z.boolean().optional(),
|
|
100
209
|
allowInsecure: z.boolean().optional(),
|
|
210
|
+
handoff: EngineHandoffSchema.optional(),
|
|
101
211
|
});
|
|
102
212
|
|
|
213
|
+
const OutputMappingItemSchema = z.union([
|
|
214
|
+
z.string(), // Rename: alias -> originalKey
|
|
215
|
+
z.object({
|
|
216
|
+
from: z.string(),
|
|
217
|
+
default: z.any().optional(),
|
|
218
|
+
}),
|
|
219
|
+
]);
|
|
220
|
+
|
|
103
221
|
const WorkflowStepSchema = BaseStepSchema.extend({
|
|
104
222
|
type: z.literal('workflow'),
|
|
105
223
|
path: z.string(),
|
|
106
224
|
inputs: z.record(z.string()).optional(),
|
|
225
|
+
outputMapping: z.record(OutputMappingItemSchema).optional(),
|
|
107
226
|
});
|
|
108
227
|
|
|
109
228
|
const FileStepSchema = BaseStepSchema.extend({
|
|
@@ -117,9 +236,10 @@ const FileStepSchema = BaseStepSchema.extend({
|
|
|
117
236
|
const RequestStepSchema = BaseStepSchema.extend({
|
|
118
237
|
type: z.literal('request'),
|
|
119
238
|
url: z.string(),
|
|
120
|
-
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).default('GET'),
|
|
239
|
+
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD']).default('GET'),
|
|
121
240
|
body: z.any().optional(),
|
|
122
241
|
headers: z.record(z.string()).optional(),
|
|
242
|
+
allowInsecure: z.boolean().optional(),
|
|
123
243
|
});
|
|
124
244
|
|
|
125
245
|
const HumanStepSchema = BaseStepSchema.extend({
|
|
@@ -131,6 +251,7 @@ const HumanStepSchema = BaseStepSchema.extend({
|
|
|
131
251
|
const SleepStepSchema = BaseStepSchema.extend({
|
|
132
252
|
type: z.literal('sleep'),
|
|
133
253
|
duration: z.union([z.number().int().positive(), z.string()]),
|
|
254
|
+
durable: z.boolean().optional(), // Persist across restarts for long sleeps
|
|
134
255
|
});
|
|
135
256
|
|
|
136
257
|
const ScriptStepSchema = BaseStepSchema.extend({
|
|
@@ -139,6 +260,57 @@ const ScriptStepSchema = BaseStepSchema.extend({
|
|
|
139
260
|
allowInsecure: z.boolean().optional().default(false),
|
|
140
261
|
});
|
|
141
262
|
|
|
263
|
+
const EngineStepSchema = BaseStepSchema.extend({
|
|
264
|
+
type: z.literal('engine'),
|
|
265
|
+
}).merge(EngineConfigSchema);
|
|
266
|
+
|
|
267
|
+
const BlueprintSchema = z.object({
|
|
268
|
+
architecture: z.object({
|
|
269
|
+
description: z.string(),
|
|
270
|
+
patterns: z.array(z.string()).optional(),
|
|
271
|
+
}),
|
|
272
|
+
apis: z
|
|
273
|
+
.array(
|
|
274
|
+
z.object({
|
|
275
|
+
name: z.string(),
|
|
276
|
+
description: z.string(),
|
|
277
|
+
endpoints: z
|
|
278
|
+
.array(
|
|
279
|
+
z.object({
|
|
280
|
+
path: z.string(),
|
|
281
|
+
method: z.string(),
|
|
282
|
+
purpose: z.string(),
|
|
283
|
+
})
|
|
284
|
+
)
|
|
285
|
+
.optional(),
|
|
286
|
+
})
|
|
287
|
+
)
|
|
288
|
+
.optional(),
|
|
289
|
+
files: z.array(
|
|
290
|
+
z.object({
|
|
291
|
+
path: z.string(),
|
|
292
|
+
purpose: z.string(),
|
|
293
|
+
constraints: z.array(z.string()).optional(),
|
|
294
|
+
})
|
|
295
|
+
),
|
|
296
|
+
dependencies: z
|
|
297
|
+
.array(
|
|
298
|
+
z.object({
|
|
299
|
+
name: z.string(),
|
|
300
|
+
version: z.string().optional(),
|
|
301
|
+
purpose: z.string(),
|
|
302
|
+
})
|
|
303
|
+
)
|
|
304
|
+
.optional(),
|
|
305
|
+
constraints: z.array(z.string()).optional(),
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const BlueprintStepSchema = BaseStepSchema.extend({
|
|
309
|
+
type: z.literal('blueprint'),
|
|
310
|
+
prompt: z.string(),
|
|
311
|
+
agent: z.string().optional().default('keystone-architect'),
|
|
312
|
+
});
|
|
313
|
+
|
|
142
314
|
const MemoryStepSchema = BaseStepSchema.extend({
|
|
143
315
|
type: z.literal('memory'),
|
|
144
316
|
op: z.enum(['search', 'store']),
|
|
@@ -162,7 +334,10 @@ export const StepSchema: z.ZodType<any> = z.lazy(() =>
|
|
|
162
334
|
HumanStepSchema,
|
|
163
335
|
SleepStepSchema,
|
|
164
336
|
ScriptStepSchema,
|
|
337
|
+
EngineStepSchema,
|
|
165
338
|
MemoryStepSchema,
|
|
339
|
+
JoinStepSchema,
|
|
340
|
+
BlueprintStepSchema,
|
|
166
341
|
])
|
|
167
342
|
);
|
|
168
343
|
|
|
@@ -173,6 +348,8 @@ const EvalSchema = z.object({
|
|
|
173
348
|
agent: z.string().optional(),
|
|
174
349
|
prompt: z.string().optional(),
|
|
175
350
|
run: z.string().optional(), // for script scorer
|
|
351
|
+
allowInsecure: z.boolean().optional(),
|
|
352
|
+
allowSecrets: z.boolean().optional(),
|
|
176
353
|
});
|
|
177
354
|
|
|
178
355
|
// ===== Workflow Schema =====
|
|
@@ -182,10 +359,14 @@ export const WorkflowSchema = z.object({
|
|
|
182
359
|
description: z.string().optional(),
|
|
183
360
|
inputs: z.record(InputSchema).optional(),
|
|
184
361
|
outputs: z.record(z.string()).optional(),
|
|
362
|
+
outputSchema: z.any().optional(), // JSON Schema for final workflow outputs
|
|
185
363
|
env: z.record(z.string()).optional(),
|
|
186
364
|
concurrency: z.union([z.number().int().positive(), z.string()]).optional(),
|
|
365
|
+
pools: z.record(z.union([z.number().int().positive(), z.string()])).optional(), // Resource pool overrides
|
|
187
366
|
steps: z.array(StepSchema),
|
|
367
|
+
errors: z.array(StepSchema).optional(),
|
|
188
368
|
finally: z.array(StepSchema).optional(),
|
|
369
|
+
compensate: z.lazy(() => StepSchema).optional(), // Top-level compensation for the entire workflow
|
|
189
370
|
eval: EvalSchema.optional(),
|
|
190
371
|
});
|
|
191
372
|
|
|
@@ -214,6 +395,10 @@ export type HumanStep = z.infer<typeof HumanStepSchema>;
|
|
|
214
395
|
export type SleepStep = z.infer<typeof SleepStepSchema>;
|
|
215
396
|
export type ScriptStep = z.infer<typeof ScriptStepSchema>;
|
|
216
397
|
export type MemoryStep = z.infer<typeof MemoryStepSchema>;
|
|
398
|
+
export type EngineStep = z.infer<typeof EngineStepSchema>;
|
|
399
|
+
export type JoinStep = z.infer<typeof JoinStepSchema>;
|
|
400
|
+
export type BlueprintStep = z.infer<typeof BlueprintStepSchema>;
|
|
401
|
+
export type Blueprint = z.infer<typeof BlueprintSchema>;
|
|
217
402
|
export type Workflow = z.infer<typeof WorkflowSchema>;
|
|
218
403
|
export type AgentTool = z.infer<typeof AgentToolSchema>;
|
|
219
404
|
export type Agent = z.infer<typeof AgentSchema>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface TestDefinition {
|
|
2
|
+
name: string;
|
|
3
|
+
workflow: string; // Name or path
|
|
4
|
+
fixture: {
|
|
5
|
+
inputs?: Record<string, unknown>;
|
|
6
|
+
env?: Record<string, string>;
|
|
7
|
+
secrets?: Record<string, string>;
|
|
8
|
+
mocks?: Array<{
|
|
9
|
+
step?: string;
|
|
10
|
+
type?: string;
|
|
11
|
+
prompt?: string;
|
|
12
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mock responses can be any type
|
|
13
|
+
response: any;
|
|
14
|
+
}>;
|
|
15
|
+
};
|
|
16
|
+
snapshot?: {
|
|
17
|
+
steps: Record<
|
|
18
|
+
string,
|
|
19
|
+
{
|
|
20
|
+
status: string;
|
|
21
|
+
// biome-ignore lint/suspicious/noExplicitAny: Step outputs can be any type
|
|
22
|
+
output: any;
|
|
23
|
+
error?: string;
|
|
24
|
+
}
|
|
25
|
+
>;
|
|
26
|
+
// biome-ignore lint/suspicious/noExplicitAny: Workflow outputs can be any type
|
|
27
|
+
outputs: Record<string, any>;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -94,6 +94,24 @@ steps:
|
|
|
94
94
|
expect(() => WorkflowParser.loadWorkflow(filePath)).toThrow(/Invalid workflow schema/);
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
+
test('should throw on invalid input enum defaults', () => {
|
|
98
|
+
const content = `
|
|
99
|
+
name: invalid-enum
|
|
100
|
+
inputs:
|
|
101
|
+
mode:
|
|
102
|
+
type: string
|
|
103
|
+
values: [fast, slow]
|
|
104
|
+
default: medium
|
|
105
|
+
steps:
|
|
106
|
+
- id: step1
|
|
107
|
+
type: shell
|
|
108
|
+
run: echo test
|
|
109
|
+
`;
|
|
110
|
+
const filePath = join(tempDir, 'invalid-enum.yaml');
|
|
111
|
+
writeFileSync(filePath, content);
|
|
112
|
+
expect(() => WorkflowParser.loadWorkflow(filePath)).toThrow(/Invalid workflow schema/);
|
|
113
|
+
});
|
|
114
|
+
|
|
97
115
|
test('should throw on non-existent file', () => {
|
|
98
116
|
expect(() => WorkflowParser.loadWorkflow('non-existent.yaml')).toThrow(
|
|
99
117
|
/Failed to parse workflow/
|
|
@@ -213,4 +231,40 @@ finally:
|
|
|
213
231
|
expect(cleanupStep?.needs).toContain('step1');
|
|
214
232
|
});
|
|
215
233
|
});
|
|
234
|
+
|
|
235
|
+
describe('validateStrict', () => {
|
|
236
|
+
test('should throw on invalid step schema definitions', () => {
|
|
237
|
+
const workflow = {
|
|
238
|
+
name: 'strict-invalid',
|
|
239
|
+
steps: [
|
|
240
|
+
{
|
|
241
|
+
id: 's1',
|
|
242
|
+
type: 'shell',
|
|
243
|
+
run: 'echo ok',
|
|
244
|
+
needs: [],
|
|
245
|
+
inputSchema: { type: 123 },
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
} as unknown as Workflow;
|
|
249
|
+
|
|
250
|
+
expect(() => WorkflowParser.validateStrict(workflow)).toThrow(/Strict validation failed/);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('should pass on valid step schema definitions', () => {
|
|
254
|
+
const workflow = {
|
|
255
|
+
name: 'strict-valid',
|
|
256
|
+
steps: [
|
|
257
|
+
{
|
|
258
|
+
id: 's1',
|
|
259
|
+
type: 'shell',
|
|
260
|
+
run: 'echo ok',
|
|
261
|
+
needs: [],
|
|
262
|
+
inputSchema: { type: 'object', properties: { run: { type: 'string' } } },
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
} as unknown as Workflow;
|
|
266
|
+
|
|
267
|
+
expect(() => WorkflowParser.validateStrict(workflow)).not.toThrow();
|
|
268
|
+
});
|
|
269
|
+
});
|
|
216
270
|
});
|