@shipfox/api-definitions 11.0.0 → 12.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/.turbo/turbo-build.log +13 -12
  2. package/CHANGELOG.md +81 -0
  3. package/dist/core/entities/index.d.ts +1 -0
  4. package/dist/core/entities/index.d.ts.map +1 -1
  5. package/dist/core/entities/index.js +1 -0
  6. package/dist/core/entities/index.js.map +1 -1
  7. package/dist/core/entities/sync-state.d.ts +4 -0
  8. package/dist/core/entities/sync-state.d.ts.map +1 -1
  9. package/dist/core/entities/sync-state.js +10 -0
  10. package/dist/core/entities/sync-state.js.map +1 -1
  11. package/dist/core/entities/validation-warning.d.ts +6 -0
  12. package/dist/core/entities/validation-warning.d.ts.map +1 -0
  13. package/dist/core/entities/validation-warning.js +3 -0
  14. package/dist/core/entities/validation-warning.js.map +1 -0
  15. package/dist/core/parse-definition.d.ts +9 -7
  16. package/dist/core/parse-definition.d.ts.map +1 -1
  17. package/dist/core/parse-definition.js +14 -2
  18. package/dist/core/parse-definition.js.map +1 -1
  19. package/dist/core/sync-definitions.d.ts +2 -0
  20. package/dist/core/sync-definitions.d.ts.map +1 -1
  21. package/dist/core/sync-definitions.js +6 -5
  22. package/dist/core/sync-definitions.js.map +1 -1
  23. package/dist/core/validate-definition.d.ts +9 -5
  24. package/dist/core/validate-definition.d.ts.map +1 -1
  25. package/dist/core/validate-definition.js +23 -2
  26. package/dist/core/validate-definition.js.map +1 -1
  27. package/dist/core/workflow-model/index.d.ts +1 -1
  28. package/dist/core/workflow-model/index.d.ts.map +1 -1
  29. package/dist/core/workflow-model/index.js.map +1 -1
  30. package/dist/core/workflow-model/invalid-workflow-model-error.d.ts +3 -1
  31. package/dist/core/workflow-model/invalid-workflow-model-error.d.ts.map +1 -1
  32. package/dist/core/workflow-model/invalid-workflow-model-error.js.map +1 -1
  33. package/dist/core/workflow-model/normalize-if-condition.d.ts +0 -1
  34. package/dist/core/workflow-model/normalize-if-condition.d.ts.map +1 -1
  35. package/dist/core/workflow-model/normalize-if-condition.js +0 -1
  36. package/dist/core/workflow-model/normalize-if-condition.js.map +1 -1
  37. package/dist/core/workflow-model/normalize-job-checkout.d.ts +15 -3
  38. package/dist/core/workflow-model/normalize-job-checkout.d.ts.map +1 -1
  39. package/dist/core/workflow-model/normalize-job-checkout.js +83 -4
  40. package/dist/core/workflow-model/normalize-job-checkout.js.map +1 -1
  41. package/dist/core/workflow-model/normalize-job-listening.js +0 -1
  42. package/dist/core/workflow-model/normalize-job-listening.js.map +1 -1
  43. package/dist/core/workflow-model/normalize-job-success.d.ts.map +1 -1
  44. package/dist/core/workflow-model/normalize-job-success.js +0 -1
  45. package/dist/core/workflow-model/normalize-job-success.js.map +1 -1
  46. package/dist/core/workflow-model/normalize-jobs.d.ts.map +1 -1
  47. package/dist/core/workflow-model/normalize-jobs.js +310 -45
  48. package/dist/core/workflow-model/normalize-jobs.js.map +1 -1
  49. package/dist/core/workflow-model/normalize-step-gate.js +0 -1
  50. package/dist/core/workflow-model/normalize-step-gate.js.map +1 -1
  51. package/dist/core/workflow-model/normalize-triggers.d.ts.map +1 -1
  52. package/dist/core/workflow-model/normalize-triggers.js +0 -1
  53. package/dist/core/workflow-model/normalize-triggers.js.map +1 -1
  54. package/dist/core/workflow-model/normalize-workflow-document.d.ts +3 -0
  55. package/dist/core/workflow-model/normalize-workflow-document.d.ts.map +1 -1
  56. package/dist/core/workflow-model/normalize-workflow-document.js +34 -4
  57. package/dist/core/workflow-model/normalize-workflow-document.js.map +1 -1
  58. package/dist/core/workflow-model/parse-interpolation-field.d.ts +1 -1
  59. package/dist/core/workflow-model/parse-interpolation-field.d.ts.map +1 -1
  60. package/dist/core/workflow-model/parse-interpolation-field.js +27 -57
  61. package/dist/core/workflow-model/parse-interpolation-field.js.map +1 -1
  62. package/dist/core/workflow-model/validate-literal-name.d.ts +12 -0
  63. package/dist/core/workflow-model/validate-literal-name.d.ts.map +1 -0
  64. package/dist/core/workflow-model/validate-literal-name.js +36 -0
  65. package/dist/core/workflow-model/validate-literal-name.js.map +1 -0
  66. package/dist/core/workflow-model/validate-predicate-expression.d.ts +1 -2
  67. package/dist/core/workflow-model/validate-predicate-expression.d.ts.map +1 -1
  68. package/dist/core/workflow-model/validate-predicate-expression.js +31 -90
  69. package/dist/core/workflow-model/validate-predicate-expression.js.map +1 -1
  70. package/dist/core/workflow-model/validation-issue.d.ts +2 -1
  71. package/dist/core/workflow-model/validation-issue.d.ts.map +1 -1
  72. package/dist/core/workflow-model/validation-issue.js +8 -2
  73. package/dist/core/workflow-model/validation-issue.js.map +1 -1
  74. package/dist/core/workflow-model/workflow-field-label.d.ts.map +1 -1
  75. package/dist/core/workflow-model/workflow-field-label.js +16 -2
  76. package/dist/core/workflow-model/workflow-field-label.js.map +1 -1
  77. package/dist/db/db.d.ts +38 -0
  78. package/dist/db/db.d.ts.map +1 -1
  79. package/dist/db/schema/sync-states.d.ts +19 -0
  80. package/dist/db/schema/sync-states.d.ts.map +1 -1
  81. package/dist/db/schema/sync-states.js +3 -1
  82. package/dist/db/schema/sync-states.js.map +1 -1
  83. package/dist/db/sync-states.d.ts +2 -1
  84. package/dist/db/sync-states.d.ts.map +1 -1
  85. package/dist/db/sync-states.js +4 -0
  86. package/dist/db/sync-states.js.map +1 -1
  87. package/dist/presentation/dto/definition.d.ts.map +1 -1
  88. package/dist/presentation/dto/definition.js +2 -1
  89. package/dist/presentation/dto/definition.js.map +1 -1
  90. package/dist/presentation/routes/project-access.d.ts +1 -0
  91. package/dist/presentation/routes/project-access.d.ts.map +1 -1
  92. package/dist/presentation/routes/validate-definition.d.ts.map +1 -1
  93. package/dist/presentation/routes/validate-definition.js +5 -3
  94. package/dist/presentation/routes/validate-definition.js.map +1 -1
  95. package/dist/temporal/activities/sync-activities.d.ts +6 -2
  96. package/dist/temporal/activities/sync-activities.d.ts.map +1 -1
  97. package/dist/temporal/activities/sync-activities.js +8 -1
  98. package/dist/temporal/activities/sync-activities.js.map +1 -1
  99. package/dist/temporal/workflows/definition-sync.js +4 -1
  100. package/dist/temporal/workflows/definition-sync.js.map +1 -1
  101. package/dist/temporal/workflows/index.bundle.js +9029 -3
  102. package/dist/tsconfig.test.tsbuildinfo +1 -1
  103. package/drizzle/0002_lumpy_ben_parker.sql +1 -0
  104. package/drizzle/meta/0002_snapshot.json +534 -0
  105. package/drizzle/meta/_journal.json +7 -0
  106. package/package.json +14 -14
  107. package/src/core/entities/index.ts +1 -0
  108. package/src/core/entities/sync-state.ts +23 -0
  109. package/src/core/entities/validation-warning.ts +5 -0
  110. package/src/core/parse-definition-warnings.test.ts +33 -0
  111. package/src/core/parse-definition.test.ts +7 -1
  112. package/src/core/parse-definition.ts +28 -10
  113. package/src/core/sync-definitions.test.ts +39 -0
  114. package/src/core/sync-definitions.ts +14 -4
  115. package/src/core/validate-definition.test.ts +128 -0
  116. package/src/core/validate-definition.ts +39 -8
  117. package/src/core/workflow-model/README.md +2 -2
  118. package/src/core/workflow-model/index.ts +1 -0
  119. package/src/core/workflow-model/invalid-workflow-model-error.ts +8 -2
  120. package/src/core/workflow-model/normalize-if-condition.ts +0 -2
  121. package/src/core/workflow-model/normalize-job-checkout.ts +105 -8
  122. package/src/core/workflow-model/normalize-job-listening.ts +0 -1
  123. package/src/core/workflow-model/normalize-job-success.ts +0 -1
  124. package/src/core/workflow-model/normalize-jobs.ts +330 -49
  125. package/src/core/workflow-model/normalize-step-gate.ts +0 -1
  126. package/src/core/workflow-model/normalize-triggers.ts +0 -1
  127. package/src/core/workflow-model/normalize-workflow-document.test.ts +732 -185
  128. package/src/core/workflow-model/normalize-workflow-document.ts +29 -4
  129. package/src/core/workflow-model/parse-interpolation-field.ts +50 -79
  130. package/src/core/workflow-model/validate-literal-name.ts +64 -0
  131. package/src/core/workflow-model/validate-predicate-expression.test.ts +153 -15
  132. package/src/core/workflow-model/validate-predicate-expression.ts +52 -120
  133. package/src/core/workflow-model/validation-issue.ts +4 -0
  134. package/src/core/workflow-model/workflow-field-label.ts +16 -2
  135. package/src/core/workflow-yaml/README.md +2 -2
  136. package/src/db/schema/sync-states.ts +4 -1
  137. package/src/db/sync-states.test.ts +57 -0
  138. package/src/db/sync-states.ts +6 -0
  139. package/src/presentation/dto/definition.ts +1 -0
  140. package/src/presentation/routes/list-definitions.test.ts +1 -0
  141. package/src/presentation/routes/validate-definition.test.ts +1 -0
  142. package/src/presentation/routes/validate-definition.ts +7 -1
  143. package/src/temporal/activities/sync-activities.test.ts +16 -0
  144. package/src/temporal/activities/sync-activities.ts +18 -3
  145. package/src/temporal/workflows/definition-sync.ts +1 -1
  146. package/test/agent-validation-catalog.ts +17 -0
  147. package/test/fixtures/valid-listening-job.yml +2 -1
  148. package/tsconfig.build.tsbuildinfo +1 -1
