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.
Files changed (105) hide show
  1. package/README.md +486 -54
  2. package/package.json +8 -2
  3. package/src/__fixtures__/index.ts +100 -0
  4. package/src/cli.ts +809 -90
  5. package/src/db/memory-db.ts +35 -1
  6. package/src/db/workflow-db.test.ts +24 -0
  7. package/src/db/workflow-db.ts +469 -14
  8. package/src/expression/evaluator.ts +68 -4
  9. package/src/parser/agent-parser.ts +6 -3
  10. package/src/parser/config-schema.ts +38 -2
  11. package/src/parser/schema.ts +192 -7
  12. package/src/parser/test-schema.ts +29 -0
  13. package/src/parser/workflow-parser.test.ts +54 -0
  14. package/src/parser/workflow-parser.ts +153 -7
  15. package/src/runner/aggregate-error.test.ts +57 -0
  16. package/src/runner/aggregate-error.ts +46 -0
  17. package/src/runner/audit-verification.test.ts +2 -2
  18. package/src/runner/auto-heal.test.ts +1 -1
  19. package/src/runner/blueprint-executor.test.ts +63 -0
  20. package/src/runner/blueprint-executor.ts +157 -0
  21. package/src/runner/concurrency-limit.test.ts +82 -0
  22. package/src/runner/debug-repl.ts +18 -3
  23. package/src/runner/durable-timers.test.ts +200 -0
  24. package/src/runner/engine-executor.test.ts +464 -0
  25. package/src/runner/engine-executor.ts +489 -0
  26. package/src/runner/foreach-executor.ts +30 -12
  27. package/src/runner/llm-adapter.test.ts +282 -5
  28. package/src/runner/llm-adapter.ts +581 -8
  29. package/src/runner/llm-clarification.test.ts +79 -21
  30. package/src/runner/llm-errors.ts +83 -0
  31. package/src/runner/llm-executor.test.ts +258 -219
  32. package/src/runner/llm-executor.ts +226 -29
  33. package/src/runner/mcp-client.ts +70 -3
  34. package/src/runner/mcp-manager.test.ts +52 -52
  35. package/src/runner/mcp-manager.ts +12 -5
  36. package/src/runner/mcp-server.test.ts +117 -78
  37. package/src/runner/mcp-server.ts +13 -4
  38. package/src/runner/optimization-runner.ts +48 -31
  39. package/src/runner/reflexion.test.ts +1 -1
  40. package/src/runner/resource-pool.test.ts +113 -0
  41. package/src/runner/resource-pool.ts +164 -0
  42. package/src/runner/shell-executor.ts +130 -32
  43. package/src/runner/standard-tools-integration.test.ts +36 -36
  44. package/src/runner/standard-tools.test.ts +18 -0
  45. package/src/runner/standard-tools.ts +110 -37
  46. package/src/runner/step-executor.test.ts +176 -16
  47. package/src/runner/step-executor.ts +530 -86
  48. package/src/runner/stream-utils.test.ts +14 -0
  49. package/src/runner/subflow-outputs.test.ts +103 -0
  50. package/src/runner/test-harness.ts +161 -0
  51. package/src/runner/tool-integration.test.ts +73 -79
  52. package/src/runner/workflow-runner.test.ts +492 -15
  53. package/src/runner/workflow-runner.ts +1438 -79
  54. package/src/runner/workflow-subflows.test.ts +255 -0
  55. package/src/templates/agents/keystone-architect.md +19 -14
  56. package/src/templates/agents/tester.md +21 -0
  57. package/src/templates/batch-processor.yaml +1 -1
  58. package/src/templates/child-rollback.yaml +11 -0
  59. package/src/templates/decompose-implement.yaml +53 -0
  60. package/src/templates/decompose-problem.yaml +159 -0
  61. package/src/templates/decompose-research.yaml +52 -0
  62. package/src/templates/decompose-review.yaml +51 -0
  63. package/src/templates/dev.yaml +134 -0
  64. package/src/templates/engine-example.yaml +33 -0
  65. package/src/templates/fan-out-fan-in.yaml +61 -0
  66. package/src/templates/loop-parallel.yaml +1 -1
  67. package/src/templates/memory-service.yaml +1 -1
  68. package/src/templates/parent-rollback.yaml +16 -0
  69. package/src/templates/robust-automation.yaml +1 -1
  70. package/src/templates/scaffold-feature.yaml +29 -27
  71. package/src/templates/scaffold-generate.yaml +41 -0
  72. package/src/templates/scaffold-plan.yaml +53 -0
  73. package/src/types/status.ts +3 -0
  74. package/src/ui/dashboard.tsx +4 -3
  75. package/src/utils/assets.macro.ts +36 -0
  76. package/src/utils/auth-manager.ts +585 -8
  77. package/src/utils/blueprint-utils.test.ts +49 -0
  78. package/src/utils/blueprint-utils.ts +80 -0
  79. package/src/utils/circuit-breaker.test.ts +177 -0
  80. package/src/utils/circuit-breaker.ts +160 -0
  81. package/src/utils/config-loader.test.ts +100 -13
  82. package/src/utils/config-loader.ts +44 -17
  83. package/src/utils/constants.ts +62 -0
  84. package/src/utils/error-renderer.test.ts +267 -0
  85. package/src/utils/error-renderer.ts +320 -0
  86. package/src/utils/json-parser.test.ts +4 -0
  87. package/src/utils/json-parser.ts +18 -1
  88. package/src/utils/mermaid.ts +4 -0
  89. package/src/utils/paths.test.ts +46 -0
  90. package/src/utils/paths.ts +70 -0
  91. package/src/utils/process-sandbox.test.ts +128 -0
  92. package/src/utils/process-sandbox.ts +293 -0
  93. package/src/utils/rate-limiter.test.ts +143 -0
  94. package/src/utils/rate-limiter.ts +221 -0
  95. package/src/utils/redactor.test.ts +23 -15
  96. package/src/utils/redactor.ts +65 -25
  97. package/src/utils/resource-loader.test.ts +54 -0
  98. package/src/utils/resource-loader.ts +158 -0
  99. package/src/utils/sandbox.test.ts +69 -4
  100. package/src/utils/sandbox.ts +69 -6
  101. package/src/utils/schema-validator.ts +65 -0
  102. package/src/utils/workflow-registry.test.ts +57 -0
  103. package/src/utils/workflow-registry.ts +45 -25
  104. /package/src/expression/{evaluator.audit.test.ts → evaluator-audit.test.ts} +0 -0
  105. /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
- steps?: Record<string, { output?: unknown; outputs?: Record<string, unknown>; status?: string }>;
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
- return ExpressionEvaluator.evaluateNode(ast, context);
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(node: ASTNode, context: ExpressionContext): unknown {
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 = readFileSync(filePath, 'utf8');
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 (existsSync(path)) {
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.enum(['openai', 'anthropic', 'copilot']).default('openai'),
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>;
@@ -2,11 +2,84 @@ import { z } from 'zod';
2
2
 
3
3
  // ===== Input/Output Schema =====
4
4
 
5
- const InputSchema = z.object({
6
- type: z.enum(['string', 'number', 'boolean', 'array', 'object']),
7
- default: z.any().optional(),
8
- description: z.string().optional(),
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
  });