@wichayutdew/pi-workflows 0.2.3 → 1.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 +66 -38
- package/dist/index.js +5903 -4782
- package/package.json +1 -1
- package/src/command-names.ts +7 -1
- package/src/commands.ts +126 -52
- package/src/config/catalog.ts +162 -0
- package/src/config/ceiling.ts +116 -116
- package/src/config/command-conflicts.ts +4 -4
- package/src/config/diagnostics.ts +28 -0
- package/src/config/load-settings.ts +37 -0
- package/src/config/load-types.ts +53 -0
- package/src/config/load-workflows.ts +135 -0
- package/src/config/load.ts +43 -311
- package/src/config/types.ts +137 -135
- package/src/config/validate.ts +12 -1261
- package/src/config/validation/permissions.ts +291 -0
- package/src/config/validation/prompt.ts +19 -0
- package/src/config/validation/settings.ts +122 -0
- package/src/config/validation/shared.ts +129 -0
- package/src/config/validation/shortcut.ts +118 -0
- package/src/config/validation/step.ts +228 -0
- package/src/config/validation/subagent.ts +288 -0
- package/src/config/validation/workflow.ts +156 -0
- package/src/config/yaml.ts +29 -0
- package/src/digest.ts +38 -6
- package/src/engine/checkpoint.ts +13 -9
- package/src/engine/create-run.ts +42 -0
- package/src/engine/gate-transitions.ts +184 -0
- package/src/engine/reconciliation-history.ts +88 -0
- package/src/engine/resume.ts +19 -7
- package/src/engine/run-advance.ts +122 -0
- package/src/engine/run-lifecycle.ts +124 -0
- package/src/engine/run-reconciliation.ts +116 -0
- package/src/engine/run-validation.ts +128 -0
- package/src/engine/state-types.ts +64 -0
- package/src/engine/state.ts +10 -193
- package/src/engine/transition-helpers.ts +28 -0
- package/src/engine/transition-types.ts +8 -0
- package/src/engine/transitions.ts +17 -471
- package/src/harness/action-context.ts +181 -0
- package/src/harness/catalog.ts +45 -0
- package/src/harness/context-idle.ts +21 -0
- package/src/harness/core-actions.ts +299 -0
- package/src/harness/delegation-control-actions.ts +250 -0
- package/src/harness/delegation-failure.ts +215 -0
- package/src/harness/delegation-plan.ts +219 -0
- package/src/harness/delegation-recovery-validation.ts +159 -0
- package/src/harness/delegation-response-actions.ts +295 -0
- package/src/harness/delegation-retry-policy.ts +118 -0
- package/src/harness/dependencies.ts +165 -0
- package/src/harness/gate-submission-action.ts +154 -0
- package/src/harness/lifecycle-actions.ts +154 -0
- package/src/harness/pause-actions.ts +120 -0
- package/src/harness/plannotator-result-actions.ts +123 -0
- package/src/harness/prompt-gate-actions.ts +270 -0
- package/src/harness/resume-action.ts +261 -0
- package/src/harness/start-actions.ts +183 -0
- package/src/harness/status-actions.ts +191 -0
- package/src/harness/step-execution-actions.ts +283 -0
- package/src/harness/types.ts +76 -0
- package/src/harness.ts +237 -2201
- package/src/index.ts +73 -11
- package/src/integrations/plannotator-requests.ts +127 -0
- package/src/integrations/plannotator-responses.ts +141 -0
- package/src/integrations/plannotator-types.ts +44 -0
- package/src/integrations/plannotator.ts +16 -235
- package/src/integrations/prompt-gate.ts +17 -10
- package/src/integrations/subagents/child-policy-envelope.ts +119 -0
- package/src/integrations/subagents/child-policy-paths.ts +66 -0
- package/src/integrations/subagents/child-policy-sections.ts +174 -0
- package/src/integrations/subagents/child-policy-types.ts +32 -0
- package/src/integrations/subagents/child-policy-validation.ts +163 -0
- package/src/integrations/subagents/child-runtime-completion.ts +56 -0
- package/src/integrations/subagents/child-runtime-dependencies.ts +47 -0
- package/src/integrations/subagents/child-runtime-files.ts +156 -0
- package/src/integrations/subagents/child-runtime-policy.ts +83 -0
- package/src/integrations/subagents/child-runtime-types.ts +30 -0
- package/src/integrations/subagents/child-runtime.ts +140 -301
- package/src/integrations/subagents/client-delegation.ts +181 -0
- package/src/integrations/subagents/client-messages.ts +66 -0
- package/src/integrations/subagents/client-types.ts +36 -0
- package/src/integrations/subagents/client.ts +127 -219
- package/src/integrations/subagents/delegated-result.ts +31 -0
- package/src/integrations/subagents/diagnostic-format.ts +40 -0
- package/src/integrations/subagents/diagnostic-text.ts +114 -0
- package/src/integrations/subagents/diagnostic-types.ts +77 -0
- package/src/integrations/subagents/diagnostics.ts +24 -799
- package/src/integrations/subagents/failure-correlation.ts +235 -0
- package/src/integrations/subagents/failure-transcript.ts +192 -0
- package/src/integrations/subagents/hidden-bash-failure.ts +98 -0
- package/src/integrations/subagents/protocol-events.ts +27 -0
- package/src/integrations/subagents/protocol.ts +30 -375
- package/src/integrations/subagents/replay-audit.ts +147 -0
- package/src/integrations/subagents/replay-safety.ts +82 -0
- package/src/integrations/subagents/session-diagnostics.ts +258 -0
- package/src/policy/approved-command-extraction.ts +71 -0
- package/src/policy/approved-commands.ts +15 -432
- package/src/policy/bash-authorization.ts +86 -0
- package/src/policy/bash-read-only.ts +161 -0
- package/src/policy/bash-types.ts +20 -0
- package/src/policy/bash.ts +4 -346
- package/src/policy/completion-batch.ts +21 -16
- package/src/policy/immutable-input.ts +5 -2
- package/src/policy/mcp-authorization.ts +71 -0
- package/src/policy/restricted-command.ts +141 -0
- package/src/policy/restricted-git.ts +30 -0
- package/src/policy/reviewed-artifact.ts +97 -0
- package/src/policy/reviewed-command-safety.ts +150 -0
- package/src/policy/reviewed-command-shape.ts +49 -0
- package/src/policy/reviewed-repository-contract.ts +138 -0
- package/src/policy/reviewed-repository-cwd.ts +102 -0
- package/src/policy/tool-call-authorization.ts +58 -0
- package/src/policy/tool-selection.ts +72 -0
- package/src/policy/tool-types.ts +14 -0
- package/src/policy/tools.ts +11 -150
- package/src/preflight.ts +96 -61
- package/src/prompt/main-workflow-notice.ts +41 -0
- package/src/prompt/retry-task.ts +83 -0
- package/src/prompt/step-contract.ts +101 -0
- package/src/prompt/step-sections.ts +75 -0
- package/src/prompt/step-task.ts +156 -0
- package/src/prompt/template.ts +81 -0
- package/src/prompt.ts +10 -245
- package/src/runtime/completion-tool.ts +3 -0
- package/src/runtime/main-step-completion.ts +78 -0
- package/src/runtime/main-step-lifecycle.ts +37 -0
- package/src/runtime/main-step-policy.ts +84 -0
- package/src/runtime/main-step-runtime-types.ts +64 -0
- package/src/runtime/main-step-runtime.ts +129 -200
- package/src/runtime/main-step-state.ts +85 -0
- package/src/runtime/serial-task-queue.ts +48 -13
- package/src/runtime/step-result.ts +48 -27
- package/src/workflow-list.ts +15 -6
- package/src/workflow-status/format-status.ts +52 -0
- package/src/workflow-status/formatting.ts +178 -0
- package/src/workflow-status/layout.ts +109 -0
- package/src/workflow-status/render-board.ts +101 -0
- package/src/workflow-status/render-path.ts +123 -0
- package/src/workflow-status/render-summary.ts +144 -0
- package/src/workflow-status/types.ts +40 -0
- package/src/workflow-status/view.ts +273 -0
- package/src/workflow-status.ts +20 -820
|
@@ -1,375 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from '
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
} from '
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
export
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
export const SUBAGENT_DELEGATION_CANCEL_EVENT =
|
|
32
|
-
'prompt-template:subagent:cancel';
|
|
33
|
-
|
|
34
|
-
const CHILD_POLICY_OPEN = '<pi-workflows-policy-v1>';
|
|
35
|
-
const CHILD_POLICY_CLOSE = '</pi-workflows-policy-v1>';
|
|
36
|
-
const UPSTREAM_TASK_PREFIX = 'Task: ';
|
|
37
|
-
const UPSTREAM_TASK_FILE_OPEN = '<file name="';
|
|
38
|
-
const UPSTREAM_TASK_FILE_HEADER_CLOSE = '">\n';
|
|
39
|
-
const UPSTREAM_TASK_FILE_CLOSE = '\n</file>\n';
|
|
40
|
-
const UPSTREAM_TASK_DIRECTORY_PREFIX = 'pi-subagent-';
|
|
41
|
-
const POLICY_DIGEST_PATTERN = /^[a-f0-9]{64}$/;
|
|
42
|
-
const CAPABILITY_TOKEN_PATTERN = /^[a-f0-9]{64}$/;
|
|
43
|
-
const RESULT_FILE_NAME = 'result.json';
|
|
44
|
-
const CAPABILITY_FILE_NAME = 'capability';
|
|
45
|
-
const RESULT_DIRECTORY_PREFIX = 'pi-workflows-step-';
|
|
46
|
-
|
|
47
|
-
export type SubagentDelegationRequest = UpstreamDelegationRequest;
|
|
48
|
-
export type SubagentDelegationUpdate = UpstreamDelegationUpdate;
|
|
49
|
-
export type SubagentDelegationStatus = UpstreamDelegationStatus;
|
|
50
|
-
export type SubagentDelegationResponse = UpstreamDelegationResponse;
|
|
51
|
-
|
|
52
|
-
export interface ChildStepPolicy {
|
|
53
|
-
version: 1;
|
|
54
|
-
requestId: string;
|
|
55
|
-
agent: string;
|
|
56
|
-
workflowId: string;
|
|
57
|
-
runId: string;
|
|
58
|
-
stepId: string;
|
|
59
|
-
stepTitle: string;
|
|
60
|
-
policyDigest: string;
|
|
61
|
-
capabilityPath: string;
|
|
62
|
-
capabilityToken: string;
|
|
63
|
-
resultPath: string;
|
|
64
|
-
permissions: StepPermissions;
|
|
65
|
-
/** Exact Bash command strings extracted from a reviewed gate artifact. */
|
|
66
|
-
approvedBashCommands?: string[];
|
|
67
|
-
/** Reviewed repository root that file mutations must remain inside. */
|
|
68
|
-
repositoryCwd?: string;
|
|
69
|
-
/** Existing reviewed source directory used only to bootstrap repositoryCwd. */
|
|
70
|
-
bootstrapCwd?: string;
|
|
71
|
-
outcomes: string[];
|
|
72
|
-
/** Outcomes that pause instead of advancing to another workflow step. */
|
|
73
|
-
pauseOutcomes: string[];
|
|
74
|
-
summaryMaxChars: number;
|
|
75
|
-
gateSubmitOutcome?: string;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export type DelegatedStepResult = WorkflowStepResult;
|
|
79
|
-
|
|
80
|
-
export interface ExtractedChildPolicy {
|
|
81
|
-
policy: ChildStepPolicy;
|
|
82
|
-
task: string;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function isObject(value: unknown): value is Record<string, unknown> {
|
|
86
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function isStringArray(value: unknown): value is string[] {
|
|
90
|
-
return (
|
|
91
|
-
Array.isArray(value) && value.every((item) => typeof item === 'string')
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function isStepPermissions(value: unknown): value is StepPermissions {
|
|
96
|
-
if (!isObject(value) || !isObject(value.bash)) return false;
|
|
97
|
-
const bash = value.bash;
|
|
98
|
-
const modeIsValid =
|
|
99
|
-
bash.mode === 'deny' ||
|
|
100
|
-
bash.mode === 'read-only' ||
|
|
101
|
-
bash.mode === 'allow-list' ||
|
|
102
|
-
bash.mode === 'unrestricted';
|
|
103
|
-
const rulesAreValid =
|
|
104
|
-
Array.isArray(bash.allow) &&
|
|
105
|
-
bash.allow.every(
|
|
106
|
-
(rule) =>
|
|
107
|
-
isObject(rule) &&
|
|
108
|
-
typeof rule.executable === 'string' &&
|
|
109
|
-
isStringArray(rule.argsPrefix),
|
|
110
|
-
);
|
|
111
|
-
const approvedSourcesAreValid =
|
|
112
|
-
bash.approvedSources === undefined ||
|
|
113
|
-
(isStringArray(bash.approvedSources) &&
|
|
114
|
-
new Set(bash.approvedSources).size === bash.approvedSources.length &&
|
|
115
|
-
bash.approvedSources.every(
|
|
116
|
-
(source) =>
|
|
117
|
-
source === 'verification-worker' ||
|
|
118
|
-
source === 'verification-reviewer' ||
|
|
119
|
-
source === 'remote-actions',
|
|
120
|
-
));
|
|
121
|
-
const approvalShapeIsValid =
|
|
122
|
-
bash.mode === 'allow-list'
|
|
123
|
-
? (bash.allow as unknown[]).length > 0 ||
|
|
124
|
-
(Array.isArray(bash.approvedSources) && bash.approvedSources.length > 0)
|
|
125
|
-
: bash.approvedSources === undefined;
|
|
126
|
-
return (
|
|
127
|
-
isStringArray(value.tools) &&
|
|
128
|
-
isStringArray(value.mcp) &&
|
|
129
|
-
isStringArray(value.extensions) &&
|
|
130
|
-
isStringArray(value.skills) &&
|
|
131
|
-
modeIsValid &&
|
|
132
|
-
rulesAreValid &&
|
|
133
|
-
approvedSourcesAreValid &&
|
|
134
|
-
approvalShapeIsValid
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function isSafeStepFilePath(path: string, expectedName: string): boolean {
|
|
139
|
-
const base = resolve(tmpdir());
|
|
140
|
-
const candidate = resolve(path);
|
|
141
|
-
const fromBase = relative(base, candidate);
|
|
142
|
-
return (
|
|
143
|
-
fromBase !== '' &&
|
|
144
|
-
!fromBase.startsWith('..') &&
|
|
145
|
-
!fromBase.includes('\0') &&
|
|
146
|
-
basename(candidate) === expectedName &&
|
|
147
|
-
basename(dirname(candidate)).startsWith(RESULT_DIRECTORY_PREFIX)
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export function isSafeStepResultPath(path: string): boolean {
|
|
152
|
-
return isSafeStepFilePath(path, RESULT_FILE_NAME);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export function isSafeStepCapabilityPath(path: string): boolean {
|
|
156
|
-
return isSafeStepFilePath(path, CAPABILITY_FILE_NAME);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function isSubagentRuntimeName(
|
|
160
|
-
name: string | undefined,
|
|
161
|
-
): name is string {
|
|
162
|
-
return Boolean(name && SUBAGENT_RUNTIME_NAME_PATTERN.test(name));
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function parseChildPolicy(value: unknown): ChildStepPolicy {
|
|
166
|
-
if (!isObject(value)) throw new Error('child policy must be an object');
|
|
167
|
-
const allowedKeys = new Set([
|
|
168
|
-
'version',
|
|
169
|
-
'requestId',
|
|
170
|
-
'agent',
|
|
171
|
-
'workflowId',
|
|
172
|
-
'runId',
|
|
173
|
-
'stepId',
|
|
174
|
-
'stepTitle',
|
|
175
|
-
'policyDigest',
|
|
176
|
-
'capabilityPath',
|
|
177
|
-
'capabilityToken',
|
|
178
|
-
'resultPath',
|
|
179
|
-
'permissions',
|
|
180
|
-
'approvedBashCommands',
|
|
181
|
-
'repositoryCwd',
|
|
182
|
-
'bootstrapCwd',
|
|
183
|
-
'outcomes',
|
|
184
|
-
'pauseOutcomes',
|
|
185
|
-
'summaryMaxChars',
|
|
186
|
-
'gateSubmitOutcome',
|
|
187
|
-
]);
|
|
188
|
-
const unknownKey = Object.keys(value).find((key) => !allowedKeys.has(key));
|
|
189
|
-
if (unknownKey) {
|
|
190
|
-
throw new Error(`child policy has unknown property "${unknownKey}"`);
|
|
191
|
-
}
|
|
192
|
-
const stringFields = [
|
|
193
|
-
'requestId',
|
|
194
|
-
'agent',
|
|
195
|
-
'workflowId',
|
|
196
|
-
'runId',
|
|
197
|
-
'stepId',
|
|
198
|
-
'stepTitle',
|
|
199
|
-
'policyDigest',
|
|
200
|
-
'capabilityPath',
|
|
201
|
-
'capabilityToken',
|
|
202
|
-
'resultPath',
|
|
203
|
-
] as const;
|
|
204
|
-
for (const field of stringFields) {
|
|
205
|
-
if (typeof value[field] !== 'string' || !value[field]) {
|
|
206
|
-
throw new Error(`child policy ${field} must be a non-empty string`);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
if (value.version !== 1) throw new Error('unsupported child policy version');
|
|
210
|
-
if (!POLICY_DIGEST_PATTERN.test(value.policyDigest as string)) {
|
|
211
|
-
throw new Error('child policy digest is invalid');
|
|
212
|
-
}
|
|
213
|
-
if (!isSubagentRuntimeName(value.agent as string)) {
|
|
214
|
-
throw new Error('child policy agent is not a valid subagent runtime name');
|
|
215
|
-
}
|
|
216
|
-
if (!CAPABILITY_TOKEN_PATTERN.test(value.capabilityToken as string)) {
|
|
217
|
-
throw new Error('child policy capability token is invalid');
|
|
218
|
-
}
|
|
219
|
-
if (!isSafeStepCapabilityPath(value.capabilityPath as string)) {
|
|
220
|
-
throw new Error(
|
|
221
|
-
'child policy capability path is outside its temporary directory',
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
if (!isSafeStepResultPath(value.resultPath as string)) {
|
|
225
|
-
throw new Error(
|
|
226
|
-
'child policy result path is outside its temporary directory',
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
if (
|
|
230
|
-
dirname(resolve(value.capabilityPath as string)) !==
|
|
231
|
-
dirname(resolve(value.resultPath as string))
|
|
232
|
-
) {
|
|
233
|
-
throw new Error('child policy files must share one temporary directory');
|
|
234
|
-
}
|
|
235
|
-
if (!isStepPermissions(value.permissions)) {
|
|
236
|
-
throw new Error('child policy permissions are invalid');
|
|
237
|
-
}
|
|
238
|
-
if (
|
|
239
|
-
value.approvedBashCommands !== undefined &&
|
|
240
|
-
(!isStringArray(value.approvedBashCommands) ||
|
|
241
|
-
new Set(value.approvedBashCommands).size !==
|
|
242
|
-
value.approvedBashCommands.length)
|
|
243
|
-
) {
|
|
244
|
-
throw new Error('child policy approved Bash commands are invalid');
|
|
245
|
-
}
|
|
246
|
-
if (
|
|
247
|
-
value.repositoryCwd !== undefined &&
|
|
248
|
-
(typeof value.repositoryCwd !== 'string' ||
|
|
249
|
-
!isAbsolute(value.repositoryCwd) ||
|
|
250
|
-
value.repositoryCwd.includes('\0'))
|
|
251
|
-
) {
|
|
252
|
-
throw new Error('child policy repository cwd is invalid');
|
|
253
|
-
}
|
|
254
|
-
if (
|
|
255
|
-
value.bootstrapCwd !== undefined &&
|
|
256
|
-
(typeof value.bootstrapCwd !== 'string' ||
|
|
257
|
-
!isAbsolute(value.bootstrapCwd) ||
|
|
258
|
-
value.bootstrapCwd.includes('\0') ||
|
|
259
|
-
typeof value.repositoryCwd !== 'string' ||
|
|
260
|
-
resolve(value.bootstrapCwd) === resolve(value.repositoryCwd))
|
|
261
|
-
) {
|
|
262
|
-
throw new Error('child policy bootstrap cwd is invalid');
|
|
263
|
-
}
|
|
264
|
-
if (
|
|
265
|
-
!isStringArray(value.outcomes) ||
|
|
266
|
-
value.outcomes.length === 0 ||
|
|
267
|
-
new Set(value.outcomes).size !== value.outcomes.length
|
|
268
|
-
) {
|
|
269
|
-
throw new Error('child policy outcomes are invalid');
|
|
270
|
-
}
|
|
271
|
-
if (
|
|
272
|
-
!isStringArray(value.pauseOutcomes) ||
|
|
273
|
-
new Set(value.pauseOutcomes).size !== value.pauseOutcomes.length ||
|
|
274
|
-
value.pauseOutcomes.some(
|
|
275
|
-
(outcome) => !(value.outcomes as string[]).includes(outcome),
|
|
276
|
-
)
|
|
277
|
-
) {
|
|
278
|
-
throw new Error('child policy pause outcomes are invalid');
|
|
279
|
-
}
|
|
280
|
-
if (
|
|
281
|
-
!Number.isInteger(value.summaryMaxChars) ||
|
|
282
|
-
(value.summaryMaxChars as number) < 100 ||
|
|
283
|
-
(value.summaryMaxChars as number) > 50_000
|
|
284
|
-
) {
|
|
285
|
-
throw new Error('child policy summaryMaxChars is invalid');
|
|
286
|
-
}
|
|
287
|
-
if (
|
|
288
|
-
value.gateSubmitOutcome !== undefined &&
|
|
289
|
-
(typeof value.gateSubmitOutcome !== 'string' ||
|
|
290
|
-
!value.outcomes.includes(value.gateSubmitOutcome))
|
|
291
|
-
) {
|
|
292
|
-
throw new Error('child policy gate outcome is invalid');
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
return value as unknown as ChildStepPolicy;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
export function encodeChildPolicy(policy: ChildStepPolicy): string {
|
|
299
|
-
const encoded = Buffer.from(JSON.stringify(policy), 'utf8').toString(
|
|
300
|
-
'base64url',
|
|
301
|
-
);
|
|
302
|
-
return `${CHILD_POLICY_OPEN}${encoded}${CHILD_POLICY_CLOSE}`;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function unwrapUpstreamTask(text: string): string | undefined {
|
|
306
|
-
if (text.startsWith(CHILD_POLICY_OPEN)) return text;
|
|
307
|
-
if (text.startsWith(`${UPSTREAM_TASK_PREFIX}${CHILD_POLICY_OPEN}`)) {
|
|
308
|
-
return text.slice(UPSTREAM_TASK_PREFIX.length);
|
|
309
|
-
}
|
|
310
|
-
if (
|
|
311
|
-
!text.startsWith(UPSTREAM_TASK_FILE_OPEN) ||
|
|
312
|
-
!text.endsWith(UPSTREAM_TASK_FILE_CLOSE)
|
|
313
|
-
) {
|
|
314
|
-
return undefined;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const pathStart = UPSTREAM_TASK_FILE_OPEN.length;
|
|
318
|
-
const headerEnd = text.indexOf(UPSTREAM_TASK_FILE_HEADER_CLOSE, pathStart);
|
|
319
|
-
if (headerEnd === -1) return undefined;
|
|
320
|
-
const taskFilePath = text.slice(pathStart, headerEnd);
|
|
321
|
-
const taskDirectory = dirname(resolve(taskFilePath));
|
|
322
|
-
if (
|
|
323
|
-
basename(taskFilePath) !== 'task.md' ||
|
|
324
|
-
!basename(taskDirectory).startsWith(UPSTREAM_TASK_DIRECTORY_PREFIX) ||
|
|
325
|
-
dirname(taskDirectory) !== resolve(tmpdir())
|
|
326
|
-
) {
|
|
327
|
-
return undefined;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const bodyStart = headerEnd + UPSTREAM_TASK_FILE_HEADER_CLOSE.length;
|
|
331
|
-
const body = text.slice(bodyStart, -UPSTREAM_TASK_FILE_CLOSE.length);
|
|
332
|
-
if (!body.startsWith(`${UPSTREAM_TASK_PREFIX}${CHILD_POLICY_OPEN}`)) {
|
|
333
|
-
return undefined;
|
|
334
|
-
}
|
|
335
|
-
return body.slice(UPSTREAM_TASK_PREFIX.length);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
export function extractChildPolicy(
|
|
339
|
-
text: string,
|
|
340
|
-
): ExtractedChildPolicy | undefined {
|
|
341
|
-
const taskWithPolicy = unwrapUpstreamTask(text);
|
|
342
|
-
if (taskWithPolicy === undefined) return undefined;
|
|
343
|
-
const payloadStart = CHILD_POLICY_OPEN.length;
|
|
344
|
-
const end = taskWithPolicy.indexOf(CHILD_POLICY_CLOSE, payloadStart);
|
|
345
|
-
if (
|
|
346
|
-
end === -1 ||
|
|
347
|
-
taskWithPolicy.indexOf(CHILD_POLICY_OPEN, payloadStart) !== -1
|
|
348
|
-
) {
|
|
349
|
-
throw new Error('delegated task contains an invalid child policy envelope');
|
|
350
|
-
}
|
|
351
|
-
const encoded = taskWithPolicy.slice(payloadStart, end);
|
|
352
|
-
let decoded: unknown;
|
|
353
|
-
try {
|
|
354
|
-
decoded = JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8'));
|
|
355
|
-
} catch {
|
|
356
|
-
throw new Error('delegated task child policy cannot be decoded');
|
|
357
|
-
}
|
|
358
|
-
const task = taskWithPolicy.slice(end + CHILD_POLICY_CLOSE.length).trim();
|
|
359
|
-
if (!task) throw new Error('delegated task is empty after policy extraction');
|
|
360
|
-
return { policy: parseChildPolicy(decoded), task };
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
export function parseDelegatedStepResult(
|
|
364
|
-
value: unknown,
|
|
365
|
-
policy: ChildStepPolicy,
|
|
366
|
-
): DelegatedStepResult {
|
|
367
|
-
try {
|
|
368
|
-
return parseWorkflowStepResult(value, policy);
|
|
369
|
-
} catch (error) {
|
|
370
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
371
|
-
throw new Error(message.replaceAll('workflow step', 'delegated step'), {
|
|
372
|
-
cause: error,
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
extractChildPolicy,
|
|
3
|
+
encodeChildPolicy,
|
|
4
|
+
} from './child-policy-envelope.ts';
|
|
5
|
+
export {
|
|
6
|
+
isSafeStepCapabilityPath,
|
|
7
|
+
isSafeStepResultPath,
|
|
8
|
+
} from './child-policy-paths.ts';
|
|
9
|
+
export type { ChildPolicyEnvironment } from './child-policy-paths.ts';
|
|
10
|
+
export type {
|
|
11
|
+
ChildStepPolicy,
|
|
12
|
+
ExtractedChildPolicy,
|
|
13
|
+
} from './child-policy-types.ts';
|
|
14
|
+
export { isSubagentRuntimeName } from './child-policy-validation.ts';
|
|
15
|
+
export { parseDelegatedStepResult } from './delegated-result.ts';
|
|
16
|
+
export type { DelegatedStepResult } from './delegated-result.ts';
|
|
17
|
+
export {
|
|
18
|
+
SUBAGENT_DELEGATION_CANCEL_EVENT,
|
|
19
|
+
SUBAGENT_DELEGATION_PROTOCOL_VERSION,
|
|
20
|
+
SUBAGENT_DELEGATION_REQUEST_EVENT,
|
|
21
|
+
SUBAGENT_DELEGATION_RESPONSE_EVENT,
|
|
22
|
+
SUBAGENT_DELEGATION_STARTED_EVENT,
|
|
23
|
+
SUBAGENT_DELEGATION_UPDATE_EVENT,
|
|
24
|
+
} from './protocol-events.ts';
|
|
25
|
+
export type {
|
|
26
|
+
SubagentDelegationRequest,
|
|
27
|
+
SubagentDelegationResponse,
|
|
28
|
+
SubagentDelegationStatus,
|
|
29
|
+
SubagentDelegationUpdate,
|
|
30
|
+
} from './protocol-events.ts';
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import {
|
|
2
|
+
diagnosticTextContent,
|
|
3
|
+
diagnosticToolCallText,
|
|
4
|
+
isDiagnosticRecord,
|
|
5
|
+
} from './diagnostic-text.ts';
|
|
6
|
+
import type {
|
|
7
|
+
DelegationReplayAudit,
|
|
8
|
+
DelegationReplayExpectation,
|
|
9
|
+
RecordedToolCall,
|
|
10
|
+
RecordedToolFailure,
|
|
11
|
+
} from './diagnostic-types.ts';
|
|
12
|
+
import { isReplaySafeToolCall } from './replay-safety.ts';
|
|
13
|
+
|
|
14
|
+
const initialDelegationTask = (transcript: string): string | undefined => {
|
|
15
|
+
for (const line of transcript.split('\n')) {
|
|
16
|
+
if (!line.trim()) continue;
|
|
17
|
+
let entry: unknown;
|
|
18
|
+
try {
|
|
19
|
+
entry = JSON.parse(line);
|
|
20
|
+
} catch {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (!isDiagnosticRecord(entry) || entry.type !== 'message') continue;
|
|
24
|
+
const message = entry.message;
|
|
25
|
+
if (!isDiagnosticRecord(message) || message.role !== 'user') continue;
|
|
26
|
+
if (!Array.isArray(message.content)) return undefined;
|
|
27
|
+
const textParts = message.content.flatMap((item) =>
|
|
28
|
+
isDiagnosticRecord(item) &&
|
|
29
|
+
item.type === 'text' &&
|
|
30
|
+
typeof item.text === 'string'
|
|
31
|
+
? [item.text]
|
|
32
|
+
: [],
|
|
33
|
+
);
|
|
34
|
+
return textParts.length === 1 ? textParts[0] : undefined;
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Audits whether a complete transcript can be safely replayed under the
|
|
41
|
+
* expected delegated policy.
|
|
42
|
+
*/
|
|
43
|
+
export const parseDelegationReplayAudit = (
|
|
44
|
+
transcript: string,
|
|
45
|
+
expectation: DelegationReplayExpectation,
|
|
46
|
+
isCompleteTranscript = true,
|
|
47
|
+
): DelegationReplayAudit => {
|
|
48
|
+
const calls = new Map<string, RecordedToolCall>();
|
|
49
|
+
const recordedCalls: Array<RecordedToolCall> = [];
|
|
50
|
+
const diagnostics: Array<RecordedToolFailure> = [];
|
|
51
|
+
const resultCallIds = new Set<string>();
|
|
52
|
+
let isStructurallyValid = true;
|
|
53
|
+
let order = 0;
|
|
54
|
+
|
|
55
|
+
for (const line of transcript.split('\n')) {
|
|
56
|
+
order += 1;
|
|
57
|
+
if (!line.trim()) continue;
|
|
58
|
+
let entry: unknown;
|
|
59
|
+
try {
|
|
60
|
+
entry = JSON.parse(line);
|
|
61
|
+
} catch {
|
|
62
|
+
isStructurallyValid = false;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (!isDiagnosticRecord(entry) || entry.type !== 'message') continue;
|
|
66
|
+
const message = entry.message;
|
|
67
|
+
if (!isDiagnosticRecord(message)) {
|
|
68
|
+
isStructurallyValid = false;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (message.role === 'assistant') {
|
|
73
|
+
if (!Array.isArray(message.content)) {
|
|
74
|
+
isStructurallyValid = false;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
for (const item of message.content) {
|
|
78
|
+
if (!isDiagnosticRecord(item) || item.type !== 'toolCall') continue;
|
|
79
|
+
if (
|
|
80
|
+
typeof item.id !== 'string' ||
|
|
81
|
+
typeof item.name !== 'string' ||
|
|
82
|
+
calls.has(item.id)
|
|
83
|
+
) {
|
|
84
|
+
isStructurallyValid = false;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const call = diagnosticToolCallText({
|
|
88
|
+
tool: item.name,
|
|
89
|
+
argumentsValue: item.arguments,
|
|
90
|
+
});
|
|
91
|
+
const recordedCall: RecordedToolCall = {
|
|
92
|
+
id: item.id,
|
|
93
|
+
order,
|
|
94
|
+
tool: item.name,
|
|
95
|
+
...(call ? { call } : {}),
|
|
96
|
+
};
|
|
97
|
+
calls.set(item.id, recordedCall);
|
|
98
|
+
recordedCalls.push(recordedCall);
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (message.role !== 'toolResult') continue;
|
|
104
|
+
if (
|
|
105
|
+
typeof message.toolCallId !== 'string' ||
|
|
106
|
+
typeof message.toolName !== 'string' ||
|
|
107
|
+
typeof message.isError !== 'boolean' ||
|
|
108
|
+
!Array.isArray(message.content) ||
|
|
109
|
+
resultCallIds.has(message.toolCallId)
|
|
110
|
+
) {
|
|
111
|
+
isStructurallyValid = false;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
resultCallIds.add(message.toolCallId);
|
|
115
|
+
const recorded = calls.get(message.toolCallId);
|
|
116
|
+
if (recorded?.tool !== message.toolName) {
|
|
117
|
+
isStructurallyValid = false;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (message.isError) {
|
|
121
|
+
const output = diagnosticTextContent(message.content);
|
|
122
|
+
diagnostics.push({
|
|
123
|
+
tool: message.toolName,
|
|
124
|
+
callId: message.toolCallId,
|
|
125
|
+
order,
|
|
126
|
+
...(recorded.call ? { call: recorded.call } : {}),
|
|
127
|
+
...(output ? { output } : {}),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
replaySafe:
|
|
134
|
+
isCompleteTranscript &&
|
|
135
|
+
isStructurallyValid &&
|
|
136
|
+
initialDelegationTask(transcript) === expectation.task &&
|
|
137
|
+
recordedCalls.every((call) =>
|
|
138
|
+
isReplaySafeToolCall({
|
|
139
|
+
call,
|
|
140
|
+
diagnostics,
|
|
141
|
+
bashPermission: expectation.bashPermission,
|
|
142
|
+
approvedBashCommands: expectation.approvedBashCommands,
|
|
143
|
+
}),
|
|
144
|
+
),
|
|
145
|
+
toolCount: recordedCalls.length,
|
|
146
|
+
};
|
|
147
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { BashPermission } from '../../config/types.ts';
|
|
2
|
+
import { authorizeBash } from '../../policy/bash.ts';
|
|
3
|
+
import type {
|
|
4
|
+
RecordedToolCall,
|
|
5
|
+
RecordedToolFailure,
|
|
6
|
+
} from './diagnostic-types.ts';
|
|
7
|
+
|
|
8
|
+
const REPLAY_SAFE_TOOLS: ReadonlySet<string> = new Set([
|
|
9
|
+
'find',
|
|
10
|
+
'grep',
|
|
11
|
+
'ls',
|
|
12
|
+
'read',
|
|
13
|
+
'structured_output',
|
|
14
|
+
]);
|
|
15
|
+
const PRE_EXECUTION_BASH_FAILURES = [
|
|
16
|
+
'command does not match this step',
|
|
17
|
+
'environment assignments are not allowed',
|
|
18
|
+
'not enabled by subagent',
|
|
19
|
+
'shell operators, substitutions, expansions, and comments are not allowed',
|
|
20
|
+
'shell wrapper',
|
|
21
|
+
'substitutions and escapes are not allowed inside double quotes',
|
|
22
|
+
'trailing bash escape is not allowed',
|
|
23
|
+
'unterminated bash quote',
|
|
24
|
+
'unquoted pathname and tilde expansion are not allowed',
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
const isPreExecutionBashFailure = (output: string | undefined): boolean => {
|
|
28
|
+
if (!output) return false;
|
|
29
|
+
const normalizedOutput = output.toLowerCase();
|
|
30
|
+
return PRE_EXECUTION_BASH_FAILURES.some((fragment) =>
|
|
31
|
+
normalizedOutput.includes(fragment),
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Returns whether replaying a recorded call cannot repeat a mutation.
|
|
37
|
+
*/
|
|
38
|
+
export const isReplaySafeToolCall = ({
|
|
39
|
+
call,
|
|
40
|
+
diagnostics,
|
|
41
|
+
bashPermission,
|
|
42
|
+
approvedBashCommands = [],
|
|
43
|
+
}: {
|
|
44
|
+
readonly call: RecordedToolCall;
|
|
45
|
+
readonly diagnostics: ReadonlyArray<RecordedToolFailure>;
|
|
46
|
+
readonly bashPermission?: BashPermission;
|
|
47
|
+
readonly approvedBashCommands?: ReadonlyArray<string>;
|
|
48
|
+
}): boolean => {
|
|
49
|
+
const tool = call.tool.toLowerCase();
|
|
50
|
+
if (REPLAY_SAFE_TOOLS.has(tool)) return true;
|
|
51
|
+
if (tool !== 'bash' || !call.call) return false;
|
|
52
|
+
if (authorizeBash(call.call, { mode: 'read-only', allow: [] }).allowed) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (
|
|
56
|
+
!bashPermission ||
|
|
57
|
+
authorizeBash(call.call, bashPermission, approvedBashCommands).allowed
|
|
58
|
+
) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
const failure = diagnostics.find(
|
|
62
|
+
(diagnostic) => diagnostic.callId === call.id,
|
|
63
|
+
);
|
|
64
|
+
return isPreExecutionBashFailure(failure?.output);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Returns whether a complete failure transcript contains only replay-safe
|
|
69
|
+
* calls.
|
|
70
|
+
*/
|
|
71
|
+
export const isFailureTranscriptReplaySafe = ({
|
|
72
|
+
calls,
|
|
73
|
+
diagnostics,
|
|
74
|
+
isCompleteTranscript,
|
|
75
|
+
}: {
|
|
76
|
+
readonly calls: ReadonlyArray<RecordedToolCall>;
|
|
77
|
+
readonly diagnostics: ReadonlyArray<RecordedToolFailure>;
|
|
78
|
+
readonly isCompleteTranscript: boolean;
|
|
79
|
+
}): boolean =>
|
|
80
|
+
isCompleteTranscript &&
|
|
81
|
+
calls.length > 0 &&
|
|
82
|
+
calls.every((call) => isReplaySafeToolCall({ call, diagnostics }));
|