@@ -2,8 +2,13 @@ import type {AgentValidationCatalog} from '@shipfox/api-agent-dto/inter-module';
2
2
  import {
3
3
  type AvailabilitySite,
4
4
  buildTypedRootsEnvironment,
5
+ classifyShellCodePosition,
5
6
  type ExpressionType,
6
7
  type ExpressionTypeEnvironment,
8
+ hoistPlannedRunCommand,
9
+ type ShellReevaluatingConstruct,
10
+ type UnsafeRunInterpolation,
11
+ UnsafeRunInterpolationError,
7
12
  type WorkflowJobTypeOverlay,
8
13
  type WorkflowStepTypeOverlay,
9
14
  } from '@shipfox/expression';
@@ -24,17 +29,22 @@ import type {
24
29
  WorkflowEnvTemplates,
25
30
  WorkflowFieldTemplate,
26
31
  WorkflowModelAgentStep,
32
+ WorkflowModelCheckoutStep,
27
33
  WorkflowModelJob,
28
34
  WorkflowModelRunStep,
29
35
  WorkflowModelStep,
36
+ WorkflowModelStepCheckout,
30
37
  WorkflowOutputTemplates,
31
38
  WorkflowStepSourceLocationMap,
32
39
  } from '../entities/workflow-model.js';
