@wichayutdew/pi-workflows 1.0.0 → 2.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.
- package/README.md +312 -221
- package/agents/step.md +4 -11
- package/dist/index.js +2556 -961
- package/examples/mr-comments.workflow.yaml +18 -8
- package/examples/prompts/mr-comments/implement.md +13 -9
- package/examples/prompts/mr-comments/plan.md +44 -15
- package/examples/prompts/mr-comments/verify.md +8 -7
- package/examples/settings.yaml +7 -1
- package/examples/starter-kit/mr-comment.workflow.yaml +149 -0
- package/examples/starter-kit/mr-review.workflow.yaml +130 -0
- package/examples/starter-kit/steps/mr-comment/fetch.md +33 -0
- package/examples/starter-kit/steps/mr-comment/implement.md +38 -0
- package/examples/starter-kit/steps/mr-comment/plan.md +62 -0
- package/examples/starter-kit/steps/mr-comment/publish.md +36 -0
- package/examples/starter-kit/steps/mr-comment/verify.md +35 -0
- package/examples/starter-kit/steps/mr-review/fetch.md +25 -0
- package/examples/starter-kit/steps/mr-review/publish.md +30 -0
- package/examples/starter-kit/steps/mr-review/review.md +69 -0
- package/examples/starter-kit/steps/mr-review/verify.md +24 -0
- package/examples/starter-kit/steps/shared/prepare-workspace.md +57 -0
- package/examples/starter-kit/steps/ticket/implement.md +35 -0
- package/examples/starter-kit/steps/ticket/plan.md +40 -0
- package/examples/starter-kit/steps/ticket/verify.md +29 -0
- package/examples/starter-kit/steps/work/implement.md +34 -0
- package/examples/starter-kit/steps/work/plan.md +38 -0
- package/examples/starter-kit/steps/work/verify.md +30 -0
- package/examples/starter-kit/ticket.workflow.yaml +100 -0
- package/examples/starter-kit/work.workflow.yaml +97 -0
- package/package.json +3 -3
- package/schemas/workflow.schema.json +78 -35
- package/src/command-names.ts +1 -0
- package/src/commands.ts +33 -4
- package/src/config/ceiling.ts +6 -8
- package/src/config/load-workflows.ts +12 -1
- package/src/config/step-digests.ts +19 -0
- package/src/config/types.ts +17 -11
- package/src/config/validation/permissions.ts +5 -45
- package/src/config/validation/prompt.ts +3 -0
- package/src/config/validation/shared.ts +0 -2
- package/src/config/validation/step.ts +106 -2
- package/src/config/validation/workflow.ts +76 -0
- package/src/engine/create-run.ts +4 -0
- package/src/engine/gate-transitions.ts +58 -6
- package/src/engine/reconciliation-history.ts +61 -24
- package/src/engine/run-advance.ts +37 -4
- package/src/engine/run-lifecycle.ts +11 -0
- package/src/engine/run-reconciliation.ts +51 -21
- package/src/engine/run-validation.ts +236 -0
- package/src/engine/run-workflow-validation.ts +191 -0
- package/src/engine/state-types.ts +109 -4
- package/src/engine/state.ts +15 -1
- package/src/engine/step-trace.ts +495 -0
- package/src/engine/transitions.ts +1 -0
- package/src/harness/action-context.ts +31 -6
- package/src/harness/core-actions.ts +30 -1
- package/src/harness/delegation-control-actions.ts +27 -2
- package/src/harness/delegation-failure.ts +42 -9
- package/src/harness/delegation-plan.ts +96 -40
- package/src/harness/delegation-recovery-validation.ts +3 -1
- package/src/harness/delegation-response-actions.ts +73 -3
- package/src/harness/delegation-retry-policy.ts +5 -3
- package/src/harness/dependencies.ts +11 -2
- package/src/harness/gate-submission-action.ts +5 -31
- package/src/harness/lifecycle-actions.ts +4 -5
- package/src/harness/pause-actions.ts +10 -1
- package/src/harness/plannotator-result-actions.ts +20 -11
- package/src/harness/prompt-gate-actions.ts +40 -2
- package/src/harness/resume-action.ts +75 -1
- package/src/harness/start-actions.ts +77 -3
- package/src/harness/status-actions.ts +7 -17
- package/src/harness/step-effects.ts +61 -0
- package/src/harness/step-execution-actions.ts +181 -25
- package/src/harness/step-reporting.ts +168 -0
- package/src/harness/types.ts +1 -0
- package/src/harness/workspace-directory.ts +67 -0
- package/src/harness.ts +37 -9
- package/src/integrations/plannotator-requests.ts +40 -5
- package/src/integrations/subagents/child-policy-sections.ts +71 -72
- package/src/integrations/subagents/child-policy-types.ts +7 -7
- package/src/integrations/subagents/child-policy-validation.ts +10 -4
- package/src/integrations/subagents/child-runtime-completion.ts +1 -0
- package/src/integrations/subagents/child-runtime-files.ts +29 -86
- package/src/integrations/subagents/child-runtime-policy.ts +8 -33
- package/src/integrations/subagents/child-runtime.ts +3 -15
- package/src/integrations/subagents/delegated-result.ts +1 -0
- package/src/integrations/subagents/diagnostic-format.ts +5 -0
- package/src/integrations/subagents/diagnostic-types.ts +7 -1
- package/src/integrations/subagents/diagnostics.ts +2 -0
- package/src/integrations/subagents/failure-correlation.ts +52 -2
- package/src/integrations/subagents/failure-transcript.ts +60 -1
- package/src/integrations/subagents/replay-audit.ts +0 -1
- package/src/integrations/subagents/replay-safety.ts +12 -27
- package/src/integrations/subagents/session-diagnostics.ts +102 -3
- package/src/policy/bash-authorization.ts +2 -21
- package/src/policy/bash-types.ts +0 -5
- package/src/policy/bash.ts +1 -2
- package/src/policy/tool-call-authorization.ts +1 -7
- package/src/preflight.ts +0 -5
- package/src/prompt/retry-task.ts +11 -3
- package/src/prompt/step-contract.ts +10 -56
- package/src/prompt/step-sections.ts +8 -10
- package/src/prompt/step-task.ts +41 -10
- package/src/prompt/template.ts +5 -2
- package/src/runtime/completion-tool.ts +14 -0
- package/src/runtime/main-step-completion.ts +3 -0
- package/src/runtime/main-step-policy.ts +2 -1
- package/src/runtime/main-step-runtime-types.ts +9 -1
- package/src/runtime/main-step-runtime.ts +2 -0
- package/src/runtime/main-step-state.ts +6 -0
- package/src/runtime/main-step-trace.ts +45 -0
- package/src/runtime/step-result.ts +57 -0
- package/src/step-log.ts +178 -0
- package/src/workflow-doctor.ts +254 -0
- package/src/workflow-status/format-status.ts +32 -3
- package/src/workflow-status/render-board.ts +7 -4
- package/src/workflow-status/render-path.ts +51 -24
- package/src/workflow-status/render-step-detail.ts +379 -0
- package/src/workflow-status/render-summary.ts +22 -0
- package/src/workflow-status/transcript-reader.ts +231 -0
- package/src/workflow-status/types.ts +8 -0
- package/src/workflow-status/view.ts +161 -14
- package/src/workflow-status.ts +6 -0
- package/src/policy/approved-command-extraction.ts +0 -71
- package/src/policy/approved-commands.ts +0 -15
- package/src/policy/bash-read-only.ts +0 -161
- package/src/policy/restricted-git.ts +0 -30
- package/src/policy/reviewed-artifact.ts +0 -97
- package/src/policy/reviewed-command-safety.ts +0 -150
- package/src/policy/reviewed-command-shape.ts +0 -49
- package/src/policy/reviewed-repository-contract.ts +0 -138
- package/src/policy/reviewed-repository-cwd.ts +0 -102
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"tree",
|
|
49
49
|
"trust",
|
|
50
50
|
"workflow-abort",
|
|
51
|
+
"workflow-doctor",
|
|
51
52
|
"workflow-list",
|
|
52
53
|
"workflow-pause",
|
|
53
54
|
"workflow-reload",
|
|
@@ -94,6 +95,34 @@
|
|
|
94
95
|
"type": "string",
|
|
95
96
|
"pattern": "^[a-z][a-z0-9-]{0,63}$"
|
|
96
97
|
},
|
|
98
|
+
"relativePath": {
|
|
99
|
+
"type": "string",
|
|
100
|
+
"minLength": 1,
|
|
101
|
+
"maxLength": 4096,
|
|
102
|
+
"pattern": "^\\S(?:.*\\S)?$",
|
|
103
|
+
"allOf": [
|
|
104
|
+
{
|
|
105
|
+
"not": {
|
|
106
|
+
"pattern": "^/"
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"not": {
|
|
111
|
+
"pattern": "^[A-Za-z]:"
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"not": {
|
|
116
|
+
"pattern": "^\\\\"
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"not": {
|
|
121
|
+
"pattern": "\\u0000"
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
},
|
|
97
126
|
"resourceName": {
|
|
98
127
|
"type": "string",
|
|
99
128
|
"pattern": "^[A-Za-z0-9_@./:+-]+$"
|
|
@@ -114,8 +143,7 @@
|
|
|
114
143
|
"required": ["file"],
|
|
115
144
|
"properties": {
|
|
116
145
|
"file": {
|
|
117
|
-
"
|
|
118
|
-
"minLength": 1
|
|
146
|
+
"$ref": "#/$defs/relativePath"
|
|
119
147
|
}
|
|
120
148
|
}
|
|
121
149
|
}
|
|
@@ -175,32 +203,17 @@
|
|
|
175
203
|
"required": ["mode"]
|
|
176
204
|
},
|
|
177
205
|
"then": {
|
|
178
|
-
"
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
"
|
|
182
|
-
"allow": {
|
|
183
|
-
"minItems": 1
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
},
|
|
187
|
-
{
|
|
188
|
-
"required": ["approvedSources"],
|
|
189
|
-
"properties": {
|
|
190
|
-
"approvedSources": {
|
|
191
|
-
"minItems": 1
|
|
192
|
-
}
|
|
193
|
-
}
|
|
206
|
+
"required": ["allow"],
|
|
207
|
+
"properties": {
|
|
208
|
+
"allow": {
|
|
209
|
+
"minItems": 1
|
|
194
210
|
}
|
|
195
|
-
|
|
211
|
+
}
|
|
196
212
|
},
|
|
197
213
|
"else": {
|
|
198
214
|
"properties": {
|
|
199
215
|
"allow": {
|
|
200
216
|
"maxItems": 0
|
|
201
|
-
},
|
|
202
|
-
"approvedSources": {
|
|
203
|
-
"maxItems": 0
|
|
204
217
|
}
|
|
205
218
|
}
|
|
206
219
|
}
|
|
@@ -208,24 +221,13 @@
|
|
|
208
221
|
],
|
|
209
222
|
"properties": {
|
|
210
223
|
"mode": {
|
|
211
|
-
"enum": ["deny", "
|
|
224
|
+
"enum": ["deny", "allow-list", "unrestricted"]
|
|
212
225
|
},
|
|
213
226
|
"allow": {
|
|
214
227
|
"type": "array",
|
|
215
228
|
"items": {
|
|
216
229
|
"$ref": "#/$defs/bashRule"
|
|
217
230
|
}
|
|
218
|
-
},
|
|
219
|
-
"approvedSources": {
|
|
220
|
-
"type": "array",
|
|
221
|
-
"uniqueItems": true,
|
|
222
|
-
"items": {
|
|
223
|
-
"enum": [
|
|
224
|
-
"verification-worker",
|
|
225
|
-
"verification-reviewer",
|
|
226
|
-
"remote-actions"
|
|
227
|
-
]
|
|
228
|
-
}
|
|
229
231
|
}
|
|
230
232
|
}
|
|
231
233
|
},
|
|
@@ -546,16 +548,54 @@
|
|
|
546
548
|
"type": "integer",
|
|
547
549
|
"minimum": 1000,
|
|
548
550
|
"maximum": 30000,
|
|
549
|
-
"default":
|
|
551
|
+
"default": 30000
|
|
550
552
|
}
|
|
551
553
|
}
|
|
552
554
|
}
|
|
553
555
|
]
|
|
554
556
|
},
|
|
557
|
+
"workspace": {
|
|
558
|
+
"type": "object",
|
|
559
|
+
"additionalProperties": false,
|
|
560
|
+
"required": ["bindOn"],
|
|
561
|
+
"properties": {
|
|
562
|
+
"bindOn": {
|
|
563
|
+
"type": "array",
|
|
564
|
+
"minItems": 1,
|
|
565
|
+
"uniqueItems": true,
|
|
566
|
+
"items": {
|
|
567
|
+
"$ref": "#/$defs/identifier"
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
"allowedRoots": {
|
|
571
|
+
"type": "array",
|
|
572
|
+
"minItems": 1,
|
|
573
|
+
"maxItems": 32,
|
|
574
|
+
"uniqueItems": true,
|
|
575
|
+
"default": ["."],
|
|
576
|
+
"items": {
|
|
577
|
+
"$ref": "#/$defs/relativePath"
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
},
|
|
555
582
|
"step": {
|
|
556
583
|
"type": "object",
|
|
557
584
|
"additionalProperties": false,
|
|
558
585
|
"required": ["prompt", "transitions"],
|
|
586
|
+
"allOf": [
|
|
587
|
+
{
|
|
588
|
+
"if": {
|
|
589
|
+
"required": ["workspace"]
|
|
590
|
+
},
|
|
591
|
+
"then": {
|
|
592
|
+
"required": ["subagent"],
|
|
593
|
+
"not": {
|
|
594
|
+
"required": ["gate"]
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
],
|
|
559
599
|
"properties": {
|
|
560
600
|
"title": {
|
|
561
601
|
"type": "string",
|
|
@@ -586,6 +626,9 @@
|
|
|
586
626
|
},
|
|
587
627
|
"gate": {
|
|
588
628
|
"$ref": "#/$defs/gate"
|
|
629
|
+
},
|
|
630
|
+
"workspace": {
|
|
631
|
+
"$ref": "#/$defs/workspace"
|
|
589
632
|
}
|
|
590
633
|
}
|
|
591
634
|
}
|
package/src/command-names.ts
CHANGED
package/src/commands.ts
CHANGED
|
@@ -12,6 +12,11 @@ export type WorkflowCommandController = {
|
|
|
12
12
|
readonly workflowIds: () => ReadonlyArray<string>;
|
|
13
13
|
/** Lists loaded workflows. */
|
|
14
14
|
readonly list: (context: ExtensionCommandContext) => Promise<void>;
|
|
15
|
+
/** Diagnoses workflow graph liveness. */
|
|
16
|
+
readonly doctor: (
|
|
17
|
+
workflowId: string,
|
|
18
|
+
context: ExtensionCommandContext,
|
|
19
|
+
) => Promise<void>;
|
|
15
20
|
/** Starts a workflow. */
|
|
16
21
|
readonly start: (
|
|
17
22
|
workflowId: string,
|
|
@@ -23,8 +28,11 @@ export type WorkflowCommandController = {
|
|
|
23
28
|
reason: string,
|
|
24
29
|
context: ExtensionCommandContext,
|
|
25
30
|
) => Promise<void>;
|
|
26
|
-
/** Resumes the paused workflow. */
|
|
27
|
-
readonly resume: (
|
|
31
|
+
/** Resumes the paused workflow with optional user guidance. */
|
|
32
|
+
readonly resume: (
|
|
33
|
+
input: string,
|
|
34
|
+
context: ExtensionCommandContext,
|
|
35
|
+
) => Promise<void>;
|
|
28
36
|
/** Aborts the active workflow. */
|
|
29
37
|
readonly abort: (
|
|
30
38
|
reason: string,
|
|
@@ -98,6 +106,25 @@ export function createHarnessCommands(
|
|
|
98
106
|
handler: async (_args, context) => controller.list(context),
|
|
99
107
|
},
|
|
100
108
|
},
|
|
109
|
+
{
|
|
110
|
+
name: 'workflow-doctor',
|
|
111
|
+
options: {
|
|
112
|
+
description:
|
|
113
|
+
'Check workflow completion paths, unreachable steps, and cycles',
|
|
114
|
+
getArgumentCompletions: (prefix) => {
|
|
115
|
+
const completions = controller
|
|
116
|
+
.workflowIds()
|
|
117
|
+
.filter((workflowId) => workflowId.startsWith(prefix))
|
|
118
|
+
.map((workflowId) => ({
|
|
119
|
+
value: workflowId,
|
|
120
|
+
label: workflowId,
|
|
121
|
+
}));
|
|
122
|
+
return completions.length > 0 ? completions : null;
|
|
123
|
+
},
|
|
124
|
+
handler: async (args, context) =>
|
|
125
|
+
controller.doctor(args.trim(), context),
|
|
126
|
+
},
|
|
127
|
+
},
|
|
101
128
|
createStartCommand(controller),
|
|
102
129
|
{
|
|
103
130
|
name: 'workflow-pause',
|
|
@@ -110,8 +137,10 @@ export function createHarnessCommands(
|
|
|
110
137
|
{
|
|
111
138
|
name: 'workflow-resume',
|
|
112
139
|
options: {
|
|
113
|
-
description:
|
|
114
|
-
|
|
140
|
+
description:
|
|
141
|
+
'Reload configuration and resume: /workflow-resume [guidance]',
|
|
142
|
+
handler: async (input, context) =>
|
|
143
|
+
controller.resume(input.trim(), context),
|
|
115
144
|
},
|
|
116
145
|
},
|
|
117
146
|
{
|
package/src/config/ceiling.ts
CHANGED
|
@@ -28,17 +28,10 @@ function bashWithinCeiling(
|
|
|
28
28
|
if (ceiling.mode === 'unrestricted') return true;
|
|
29
29
|
if (requested.mode === 'deny') return true;
|
|
30
30
|
if (ceiling.mode === 'deny') return false;
|
|
31
|
-
if (ceiling.mode === 'read-only') return requested.mode === 'read-only';
|
|
32
31
|
if (requested.mode !== 'allow-list') return false;
|
|
33
32
|
|
|
34
33
|
const allowedRules = new Set(ceiling.allow.map(ruleKey));
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
requested.allow.every((rule) => allowedRules.has(ruleKey(rule))) &&
|
|
38
|
-
(requested.approvedSources ?? []).every((source) =>
|
|
39
|
-
allowedSources.has(source),
|
|
40
|
-
)
|
|
41
|
-
);
|
|
34
|
+
return requested.allow.every((rule) => allowedRules.has(ruleKey(rule)));
|
|
42
35
|
}
|
|
43
36
|
|
|
44
37
|
function turnBudgetErrors(
|
|
@@ -124,6 +117,11 @@ export function checkWorkflowAgainstCeiling(
|
|
|
124
117
|
return Object.entries(workflow.steps).flatMap(([stepId, step]) => {
|
|
125
118
|
const path = `workflow.steps.${stepId}.permissions`;
|
|
126
119
|
return [
|
|
120
|
+
...(Object.hasOwn(step, 'workspace')
|
|
121
|
+
? [
|
|
122
|
+
`workflow.steps.${stepId}.workspace: workspace binding is unavailable to project workflows`,
|
|
123
|
+
]
|
|
124
|
+
: []),
|
|
127
125
|
...step.permissions.tools
|
|
128
126
|
.filter((tool) => !ceiling.tools.includes(tool))
|
|
129
127
|
.map(
|
|
@@ -2,6 +2,10 @@ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
|
2
2
|
import { digest } from '../digest.ts';
|
|
3
3
|
import { createDiagnostic, errorCode, errorMessage } from './diagnostics.ts';
|
|
4
4
|
import type { ConfigFileSystem, LoadedDirectory } from './load-types.ts';
|
|
5
|
+
import {
|
|
6
|
+
digestWorkflowStep,
|
|
7
|
+
digestWorkflowStepStructure,
|
|
8
|
+
} from './step-digests.ts';
|
|
5
9
|
import type {
|
|
6
10
|
LoadedWorkflow,
|
|
7
11
|
WorkflowDefinition,
|
|
@@ -81,7 +85,13 @@ async function loadWorkflowFile(
|
|
|
81
85
|
const stepDigests = Object.fromEntries(
|
|
82
86
|
Object.entries(definition.steps).map(([stepId, step]) => [
|
|
83
87
|
stepId,
|
|
84
|
-
|
|
88
|
+
digestWorkflowStep(step, prompts[stepId] ?? ''),
|
|
89
|
+
]),
|
|
90
|
+
);
|
|
91
|
+
const stepStructuralDigests = Object.fromEntries(
|
|
92
|
+
Object.entries(definition.steps).map(([stepId, step]) => [
|
|
93
|
+
stepId,
|
|
94
|
+
digestWorkflowStepStructure(step),
|
|
85
95
|
]),
|
|
86
96
|
);
|
|
87
97
|
return {
|
|
@@ -89,6 +99,7 @@ async function loadWorkflowFile(
|
|
|
89
99
|
prompts,
|
|
90
100
|
digest: digest({ definition, prompts }),
|
|
91
101
|
stepDigests,
|
|
102
|
+
stepStructuralDigests,
|
|
92
103
|
sourcePath,
|
|
93
104
|
sourceKind,
|
|
94
105
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { digest } from '../digest.ts';
|
|
2
|
+
import type { WorkflowStep } from './types.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Digests a normalized workflow step together with its resolved prompt text.
|
|
6
|
+
*/
|
|
7
|
+
export function digestWorkflowStep(step: WorkflowStep, prompt: string): string {
|
|
8
|
+
return digest({ step, prompt });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Digests every normalized workflow-step field except the prompt specification.
|
|
13
|
+
*/
|
|
14
|
+
export function digestWorkflowStepStructure(step: WorkflowStep): string {
|
|
15
|
+
const structure = Object.fromEntries(
|
|
16
|
+
Object.entries(step).filter(([key]) => key !== 'prompt'),
|
|
17
|
+
);
|
|
18
|
+
return digest(structure);
|
|
19
|
+
}
|
package/src/config/types.ts
CHANGED
|
@@ -4,14 +4,14 @@ export const WORKFLOW_SCHEMA_VERSION = 1 as const;
|
|
|
4
4
|
export const DEFAULT_STATUS_SHORTCUT = 'ctrl+alt+w' as const satisfies KeyId;
|
|
5
5
|
export const SUBAGENT_RUNTIME_NAME_PATTERN =
|
|
6
6
|
/^[a-z0-9][a-z0-9-]*(?:\.[a-z0-9][a-z0-9-]*)*$/;
|
|
7
|
+
export const MAX_WORKSPACE_PATH_CHARS = 4_096;
|
|
8
|
+
export const MAX_WORKSPACE_ALLOWED_ROOTS = 32;
|
|
7
9
|
|
|
8
10
|
export const TERMINAL_TARGETS = ['$done', '$pause'] as const;
|
|
9
11
|
export type TerminalTarget = (typeof TERMINAL_TARGETS)[number];
|
|
10
12
|
export type StepTarget = string;
|
|
11
13
|
|
|
12
|
-
export type BashMode = 'deny' | '
|
|
13
|
-
export type BashApprovalSource =
|
|
14
|
-
'verification-worker' | 'verification-reviewer' | 'remote-actions';
|
|
14
|
+
export type BashMode = 'deny' | 'allow-list' | 'unrestricted';
|
|
15
15
|
|
|
16
16
|
export type BashRule = {
|
|
17
17
|
readonly executable: string;
|
|
@@ -21,11 +21,6 @@ export type BashRule = {
|
|
|
21
21
|
export type BashPermission = {
|
|
22
22
|
readonly mode: BashMode;
|
|
23
23
|
readonly allow: ReadonlyArray<BashRule>;
|
|
24
|
-
/**
|
|
25
|
-
* Exact commands extracted from the most recent human-approved artifact.
|
|
26
|
-
* They supplement `allow` only inside the correlated step execution.
|
|
27
|
-
*/
|
|
28
|
-
readonly approvedSources?: ReadonlyArray<BashApprovalSource>;
|
|
29
24
|
};
|
|
30
25
|
|
|
31
26
|
export type StepPermissions = {
|
|
@@ -70,9 +65,9 @@ export type StepSubagent = {
|
|
|
70
65
|
readonly toolBudget?: SubagentToolBudget;
|
|
71
66
|
readonly artifacts: boolean;
|
|
72
67
|
/**
|
|
73
|
-
* Authorizes
|
|
74
|
-
*
|
|
75
|
-
*
|
|
68
|
+
* Authorizes bounded automatic recovery when Bash can execute commands.
|
|
69
|
+
* Every failed attempt must still have a complete, mutation-safe replay
|
|
70
|
+
* audit.
|
|
76
71
|
*/
|
|
77
72
|
readonly retryToolFailures: boolean;
|
|
78
73
|
};
|
|
@@ -97,6 +92,13 @@ export type PlannotatorGate = GateDefinition & {
|
|
|
97
92
|
|
|
98
93
|
export type WorkflowGate = PromptGate | PlannotatorGate;
|
|
99
94
|
|
|
95
|
+
export type StepWorkspaceBinding = {
|
|
96
|
+
/** Outcomes whose result establishes the workspace for later steps. */
|
|
97
|
+
readonly bindOn: ReadonlyArray<string>;
|
|
98
|
+
/** Paths relative to the run-start directory that may contain the workspace. */
|
|
99
|
+
readonly allowedRoots: ReadonlyArray<string>;
|
|
100
|
+
};
|
|
101
|
+
|
|
100
102
|
export type WorkflowStep = {
|
|
101
103
|
readonly title: string;
|
|
102
104
|
readonly prompt: PromptSpec;
|
|
@@ -106,6 +108,8 @@ export type WorkflowStep = {
|
|
|
106
108
|
readonly requires: StepRequirements;
|
|
107
109
|
readonly transitions: Readonly<Record<string, StepTarget>>;
|
|
108
110
|
readonly gate?: WorkflowGate;
|
|
111
|
+
/** Optional one-time workspace binding produced by this delegated step. */
|
|
112
|
+
readonly workspace?: StepWorkspaceBinding;
|
|
109
113
|
};
|
|
110
114
|
|
|
111
115
|
export type WorkflowDefinition = {
|
|
@@ -126,6 +130,8 @@ export type LoadedWorkflow = {
|
|
|
126
130
|
readonly prompts: Readonly<Record<string, string>>;
|
|
127
131
|
readonly digest: string;
|
|
128
132
|
readonly stepDigests: Readonly<Record<string, string>>;
|
|
133
|
+
/** Per-step digest excluding only the prompt specification and resolved text. */
|
|
134
|
+
readonly stepStructuralDigests: Readonly<Record<string, string>>;
|
|
129
135
|
readonly sourcePath: string;
|
|
130
136
|
readonly sourceKind: WorkflowSourceKind;
|
|
131
137
|
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
EMPTY_PERMISSIONS,
|
|
3
|
-
type BashApprovalSource,
|
|
4
3
|
type BashMode,
|
|
5
4
|
type BashPermission,
|
|
6
5
|
type BashRule,
|
|
@@ -8,7 +7,6 @@ import {
|
|
|
8
7
|
type StepRequirements,
|
|
9
8
|
} from '../types.ts';
|
|
10
9
|
import {
|
|
11
|
-
BASH_APPROVAL_SOURCE_PATTERN,
|
|
12
10
|
EXECUTABLE_PATTERN,
|
|
13
11
|
isJsonObject,
|
|
14
12
|
MCP_SELECTOR_PATTERN,
|
|
@@ -20,16 +18,6 @@ import {
|
|
|
20
18
|
type ValidationErrors,
|
|
21
19
|
} from './shared.ts';
|
|
22
20
|
|
|
23
|
-
const BASH_APPROVAL_SOURCES = [
|
|
24
|
-
'verification-worker',
|
|
25
|
-
'verification-reviewer',
|
|
26
|
-
'remote-actions',
|
|
27
|
-
] as const satisfies ReadonlyArray<BashApprovalSource>;
|
|
28
|
-
|
|
29
|
-
function isBashApprovalSource(value: string): value is BashApprovalSource {
|
|
30
|
-
return BASH_APPROVAL_SOURCES.some((source) => source === value);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
21
|
function emptyPermissions(): StepPermissions {
|
|
34
22
|
return {
|
|
35
23
|
tools: [],
|
|
@@ -129,18 +117,13 @@ function parseBashPermission(
|
|
|
129
117
|
errors.push(`${path}: expected an object`);
|
|
130
118
|
return { ...EMPTY_PERMISSIONS.bash, allow: [] };
|
|
131
119
|
}
|
|
132
|
-
rejectUnknownKeys(value, ['mode', 'allow'
|
|
120
|
+
rejectUnknownKeys(value, ['mode', 'allow'], path, errors);
|
|
133
121
|
|
|
134
122
|
const mode = readString(value.mode, `${path}.mode`, errors);
|
|
135
123
|
const isValidMode =
|
|
136
|
-
mode === 'deny' ||
|
|
137
|
-
mode === 'read-only' ||
|
|
138
|
-
mode === 'allow-list' ||
|
|
139
|
-
mode === 'unrestricted';
|
|
124
|
+
mode === 'deny' || mode === 'allow-list' || mode === 'unrestricted';
|
|
140
125
|
if (!isValidMode) {
|
|
141
|
-
errors.push(
|
|
142
|
-
`${path}.mode: expected deny, read-only, allow-list, or unrestricted`,
|
|
143
|
-
);
|
|
126
|
+
errors.push(`${path}.mode: expected deny, allow-list, or unrestricted`);
|
|
144
127
|
}
|
|
145
128
|
|
|
146
129
|
const allow = Array.isArray(value.allow)
|
|
@@ -152,40 +135,17 @@ function parseBashPermission(
|
|
|
152
135
|
errors.push(`${path}.allow: expected an array`);
|
|
153
136
|
}
|
|
154
137
|
|
|
155
|
-
const approvedSources = (
|
|
156
|
-
value.approvedSources === undefined
|
|
157
|
-
? []
|
|
158
|
-
: readStringList(
|
|
159
|
-
value.approvedSources,
|
|
160
|
-
`${path}.approvedSources`,
|
|
161
|
-
errors,
|
|
162
|
-
BASH_APPROVAL_SOURCE_PATTERN,
|
|
163
|
-
)
|
|
164
|
-
).filter(isBashApprovalSource);
|
|
165
|
-
|
|
166
138
|
const normalizedMode: BashMode = isValidMode ? mode : 'deny';
|
|
167
139
|
if (normalizedMode !== 'allow-list' && allow.length > 0) {
|
|
168
140
|
errors.push(`${path}.allow: only valid when mode is "allow-list"`);
|
|
169
141
|
}
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
allow.length === 0 &&
|
|
173
|
-
approvedSources.length === 0
|
|
174
|
-
) {
|
|
175
|
-
errors.push(
|
|
176
|
-
`${path}: allow-list mode requires an allow rule or an approved command source`,
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
if (normalizedMode !== 'allow-list' && approvedSources.length > 0) {
|
|
180
|
-
errors.push(
|
|
181
|
-
`${path}.approvedSources: only valid when mode is "allow-list"`,
|
|
182
|
-
);
|
|
142
|
+
if (normalizedMode === 'allow-list' && allow.length === 0) {
|
|
143
|
+
errors.push(`${path}: allow-list mode requires an allow rule`);
|
|
183
144
|
}
|
|
184
145
|
|
|
185
146
|
return {
|
|
186
147
|
mode: normalizedMode,
|
|
187
148
|
allow,
|
|
188
|
-
...(approvedSources.length > 0 ? { approvedSources } : {}),
|
|
189
149
|
};
|
|
190
150
|
}
|
|
191
151
|
|
|
@@ -12,8 +12,6 @@ export const TOOL_PATTERN = /^[A-Za-z0-9_.:-]+$/;
|
|
|
12
12
|
export const RESOURCE_SELECTOR_PATTERN = /^[A-Za-z0-9_@./:+-]+$/;
|
|
13
13
|
export const MCP_SELECTOR_PATTERN = /^[A-Za-z0-9_.:-]+(?:\/[A-Za-z0-9_.:-]+)?$/;
|
|
14
14
|
export const EXECUTABLE_PATTERN = /^[A-Za-z0-9_./+-]+$/;
|
|
15
|
-
export const BASH_APPROVAL_SOURCE_PATTERN =
|
|
16
|
-
/^(verification-worker|verification-reviewer|remote-actions)$/;
|
|
17
15
|
|
|
18
16
|
type StringOptions = {
|
|
19
17
|
readonly pattern?: RegExp;
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { isAbsolute, win32 } from 'node:path';
|
|
2
|
+
import {
|
|
3
|
+
MAX_WORKSPACE_ALLOWED_ROOTS,
|
|
4
|
+
MAX_WORKSPACE_PATH_CHARS,
|
|
5
|
+
type PromptSpec,
|
|
6
|
+
type StepWorkspaceBinding,
|
|
7
|
+
type WorkflowGate,
|
|
8
|
+
type WorkflowStep,
|
|
9
|
+
} from '../types.ts';
|
|
2
10
|
import { parsePermissions, parseRequirements } from './permissions.ts';
|
|
3
11
|
import {
|
|
4
12
|
isJsonObject,
|
|
5
13
|
OUTCOME_PATTERN,
|
|
6
14
|
readInteger,
|
|
7
15
|
readString,
|
|
16
|
+
readStringList,
|
|
8
17
|
rejectUnknownKeys,
|
|
9
18
|
type ValidationErrors,
|
|
10
19
|
} from './shared.ts';
|
|
@@ -135,7 +144,7 @@ function parseGate(
|
|
|
135
144
|
rejectedOutcome,
|
|
136
145
|
timeoutMs: readInteger(
|
|
137
146
|
value.timeoutMs,
|
|
138
|
-
|
|
147
|
+
30_000,
|
|
139
148
|
`${path}.timeoutMs`,
|
|
140
149
|
errors,
|
|
141
150
|
{ min: 1_000, max: 30_000 },
|
|
@@ -143,6 +152,85 @@ function parseGate(
|
|
|
143
152
|
};
|
|
144
153
|
}
|
|
145
154
|
|
|
155
|
+
function parseWorkspaceRoots(
|
|
156
|
+
value: unknown,
|
|
157
|
+
path: string,
|
|
158
|
+
errors: ValidationErrors,
|
|
159
|
+
): Array<string> {
|
|
160
|
+
if (value === undefined) return ['.'];
|
|
161
|
+
if (!Array.isArray(value)) {
|
|
162
|
+
errors.push(`${path}: expected an array of relative paths`);
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
if (value.length > MAX_WORKSPACE_ALLOWED_ROOTS) {
|
|
166
|
+
errors.push(
|
|
167
|
+
`${path}: at most ${MAX_WORKSPACE_ALLOWED_ROOTS} relative paths are allowed`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const roots = value
|
|
172
|
+
.slice(0, MAX_WORKSPACE_ALLOWED_ROOTS)
|
|
173
|
+
.reduce<Array<string>>((result, item, index) => {
|
|
174
|
+
const itemPath = `${path}[${index}]`;
|
|
175
|
+
if (typeof item !== 'string' || !item || item.trim() !== item) {
|
|
176
|
+
errors.push(`${itemPath}: expected a non-empty relative path`);
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
const root = item;
|
|
180
|
+
if (root.length > MAX_WORKSPACE_PATH_CHARS) {
|
|
181
|
+
errors.push(
|
|
182
|
+
`${itemPath}: path exceeds ${MAX_WORKSPACE_PATH_CHARS} characters`,
|
|
183
|
+
);
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
if (
|
|
187
|
+
root.includes('\0') ||
|
|
188
|
+
isAbsolute(root) ||
|
|
189
|
+
win32.parse(root).root !== ''
|
|
190
|
+
) {
|
|
191
|
+
errors.push(`${itemPath}: expected a relative path`);
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
if (result.includes(root)) {
|
|
195
|
+
errors.push(`${itemPath}: duplicate value "${root}"`);
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
return [...result, root];
|
|
199
|
+
}, []);
|
|
200
|
+
if (roots.length === 0) {
|
|
201
|
+
errors.push(`${path}: at least one relative path is required`);
|
|
202
|
+
}
|
|
203
|
+
return roots;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function parseWorkspace(
|
|
207
|
+
value: unknown,
|
|
208
|
+
path: string,
|
|
209
|
+
errors: ValidationErrors,
|
|
210
|
+
): StepWorkspaceBinding | undefined {
|
|
211
|
+
if (value === undefined) return undefined;
|
|
212
|
+
if (!isJsonObject(value)) {
|
|
213
|
+
errors.push(`${path}: expected an object`);
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
rejectUnknownKeys(value, ['bindOn', 'allowedRoots'], path, errors);
|
|
217
|
+
const bindOn = readStringList(
|
|
218
|
+
value.bindOn,
|
|
219
|
+
`${path}.bindOn`,
|
|
220
|
+
errors,
|
|
221
|
+
OUTCOME_PATTERN,
|
|
222
|
+
);
|
|
223
|
+
if (bindOn.length === 0) {
|
|
224
|
+
errors.push(`${path}.bindOn: at least one outcome is required`);
|
|
225
|
+
}
|
|
226
|
+
const allowedRoots = parseWorkspaceRoots(
|
|
227
|
+
value.allowedRoots,
|
|
228
|
+
`${path}.allowedRoots`,
|
|
229
|
+
errors,
|
|
230
|
+
);
|
|
231
|
+
return { bindOn, allowedRoots };
|
|
232
|
+
}
|
|
233
|
+
|
|
146
234
|
/** Parse one workflow step and validate relationships within that step. */
|
|
147
235
|
export function parseWorkflowStep(
|
|
148
236
|
value: unknown,
|
|
@@ -164,6 +252,7 @@ export function parseWorkflowStep(
|
|
|
164
252
|
'requires',
|
|
165
253
|
'transitions',
|
|
166
254
|
'gate',
|
|
255
|
+
'workspace',
|
|
167
256
|
],
|
|
168
257
|
path,
|
|
169
258
|
errors,
|
|
@@ -196,6 +285,11 @@ export function parseWorkflowStep(
|
|
|
196
285
|
errors,
|
|
197
286
|
);
|
|
198
287
|
const gate = parseGate(value.gate, `${path}.gate`, errors);
|
|
288
|
+
const workspace = parseWorkspace(
|
|
289
|
+
value.workspace,
|
|
290
|
+
`${path}.workspace`,
|
|
291
|
+
errors,
|
|
292
|
+
);
|
|
199
293
|
|
|
200
294
|
if (gate) {
|
|
201
295
|
if (!Object.hasOwn(transitions, gate.approvedOutcome)) {
|
|
@@ -214,6 +308,15 @@ export function parseWorkflowStep(
|
|
|
214
308
|
);
|
|
215
309
|
}
|
|
216
310
|
}
|
|
311
|
+
if (workspace) {
|
|
312
|
+
workspace.bindOn.forEach((outcome) => {
|
|
313
|
+
if (!Object.hasOwn(transitions, outcome)) {
|
|
314
|
+
errors.push(
|
|
315
|
+
`${path}.workspace.bindOn: unknown transition outcome "${outcome}"`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
217
320
|
|
|
218
321
|
if (!title || !prompt) return undefined;
|
|
219
322
|
return {
|
|
@@ -224,5 +327,6 @@ export function parseWorkflowStep(
|
|
|
224
327
|
requires,
|
|
225
328
|
transitions,
|
|
226
329
|
...(gate ? { gate } : {}),
|
|
330
|
+
...(workspace ? { workspace } : {}),
|
|
227
331
|
};
|
|
228
332
|
}
|