33
- import type {WorkflowModelValidationIssue} from './invalid-workflow-model-error.js';
40
+ import type {
41
+ WorkflowModelValidationIssue,
42
+ WorkflowModelValidationIssuePathSegment,
43
+ } from './invalid-workflow-model-error.js';
34
44
  import {normalizeAgentIntegrations} from './normalize-agent-integrations.js';
35
45
  import {normalizeEnv} from './normalize-env.js';
36
46
  import {normalizeIfCondition} from './normalize-if-condition.js';
37
- import {normalizeJobCheckout} from './normalize-job-checkout.js';
47
+ import {normalizeCheckout, normalizeJobCheckout} from './normalize-job-checkout.js';
38
48
  import {normalizeJobListening} from './normalize-job-listening.js';
39
49
  import {normalizeJobSuccess} from './normalize-job-success.js';
40
50
  import {normalizeNeeds} from './normalize-needs.js';
@@ -43,6 +53,7 @@ import {normalizeStepOutputs} from './normalize-step-outputs.js';
43
53
  import {parseDurationMs} from './parse-duration-ms.js';
44
54
  import {parseInterpolationField} from './parse-interpolation-field.js';
45
55
  import {stableId} from './stable-id.js';
56
+ import {unescapeLiteralName, validateLiteralName} from './validate-literal-name.js';
46
57
  import {issue} from './validation-issue.js';
47
58
 
48
59
  export interface NormalizeContext {
@@ -178,6 +189,8 @@ function normalizeJob(params: {
178
189
  sourceName: params.sourceName,
179
190
  jobId: id,
180
191
  job: params.job,
192
+ workflowEnvKeys: Object.keys(params.document.env ?? {}),
193
+ jobEnvKeys: Object.keys(params.job.env ?? {}),
181
194
  issues: params.issues,
182
195
  stepSourceLocations: params.stepSourceLocations,
183
196
  fillSite: stepFillSite,
@@ -194,7 +207,9 @@ function normalizeJob(params: {
194
207
  issues: params.issues,
195
208
  defaultRunnerLabels: params.context.defaultRunnerLabels,
196
209
  });
197
- const checkout = normalizeJobCheckout(params.job.checkout);
210
+ const checkout = normalizeJobCheckout({
211
+ checkout: params.job.checkout,
212
+ });
198
213
  const jobEnv = normalizeEnv({
199
214
  env: params.job.env,
200
215
  path: ['jobs', params.sourceName, 'env'],
@@ -212,7 +227,6 @@ function normalizeJob(params: {
212
227
  const condition = normalizeIfCondition({
213
228
  field: 'job.if',
214
229
  source: params.job.if,
215
- site: 'job-activation',
216
230
  path: ['jobs', params.sourceName, 'if'],
217
231
  invalidCode: 'invalid-job-if',
218
232
  invalidMessage: 'Job if must be a valid wrapped CEL boolean expression.',
@@ -242,17 +256,29 @@ function normalizeJob(params: {
242
256
  issues: params.issues,
243
257
  allowedJobReferences,
244
258
  });
245
- const name =
246
- params.job.name === undefined
259
+ if (params.job.name !== undefined) {
260
+ validateLiteralName({
261
+ field: 'job.name',
262
+ dynamicField: 'execution_name',
263
+ source: params.job.name,
264
+ path: ['jobs', params.sourceName, 'name'],
265
+ message: 'Job name must be literal. Move runtime interpolation to execution_name.',
266
+ issues: params.issues,
267
+ });
268
+ }
269
+ const name = params.job.name === undefined ? undefined : unescapeLiteralName(params.job.name);
270
+ const executionName =
271
+ params.job.execution_name === undefined
247
272
  ? undefined
248
273
  : (parseInterpolationField({
249
- field: 'job.name',
250
- source: params.job.name,
251
- path: ['jobs', params.sourceName, 'name'],
274
+ field: 'job.execution_name',
275
+ source: params.job.execution_name,
276
+ path: ['jobs', params.sourceName, 'execution_name'],
252
277
  issues: params.issues,
278
+ fillSite: 'execution-creation',
253
279
  allowedJobReferences,
254
280
  typeOverlay: upstreamJobsTypeOverlay,
255
- }) ?? [{kind: 'literal' as const, value: params.job.name}]);
281
+ }) ?? [{kind: 'literal' as const, value: params.job.execution_name}]);
256
282
 
257
283
  return {
258
284
  id,
@@ -268,6 +294,7 @@ function normalizeJob(params: {
268
294
  ...(executionTimeoutMs === undefined ? {} : {executionTimeoutMs}),
269
295
  ...(listening === undefined ? {} : {listening}),
270
296
  ...(name === undefined ? {} : {name}),
297
+ ...(executionName === undefined ? {} : {executionName}),
271
298
  ...jobEnv,
272
299
  dependencies,
273
300
  steps,
@@ -434,6 +461,8 @@ function normalizeJobSteps(params: {
434
461
  sourceName: string;
435
462
  jobId: string;
436
463
  job: WorkflowDocumentJob;
464
+ workflowEnvKeys: readonly string[];
465
+ jobEnvKeys: readonly string[];
437
466
  issues: WorkflowModelValidationIssue[];
438
467
  stepSourceLocations: WorkflowStepSourceLocationMap | undefined;
439
468
  fillSite: AvailabilitySite;
@@ -445,13 +474,15 @@ function normalizeJobSteps(params: {
445
474
  }): readonly WorkflowModelStep[] {
446
475
  const usedStepIds = new Map<string, number>();
447
476
 
448
- return params.job.steps.map((step, index) =>
449
- normalizeStep({
477
+ return params.job.steps.flatMap((step, index) => {
478
+ const normalized = normalizeStep({
450
479
  step,
451
480
  index,
452
481
  sourceName: params.sourceName,
453
482
  jobId: params.jobId,
454
483
  allSteps: params.job.steps,
484
+ workflowEnvKeys: params.workflowEnvKeys,
485
+ jobEnvKeys: params.jobEnvKeys,
455
486
  usedStepIds,
456
487
  issues: params.issues,
457
488
  stepSourceLocations: params.stepSourceLocations,
@@ -461,8 +492,9 @@ function normalizeJobSteps(params: {
461
492
  upstreamJobs: params.upstreamJobs,
462
493
  directNeedJobs: params.directNeedJobs,
463
494
  context: params.context,
464
- }),
465
- );
495
+ });
496
+ return normalized === undefined ? [] : [normalized];
497
+ });
466
498
  }
467
499
 
468
500
  function normalizeStep(params: {
@@ -471,6 +503,8 @@ function normalizeStep(params: {
471
503
  sourceName: string;
472
504
  jobId: string;
473
505
  allSteps: readonly WorkflowDocumentStep[];
506
+ workflowEnvKeys: readonly string[];
507
+ jobEnvKeys: readonly string[];
474
508
  usedStepIds: Map<string, number>;
475
509
  issues: WorkflowModelValidationIssue[];
476
510
  stepSourceLocations: WorkflowStepSourceLocationMap | undefined;
@@ -480,7 +514,7 @@ function normalizeStep(params: {
480
514
  upstreamJobs: readonly WorkflowJobTypeOverlay[];
481
515
  directNeedJobs: readonly WorkflowJobTypeOverlay[];
482
516
  context: NormalizeContext;
483
- }): WorkflowModelStep {
517
+ }): WorkflowModelStep | undefined {
484
518
  const stepKey = params.step.key;
485
519
  const stepId =
486
520
  stepKey === undefined
@@ -532,7 +566,6 @@ function normalizeStep(params: {
532
566
  const condition = normalizeIfCondition({
533
567
  field: 'step.if',
534
568
  source: params.step.if,
535
- site: 'step-dispatch',
536
569
  path: ['jobs', params.sourceName, 'steps', params.index, 'if'],
537
570
  invalidCode: 'invalid-step-if',
538
571
  invalidMessage: 'Step if must be a valid wrapped CEL boolean expression.',
@@ -567,10 +600,25 @@ function normalizeStep(params: {
567
600
  allowedJobReferences: params.allowedJobReferences,
568
601
  typeOverlay,
569
602
  });
603
+ const workingDirectory =
604
+ params.step.working_directory === undefined
605
+ ? undefined
606
+ : parseInterpolationField({
607
+ field: 'step.working_directory',
608
+ source: params.step.working_directory,
609
+ path: ['jobs', params.sourceName, 'steps', params.index, 'working_directory'],
610
+ issues: params.issues,
611
+ fillSite: params.fillSite,
612
+ allowedJobReferences: params.allowedJobReferences,
613
+ typeOverlay,
614
+ });
570
615
  const stepBase = {
571
616
  id: stepId,
572
617
  ...(stepKey === undefined ? {} : {key: stepKey}),
573
618
  ...(params.step.name === undefined ? {} : {name: params.step.name}),
619
+ ...(params.step.working_directory === undefined
620
+ ? {}
621
+ : {workingDirectory: params.step.working_directory}),
574
622
  ...(outputs === undefined ? {} : {outputs}),
575
623
  ...(sourceLocation === undefined ? {} : {sourceLocation}),
576
624
  ...(condition === undefined ? {} : {if: condition}),
@@ -583,7 +631,10 @@ function normalizeStep(params: {
583
631
  stepBase,
584
632
  sourceName: params.sourceName,
585
633
  stepIndex: params.index,
634
+ workflowEnvKeys: params.workflowEnvKeys,
635
+ jobEnvKeys: params.jobEnvKeys,
586
636
  name,
637
+ workingDirectory,
587
638
  issues: params.issues,
588
639
  fillSite: params.fillSite,
589
640
  allowedJobReferences: params.allowedJobReferences,
@@ -598,6 +649,7 @@ function normalizeStep(params: {
598
649
  sourceName: params.sourceName,
599
650
  stepIndex: params.index,
600
651
  name,
652
+ workingDirectory,
601
653
  issues: params.issues,
602
654
  fillSite: params.fillSite,
603
655
  allowedJobReferences: params.allowedJobReferences,
@@ -606,9 +658,55 @@ function normalizeStep(params: {
606
658
  });
607
659
  }
608
660
 
609
- // workflowDocumentStepSchema requires either `run` or an agent `prompt`; this
610
- // keeps the model-step union honest if callers bypass the document parser.
611
- throw new Error(`Workflow step "${stepId}" is neither a run nor an agent step`);
661
+ if (params.step.checkout !== undefined) {
662
+ return normalizeCheckoutStep({
663
+ step: params.step,
664
+ stepBase,
665
+ name,
666
+ sourceName: params.sourceName,
667
+ stepIndex: params.index,
668
+ issues: params.issues,
669
+ fillSite: params.fillSite,
670
+ allowedJobReferences: params.allowedJobReferences,
671
+ typeOverlay,
672
+ });
673
+ }
674
+
675
+ // Keep the model-step union honest if callers bypass the document parser.
676
+ throw new Error(`Workflow step "${stepId}" is neither a run, agent, nor checkout step`);
677
+ }
678
+
679
+ function normalizeCheckoutStep(params: {
680
+ step: WorkflowDocumentStep;
681
+ stepBase: WorkflowModelStepBaseFields;
682
+ name: WorkflowFieldTemplate | undefined;
683
+ sourceName: string;
684
+ stepIndex: number;
685
+ issues: WorkflowModelValidationIssue[];
686
+ fillSite: AvailabilitySite;
687
+ allowedJobReferences: ReadonlySet<string>;
688
+ typeOverlay?: ExpressionTypeEnvironment | undefined;
689
+ }): WorkflowModelCheckoutStep {
690
+ const checkout = params.step.checkout;
691
+ if (checkout === undefined) {
692
+ throw new Error('Checkout step normalization requires checkout settings');
693
+ }
694
+
695
+ const normalizedCheckout: WorkflowModelStepCheckout = normalizeCheckout({
696
+ checkout,
697
+ issues: params.issues,
698
+ path: ['jobs', params.sourceName, 'steps', params.stepIndex, 'checkout'],
699
+ fillSite: params.fillSite,
700
+ allowedJobReferences: params.allowedJobReferences,
701
+ typeOverlay: params.typeOverlay,
702
+ });
703
+
704
+ return {
705
+ ...params.stepBase,
706
+ kind: 'checkout',
707
+ checkout: normalizedCheckout,
708
+ ...(params.name === undefined ? {} : {templates: {name: params.name}}),
709
+ };
612
710
  }
613
711
 
614
712
  function normalizeRunStep(params: {
@@ -616,7 +714,10 @@ function normalizeRunStep(params: {
616
714
  stepBase: WorkflowModelStepBaseFields;
617
715
  sourceName: string;
618
716
  stepIndex: number;
717
+ workflowEnvKeys: readonly string[];
718
+ jobEnvKeys: readonly string[];
619
719
  name: WorkflowFieldTemplate | undefined;
720
+ workingDirectory: WorkflowFieldTemplate | undefined;
620
721
  issues: WorkflowModelValidationIssue[];
621
722
  fillSite: AvailabilitySite;
622
723
  allowedJobReferences: ReadonlySet<string>;
@@ -643,9 +744,19 @@ function normalizeRunStep(params: {
643
744
  allowedJobReferences: params.allowedJobReferences,
644
745
  typeOverlay: params.typeOverlay,
645
746
  });
747
+ addReevaluatingCommandWarnings({
748
+ command: commandTemplate,
749
+ source: params.step.run,
750
+ workflowEnvKeys: params.workflowEnvKeys,
751
+ jobEnvKeys: params.jobEnvKeys,
752
+ stepEnvKeys: Object.keys(params.step.env ?? {}),
753
+ path: ['jobs', params.sourceName, 'steps', params.stepIndex, 'run'],
754
+ issues: params.issues,
755
+ });
646
756
  const templates = optionalRunStepTemplates({
647
757
  command: commandTemplate,
648
758
  name: params.name,
759
+ workingDirectory: params.workingDirectory,
649
760
  env: stepEnv.templates?.env,
650
761
  });
651
762
 
@@ -658,12 +769,110 @@ function normalizeRunStep(params: {
658
769
  };
659
770
  }
660
771
 
772
+ function addReevaluatingCommandWarnings(params: {
773
+ command: WorkflowFieldTemplate | undefined;
774
+ source: string;
775
+ workflowEnvKeys: readonly string[];
776
+ jobEnvKeys: readonly string[];
777
+ stepEnvKeys: readonly string[];
778
+ path: readonly WorkflowModelValidationIssuePathSegment[];
779
+ issues: WorkflowModelValidationIssue[];
780
+ }): void {
781
+ const envKeys = new Set([...params.workflowEnvKeys, ...params.jobEnvKeys, ...params.stepEnvKeys]);
782
+ let hoisted: ReturnType<typeof hoistPlannedRunCommand>;
783
+
784
+ try {
785
+ hoisted = hoistPlannedRunCommand({
786
+ field: {
787
+ segments: params.command ?? [{kind: 'literal', value: params.source}],
788
+ },
789
+ reservedNames: envKeys,
790
+ });
791
+ } catch (error) {
792
+ if (!(error instanceof UnsafeRunInterpolationError)) return;
793
+
794
+ for (const occurrence of error.occurrences) {
795
+ params.issues.push(
796
+ issue({
797
+ code: 're-evaluating-command',
798
+ message: unsafeInterpolationWarningMessage(occurrence),
799
+ path: params.path,
800
+ severity: 'warning',
801
+ }),
802
+ );
803
+ }
804
+
805
+ if (error.partial === undefined) return;
806
+ hoisted = error.partial;
807
+ }
808
+
809
+ try {
810
+ const matches = classifyShellCodePosition({
811
+ command: hoisted.command,
812
+ workflowDataNames: [...envKeys, ...hoisted.bindings.map((binding) => binding.name)],
813
+ }).matches;
814
+
815
+ for (const match of matches) {
816
+ params.issues.push(
817
+ issue({
818
+ code: 're-evaluating-command',
819
+ message: reevaluatingCommandWarningMessage(match.construct, match.name),
820
+ path: params.path,
821
+ severity: 'warning',
822
+ }),
823
+ );
824
+ }
825
+ } catch {
826
+ return;
827
+ }
828
+ }
829
+
830
+ function unsafeInterpolationWarningMessage(error: UnsafeRunInterpolation): string {
831
+ const regionLabel = {
832
+ single: 'single-quoted shell text',
833
+ double: 'double-quoted shell text',
834
+ 'dollar-single': 'ANSI-C shell text',
835
+ 'dollar-double': 'dollar-double-quoted shell text',
836
+ 'paren-sub': 'command substitution',
837
+ arith: 'shell arithmetic',
838
+ backtick: 'backtick substitution',
839
+ 'param-brace': 'shell parameter expansion',
840
+ heredoc: 'a heredoc',
841
+ 'line-comment': 'a shell comment',
842
+ escape: 'a shell escape',
843
+ }[error.region];
844
+
845
+ return `Value "${error.source}" is inside ${regionLabel} and will be re-executed as code. Hoisting cannot protect it; move the interpolation to a normal quoted argument instead.`;
846
+ }
847
+
848
+ function reevaluatingCommandWarningMessage(
849
+ construct: ShellReevaluatingConstruct,
850
+ name: string,
851
+ ): string {
852
+ const constructLabel = {
853
+ eval: 'eval',
854
+ 'sh-c': 'sh -c',
855
+ 'bash-c': 'bash -c',
856
+ source: 'source',
857
+ let: 'let',
858
+ 'declare-i': 'declare -i',
859
+ arithmetic: 'shell arithmetic',
860
+ awk: 'awk',
861
+ jq: 'jq',
862
+ sed: 'sed',
863
+ 'xargs-sh-c': 'xargs sh -c',
864
+ }[construct];
865
+
866
+ return `Value "$${name}" is passed to ${constructLabel} and re-executed as code. Hoisting cannot protect it; pass the value to a fixed program instead.`;
867
+ }
868
+
661
869
  function normalizeAgentStep(params: {
662
870
  step: WorkflowDocumentStep;
663
871
  stepBase: WorkflowModelStepBaseFields;
664
872
  sourceName: string;
665
873
  stepIndex: number;
666
874
  name: WorkflowFieldTemplate | undefined;
875
+ workingDirectory: WorkflowFieldTemplate | undefined;
667
876
  issues: WorkflowModelValidationIssue[];
668
877
  fillSite: AvailabilitySite;
669
878
  allowedJobReferences: ReadonlySet<string>;
@@ -707,12 +916,27 @@ function normalizeAgentStep(params: {
707
916
  allowedJobReferences: params.allowedJobReferences,
708
917
  typeOverlay: params.typeOverlay,
709
918
  });
919
+ const thinkingTemplate =
920
+ params.step.thinking === undefined
921
+ ? undefined
922
+ : parseInterpolationField({
923
+ field: 'agent.thinking',
924
+ source: params.step.thinking,
925
+ path: ['jobs', params.sourceName, 'steps', params.stepIndex, 'thinking'],
926
+ issues: params.issues,
927
+ fillSite: params.fillSite,
928
+ allowedJobReferences: params.allowedJobReferences,
929
+ typeOverlay: params.typeOverlay,
930
+ });
710
931
  validateAgentStep({
711
932
  step: params.step,
712
933
  sourceName: params.sourceName,
713
934
  stepIndex: params.stepIndex,
714
935
  issues: params.issues,
715
- validateLiteralProvider: providerTemplate === undefined,
936
+ validateLiteralModel:
937
+ params.step.model !== undefined && !hasInterpolationSyntax(params.step.model),
938
+ validateLiteralProvider:
939
+ params.step.provider !== undefined && !hasInterpolationSyntax(params.step.provider),
716
940
  agentValidationCatalog: params.context.agentValidationCatalog,
717
941
  });
718
942
  const integrations = normalizeAgentIntegrations({
@@ -726,7 +950,9 @@ function normalizeAgentStep(params: {
726
950
  prompt: promptTemplate,
727
951
  model: modelTemplate,
728
952
  provider: providerTemplate,
953
+ thinking: thinkingTemplate,
729
954
  name: params.name,
955
+ workingDirectory: params.workingDirectory,
730
956
  });
731
957
 
732
958
  return {
@@ -748,47 +974,83 @@ function validateAgentStep(params: {
748
974
  sourceName: string;
749
975
  stepIndex: number;
750
976
  issues: WorkflowModelValidationIssue[];
977
+ validateLiteralModel: boolean;
751
978
  validateLiteralProvider: boolean;
752
979
  agentValidationCatalog: AgentValidationCatalog;
753
980
  }): void {
754
981
  validateHarnessThinking(params);
755
982
  validateHarnessTools(params);
756
- if (!params.validateLiteralProvider) return;
757
-
758
983
  const providerId = params.step.provider;
759
- if (providerId === undefined) return;
760
-
761
- const provider = params.agentValidationCatalog.providers.find((entry) => entry.id === providerId);
762
984
  const harness = params.step.harness;
763
- if (provider === undefined && harness === 'pi') return;
985
+ const provider =
986
+ providerId === undefined
987
+ ? undefined
988
+ : params.agentValidationCatalog.providers.find((entry) => entry.id === providerId);
989
+
990
+ if (params.validateLiteralProvider && providerId !== undefined) {
991
+ if (provider === undefined) {
992
+ if (harness === undefined || harness === 'pi') return;
993
+ params.issues.push(
994
+ issue({
995
+ code: 'invalid-provider',
996
+ message: `Provider "${providerId}" is not supported.`,
997
+ path: ['jobs', params.sourceName, 'steps', params.stepIndex, 'provider'],
998
+ details: {provider: providerId},
999
+ }),
1000
+ );
1001
+ return;
1002
+ }
764
1003
 
765
- if (provider === undefined || provider.support_status !== 'supported') {
766
- params.issues.push(
767
- issue({
768
- code: 'invalid-provider',
769
- message: `Provider "${providerId}" is not supported.`,
770
- path: ['jobs', params.sourceName, 'steps', params.stepIndex, 'provider'],
771
- details: {provider: providerId},
772
- }),
1004
+ if (provider.support_status !== 'supported') {
1005
+ params.issues.push(
1006
+ issue({
1007
+ code: 'invalid-provider',
1008
+ message: `Provider "${providerId}" is not supported.`,
1009
+ path: ['jobs', params.sourceName, 'steps', params.stepIndex, 'provider'],
1010
+ details: {provider: providerId},
1011
+ }),
1012
+ );
1013
+ return;
1014
+ }
1015
+
1016
+ if (harness === undefined) return;
1017
+
1018
+ const descriptor = params.agentValidationCatalog.harnesses.find(
1019
+ (entry) => entry.id === harness,
773
1020
  );
774
- return;
1021
+ if (!descriptor?.supported_provider_ids.includes(providerId)) {
1022
+ params.issues.push(
1023
+ issue({
1024
+ code: 'harness-provider-incompatible',
1025
+ message: `Harness "${harness}" does not support provider: ${providerId}. Supported providers: ${descriptor?.supported_provider_ids.join(', ') ?? ''}.`,
1026
+ path: ['jobs', params.sourceName, 'steps', params.stepIndex, 'provider'],
1027
+ details: {
1028
+ harness,
1029
+ provider: providerId,
1030
+ supportedProviders: descriptor?.supported_provider_ids ?? [],
1031
+ },
1032
+ }),
1033
+ );
1034
+ return;
1035
+ }
775
1036
  }
776
1037
 
777
- if (harness === undefined) return;
1038
+ if (!params.validateLiteralModel || providerId === undefined || harness === undefined) return;
1039
+ if (provider === undefined || provider.support_status !== 'supported') return;
1040
+
1041
+ const model = params.step.model;
1042
+ if (model === undefined) return;
778
1043
 
779
1044
  const descriptor = params.agentValidationCatalog.harnesses.find((entry) => entry.id === harness);
780
- if (descriptor?.supported_provider_ids.includes(providerId)) return;
1045
+ const modelIds = descriptor?.model_ids_by_provider?.[providerId];
1046
+ if (modelIds === undefined || modelIds.includes(model)) return;
781
1047
 
782
1048
  params.issues.push(
783
1049
  issue({
784
- code: 'harness-provider-incompatible',
785
- message: `Harness "${harness}" does not support provider: ${providerId}. Supported providers: ${descriptor?.supported_provider_ids.join(', ') ?? ''}.`,
786
- path: ['jobs', params.sourceName, 'steps', params.stepIndex, 'provider'],
787
- details: {
788
- harness,
789
- provider: providerId,
790
- supportedProviders: descriptor?.supported_provider_ids ?? [],
791
- },
1050
+ code: 'invalid-model',
1051
+ message: `Agent model "${model}" is not available for harness "${harness}" and provider "${providerId}".`,
1052
+ path: ['jobs', params.sourceName, 'steps', params.stepIndex, 'model'],
1053
+ details: {harness, provider: providerId, model},
792
1054
  }),
793
1055
  );
794
1056
  }
@@ -844,11 +1106,14 @@ function validateHarnessThinking(params: {
844
1106
  }): void {
845
1107
  const {harness, thinking} = params.step;
846
1108
  if (harness === undefined || thinking === undefined) return;
1109
+ // An interpolated level is only known when the step dispatches, so the agent
1110
+ // module checks it against the resolved harness there.
1111
+ if (hasInterpolationSyntax(thinking)) return;
847
1112
 
848
1113
  const supportedLevels =
849
1114
  params.agentValidationCatalog.harnesses.find((entry) => entry.id === harness)
850
1115
  ?.thinking_levels ?? [];
851
- if (supportedLevels.includes(thinking)) return;
1116
+ if ((supportedLevels as readonly string[]).includes(thinking)) return;
852
1117
  params.issues.push(
853
1118
  issue({
854
1119
  code: 'harness-thinking-incompatible',
@@ -953,27 +1218,35 @@ function validateRunnerLabels(params: {
953
1218
 
954
1219
  type WorkflowModelStepBaseFields = Pick<
955
1220
  WorkflowModelStep,
956
- 'id' | 'key' | 'name' | 'outputs' | 'sourceLocation' | 'gate'
1221
+ 'id' | 'key' | 'name' | 'workingDirectory' | 'outputs' | 'sourceLocation' | 'gate'
957
1222
  >;
958
1223
 
959
1224
  function optionalRunStepTemplates(params: {
960
1225
  command: WorkflowFieldTemplate | undefined;
961
1226
  name: WorkflowFieldTemplate | undefined;
1227
+ workingDirectory: WorkflowFieldTemplate | undefined;
962
1228
  env: WorkflowEnvTemplates | undefined;
963
1229
  }):
964
1230
  | {
965
1231
  command?: WorkflowFieldTemplate;
966
1232
  name?: WorkflowFieldTemplate;
1233
+ workingDirectory?: WorkflowFieldTemplate;
967
1234
  env?: WorkflowEnvTemplates;
968
1235
  }
969
1236
  | undefined {
970
- if (params.command === undefined && params.name === undefined && params.env === undefined) {
1237
+ if (
1238
+ params.command === undefined &&
1239
+ params.name === undefined &&
1240
+ params.workingDirectory === undefined &&
1241
+ params.env === undefined
1242
+ ) {
971
1243
  return undefined;
972
1244
  }
973
1245
 
974
1246
  return {
975
1247
  ...(params.command === undefined ? {} : {command: params.command}),
976
1248
  ...(params.name === undefined ? {} : {name: params.name}),
1249
+ ...(params.workingDirectory === undefined ? {} : {workingDirectory: params.workingDirectory}),
977
1250
  ...(params.env === undefined ? {} : {env: params.env}),
978
1251
  };
979
1252
  }
@@ -982,20 +1255,26 @@ function optionalAgentStepTemplates(params: {
982
1255
  prompt: WorkflowFieldTemplate | undefined;
983
1256
  model: WorkflowFieldTemplate | undefined;
984
1257
  provider: WorkflowFieldTemplate | undefined;
1258
+ thinking: WorkflowFieldTemplate | undefined;
985
1259
  name: WorkflowFieldTemplate | undefined;
1260
+ workingDirectory: WorkflowFieldTemplate | undefined;
986
1261
  }):
987
1262
  | {
988
1263
  prompt?: WorkflowFieldTemplate;
989
1264
  model?: WorkflowFieldTemplate;
990
1265
  provider?: WorkflowFieldTemplate;
1266
+ thinking?: WorkflowFieldTemplate;
991
1267
  name?: WorkflowFieldTemplate;
1268
+ workingDirectory?: WorkflowFieldTemplate;
992
1269
  }
993
1270
  | undefined {
994
1271
  if (
995
1272
  params.prompt === undefined &&
996
1273
  params.model === undefined &&
997
1274
  params.provider === undefined &&
998
- params.name === undefined
1275
+ params.thinking === undefined &&
1276
+ params.name === undefined &&
1277
+ params.workingDirectory === undefined
999
1278
  ) {
1000
1279
  return undefined;
1001
1280
  }
@@ -1004,6 +1283,8 @@ function optionalAgentStepTemplates(params: {
1004
1283
  ...(params.prompt === undefined ? {} : {prompt: params.prompt}),
1005
1284
  ...(params.model === undefined ? {} : {model: params.model}),
1006
1285
  ...(params.provider === undefined ? {} : {provider: params.provider}),
1286
+ ...(params.thinking === undefined ? {} : {thinking: params.thinking}),
1007
1287
  ...(params.name === undefined ? {} : {name: params.name}),
1288
+ ...(params.workingDirectory === undefined ? {} : {workingDirectory: params.workingDirectory}),
1008
1289
  };
1009
1290
  }
@@ -79,7 +79,6 @@ function normalizeGateSuccess(params: {
79
79
  return validatePredicateExpression({
80
80
  field: 'step.success',
81
81
  source: params.source,
82
- site: 'step-report',
83
82
  path: ['jobs', params.sourceName, 'steps', params.stepIndex, 'gate', 'success'],
84
83
  invalidCode: 'invalid-step-gate-success',
85
84
  invalidMessage: 'Step gate success must be a valid CEL boolean expression.',
@@ -107,7 +107,6 @@ function validateTriggerFilter(params: {
107
107
  validatePredicateExpression({
108
108
  field: 'trigger.filter',
109
109
  source: trigger.filter,
110
- site: 'ingest',
111
110
  path,
112
111
  invalidCode: 'invalid-trigger-filter',
113
112
  invalidMessage: 'Trigger filter must be a valid boolean predicate.',