@wichayutdew/pi-workflows 2.5.1 → 2.7.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 +34 -1
- package/dist/index.js +1315 -2467
- package/examples/starter-kit/agents/planner.md +11 -0
- package/examples/starter-kit/agents/reviewer.md +10 -0
- package/examples/starter-kit/agents/scout.md +10 -0
- package/examples/starter-kit/agents/worker.md +11 -0
- package/examples/starter-kit/agents/workspace-preparer.md +10 -0
- package/examples/starter-kit/investigate.workflow.yaml +84 -0
- package/examples/starter-kit/jira.workflow.yaml +75 -0
- package/examples/starter-kit/mr-comment.workflow.yaml +86 -93
- package/examples/starter-kit/mr-review.workflow.yaml +52 -88
- package/examples/starter-kit/settings.yaml +4 -0
- package/examples/starter-kit/steps/investigate/investigate.md +40 -0
- package/examples/starter-kit/steps/investigate/retrieve.md +28 -0
- package/examples/starter-kit/steps/investigate/validate.md +20 -0
- package/examples/starter-kit/steps/jira/create.md +25 -0
- package/examples/starter-kit/steps/jira/draft.md +18 -0
- package/examples/starter-kit/steps/jira/plan.md +30 -0
- package/examples/starter-kit/steps/mr-comment/checkout-source.md +14 -0
- package/examples/starter-kit/steps/mr-comment/fetch.md +11 -28
- package/examples/starter-kit/steps/mr-comment/implement.md +9 -26
- package/examples/starter-kit/steps/mr-comment/plan.md +46 -55
- package/examples/starter-kit/steps/mr-comment/publish.md +11 -31
- package/examples/starter-kit/steps/mr-comment/verify.md +7 -34
- package/examples/starter-kit/steps/mr-review/fetch.md +13 -21
- package/examples/starter-kit/steps/mr-review/publish-approved.md +18 -0
- package/examples/starter-kit/steps/mr-review/review-for-approval.md +53 -0
- package/examples/starter-kit/steps/mr-review/verify-published.md +16 -0
- package/examples/starter-kit/steps/shared/prepare-workspace.md +11 -89
- package/examples/starter-kit/steps/shared/publish-remote.md +16 -0
- package/examples/starter-kit/steps/ticket/implement.md +10 -23
- package/examples/starter-kit/steps/ticket/plan.md +49 -98
- package/examples/starter-kit/steps/ticket/verify.md +14 -65
- package/examples/starter-kit/steps/work/implement.md +11 -22
- package/examples/starter-kit/steps/work/plan.md +43 -59
- package/examples/starter-kit/steps/work/verify.md +14 -25
- package/examples/starter-kit/ticket.workflow.yaml +57 -61
- package/examples/starter-kit/work.workflow.yaml +49 -57
- package/package.json +9 -19
- package/schemas/workflow.schema.json +63 -15
- package/scripts/patch-herdr-agent-state.mjs +36 -0
- package/src/agents/profile.ts +98 -0
- package/src/config/ceiling.ts +0 -82
- package/src/config/types.ts +15 -53
- package/src/config/validation/settings.ts +2 -14
- package/src/config/validation/step.ts +129 -8
- package/src/config/validation/workflow.ts +1 -10
- package/src/engine/run-workflow-validation.ts +0 -3
- package/src/engine/state-types.ts +1 -1
- package/src/harness/action-context.ts +1 -13
- package/src/harness/artifact-contract.ts +46 -0
- package/src/harness/delegation-control-actions.ts +4 -63
- package/src/harness/delegation-plan.ts +26 -80
- package/src/harness/delegation-response-actions.ts +22 -135
- package/src/harness/dependencies.ts +1 -12
- package/src/harness/gate-submission-action.ts +28 -0
- package/src/harness/status-actions.ts +20 -0
- package/src/harness/step-execution-actions.ts +2 -9
- package/src/harness/types.ts +2 -40
- package/src/harness.ts +2 -17
- package/src/herdr-workflow-state.ts +65 -0
- package/src/index.ts +4 -9
- package/src/integrations/subagents/child-policy-validation.ts +6 -7
- package/src/integrations/subagents/child-runtime-dependencies.ts +1 -1
- package/src/integrations/subagents/child-runtime-policy.ts +1 -7
- package/src/integrations/subagents/child-runtime.ts +18 -9
- package/src/integrations/subagents/client.ts +240 -98
- package/src/integrations/subagents/protocol-events.ts +32 -15
- package/src/integrations/subagents/protocol.ts +1 -1
- package/src/preflight.ts +0 -8
- package/src/prompt/main-workflow-notice.ts +8 -15
- package/src/prompt/step-task.ts +7 -6
- package/src/workflow-status/format-status.ts +5 -1
- package/src/workflow-status/render-step-detail.ts +94 -0
- package/src/workflow-status/types.ts +1 -0
- package/src/workflow-status/view.ts +49 -14
- package/agents/step.md +0 -32
- package/examples/mr-comments.workflow.yaml +0 -125
- package/examples/prompts/mr-comments/implement.md +0 -17
- package/examples/prompts/mr-comments/inspect.md +0 -5
- package/examples/prompts/mr-comments/plan.md +0 -54
- package/examples/prompts/mr-comments/verify.md +0 -9
- package/examples/settings.yaml +0 -27
- package/examples/starter-kit/steps/mr-review/publish.md +0 -48
- package/examples/starter-kit/steps/mr-review/review.md +0 -76
- package/examples/starter-kit/steps/mr-review/verify.md +0 -35
- package/src/config/validation/subagent.ts +0 -288
- package/src/harness/delegation-failure.ts +0 -248
- package/src/harness/delegation-recovery-validation.ts +0 -161
- package/src/harness/delegation-retry-policy.ts +0 -120
- package/src/integrations/subagents/client-delegation.ts +0 -181
- package/src/integrations/subagents/client-messages.ts +0 -66
- package/src/integrations/subagents/client-types.ts +0 -36
- package/src/integrations/subagents/diagnostic-format.ts +0 -45
- package/src/integrations/subagents/diagnostic-text.ts +0 -114
- package/src/integrations/subagents/diagnostic-types.ts +0 -83
- package/src/integrations/subagents/diagnostics.ts +0 -26
- package/src/integrations/subagents/failure-correlation.ts +0 -285
- package/src/integrations/subagents/failure-transcript.ts +0 -252
- package/src/integrations/subagents/hidden-bash-failure.ts +0 -98
- package/src/integrations/subagents/replay-audit.ts +0 -146
- package/src/integrations/subagents/replay-safety.ts +0 -67
- package/src/integrations/subagents/session-diagnostics.ts +0 -357
package/dist/index.js
CHANGED
|
@@ -28,47 +28,6 @@ function bashWithinCeiling(requested, ceiling) {
|
|
|
28
28
|
const allowedRules = new Set(ceiling.allow.map(ruleKey));
|
|
29
29
|
return requested.allow.every((rule) => allowedRules.has(ruleKey(rule)));
|
|
30
30
|
}
|
|
31
|
-
function turnBudgetErrors(subagent, ceiling, path) {
|
|
32
|
-
if (!subagent.turnBudget) {
|
|
33
|
-
return [`${path}.turnBudget: required for a project workflow`];
|
|
34
|
-
}
|
|
35
|
-
return [
|
|
36
|
-
...subagent.turnBudget.maxTurns > ceiling.maxTurns ? [`${path}.turnBudget.maxTurns: exceeds the user permission ceiling`] : [],
|
|
37
|
-
...(subagent.turnBudget.graceTurns ?? 0) > ceiling.maxGraceTurns ? [`${path}.turnBudget.graceTurns: exceeds the user permission ceiling`] : []
|
|
38
|
-
];
|
|
39
|
-
}
|
|
40
|
-
function toolBudgetErrors(subagent, ceiling, path) {
|
|
41
|
-
if (!subagent.toolBudget) {
|
|
42
|
-
return [`${path}.toolBudget: required for a project workflow`];
|
|
43
|
-
}
|
|
44
|
-
return [
|
|
45
|
-
...subagent.toolBudget.hard > ceiling.maxToolCalls ? [`${path}.toolBudget.hard: exceeds the user permission ceiling`] : [],
|
|
46
|
-
...subagent.toolBudget.block !== "*" ? [`${path}.toolBudget.block: must be "*" for a project workflow`] : []
|
|
47
|
-
];
|
|
48
|
-
}
|
|
49
|
-
function subagentErrors(subagent, ceiling, path) {
|
|
50
|
-
if (!subagent)
|
|
51
|
-
return [];
|
|
52
|
-
if (!ceiling) {
|
|
53
|
-
return [`${path}: subagent execution exceeds the user permission ceiling`];
|
|
54
|
-
}
|
|
55
|
-
return [
|
|
56
|
-
...!ceiling.agents.includes(subagent.agent) ? [
|
|
57
|
-
`${path}.agent: "${subagent.agent}" exceeds the user permission ceiling`
|
|
58
|
-
] : [],
|
|
59
|
-
...!ceiling.contexts.includes(subagent.context) ? [
|
|
60
|
-
`${path}.context: "${subagent.context}" exceeds the user permission ceiling`
|
|
61
|
-
] : [],
|
|
62
|
-
...subagent.model && !ceiling.models.includes(subagent.model) ? [
|
|
63
|
-
`${path}.model: "${subagent.model}" exceeds the user permission ceiling`
|
|
64
|
-
] : [],
|
|
65
|
-
...subagent.timeoutMs > ceiling.maxTimeoutMs ? [`${path}.timeoutMs: exceeds the user permission ceiling`] : [],
|
|
66
|
-
...subagent.artifacts && !ceiling.artifacts ? [`${path}.artifacts: exceeds the user permission ceiling`] : [],
|
|
67
|
-
...subagent.retryToolFailures && !ceiling.retryToolFailures ? [`${path}.retryToolFailures: exceeds the user permission ceiling`] : [],
|
|
68
|
-
...turnBudgetErrors(subagent, ceiling, path),
|
|
69
|
-
...toolBudgetErrors(subagent, ceiling, path)
|
|
70
|
-
];
|
|
71
|
-
}
|
|
72
31
|
function checkWorkflowAgainstCeiling(workflow, ceiling) {
|
|
73
32
|
return Object.entries(workflow.steps).flatMap(([stepId, step]) => {
|
|
74
33
|
const path = `workflow.steps.${stepId}.permissions`;
|
|
@@ -80,8 +39,7 @@ function checkWorkflowAgainstCeiling(workflow, ceiling) {
|
|
|
80
39
|
...step.permissions.mcp.filter((selector) => !selectorAllowed(selector, ceiling.mcp)).map((selector) => `${path}.mcp: "${selector}" exceeds the user permission ceiling`),
|
|
81
40
|
...step.permissions.extensions.filter((extension) => !ceiling.extensions.includes(extension)).map((extension) => `${path}.extensions: "${extension}" exceeds the user permission ceiling`),
|
|
82
41
|
...step.permissions.skills.filter((skill) => !ceiling.skills.includes(skill)).map((skill) => `${path}.skills: "${skill}" exceeds the user permission ceiling`),
|
|
83
|
-
...!bashWithinCeiling(step.permissions.bash, ceiling.bash) ? [`${path}.bash: exceeds the user permission ceiling`] : []
|
|
84
|
-
...subagentErrors(step.subagent, ceiling.subagent, `workflow.steps.${stepId}.subagent`)
|
|
42
|
+
...!bashWithinCeiling(step.permissions.bash, ceiling.bash) ? [`${path}.bash: exceeds the user permission ceiling`] : []
|
|
85
43
|
];
|
|
86
44
|
});
|
|
87
45
|
}
|
|
@@ -106,7 +64,7 @@ import { join } from "node:path";
|
|
|
106
64
|
// src/config/types.ts
|
|
107
65
|
var WORKFLOW_SCHEMA_VERSION = 1;
|
|
108
66
|
var DEFAULT_STATUS_SHORTCUT = "ctrl+alt+w";
|
|
109
|
-
var
|
|
67
|
+
var AGENT_PROFILE_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*(?:\.[a-z0-9][a-z0-9-]*)*$/;
|
|
110
68
|
var MAX_WORKSPACE_PATH_CHARS = 4096;
|
|
111
69
|
var MAX_WORKSPACE_ALLOWED_ROOTS = 32;
|
|
112
70
|
var EMPTY_PERMISSIONS = {
|
|
@@ -116,13 +74,6 @@ var EMPTY_PERMISSIONS = {
|
|
|
116
74
|
skills: [],
|
|
117
75
|
bash: { mode: "deny", allow: [] }
|
|
118
76
|
};
|
|
119
|
-
var DEFAULT_STEP_SUBAGENT = {
|
|
120
|
-
agent: "pi-workflows.step",
|
|
121
|
-
context: "fresh",
|
|
122
|
-
timeoutMs: 900000,
|
|
123
|
-
artifacts: false,
|
|
124
|
-
retryToolFailures: false
|
|
125
|
-
};
|
|
126
77
|
var DEFAULT_SETTINGS = {
|
|
127
78
|
version: WORKFLOW_SCHEMA_VERSION,
|
|
128
79
|
allowProjectWorkflows: false,
|
|
@@ -190,15 +141,6 @@ function readInteger(value, fallback, path, errors, limits) {
|
|
|
190
141
|
}
|
|
191
142
|
return value;
|
|
192
143
|
}
|
|
193
|
-
function readBoolean(value, fallback, path, errors) {
|
|
194
|
-
if (value === undefined)
|
|
195
|
-
return fallback;
|
|
196
|
-
if (typeof value !== "boolean") {
|
|
197
|
-
errors.push(`${path}: expected a boolean`);
|
|
198
|
-
return fallback;
|
|
199
|
-
}
|
|
200
|
-
return value;
|
|
201
|
-
}
|
|
202
144
|
function readStringList(value, path, errors, pattern) {
|
|
203
145
|
if (value === undefined)
|
|
204
146
|
return [];
|
|
@@ -437,164 +379,6 @@ function readStatusShortcut(value, path, errors) {
|
|
|
437
379
|
return isStatusShortcut(normalized) ? normalized : DEFAULT_STATUS_SHORTCUT;
|
|
438
380
|
}
|
|
439
381
|
|
|
440
|
-
// src/config/validation/subagent.ts
|
|
441
|
-
function parseSubagentTurnBudget(value, path, errors) {
|
|
442
|
-
if (value === undefined)
|
|
443
|
-
return;
|
|
444
|
-
if (!isJsonObject(value)) {
|
|
445
|
-
errors.push(`${path}: expected an object`);
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
rejectUnknownKeys(value, ["maxTurns", "graceTurns"], path, errors);
|
|
449
|
-
const maxTurns = readInteger(value.maxTurns, 0, `${path}.maxTurns`, errors, {
|
|
450
|
-
min: 1,
|
|
451
|
-
max: 1000
|
|
452
|
-
});
|
|
453
|
-
const graceTurns = value.graceTurns === undefined ? undefined : readInteger(value.graceTurns, 0, `${path}.graceTurns`, errors, {
|
|
454
|
-
min: 0,
|
|
455
|
-
max: 100
|
|
456
|
-
});
|
|
457
|
-
if (maxTurns === 0)
|
|
458
|
-
return;
|
|
459
|
-
return {
|
|
460
|
-
maxTurns,
|
|
461
|
-
...graceTurns !== undefined ? { graceTurns } : {}
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
function parseSubagentToolBudget(value, path, errors) {
|
|
465
|
-
if (value === undefined)
|
|
466
|
-
return;
|
|
467
|
-
if (!isJsonObject(value)) {
|
|
468
|
-
errors.push(`${path}: expected an object`);
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
rejectUnknownKeys(value, ["soft", "hard", "block"], path, errors);
|
|
472
|
-
const hard = readInteger(value.hard, 0, `${path}.hard`, errors, {
|
|
473
|
-
min: 1,
|
|
474
|
-
max: 1e5
|
|
475
|
-
});
|
|
476
|
-
const soft = value.soft === undefined ? undefined : readInteger(value.soft, 0, `${path}.soft`, errors, {
|
|
477
|
-
min: 1,
|
|
478
|
-
max: 1e5
|
|
479
|
-
});
|
|
480
|
-
let block;
|
|
481
|
-
if (value.block === "*") {
|
|
482
|
-
block = "*";
|
|
483
|
-
} else if (value.block !== undefined) {
|
|
484
|
-
block = readStringList(value.block, `${path}.block`, errors, TOOL_PATTERN);
|
|
485
|
-
if (block.length === 0) {
|
|
486
|
-
errors.push(`${path}.block: expected "*" or at least one tool name`);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
if (soft !== undefined && hard > 0 && soft > hard) {
|
|
490
|
-
errors.push(`${path}.soft: must not exceed hard`);
|
|
491
|
-
}
|
|
492
|
-
if (hard === 0)
|
|
493
|
-
return;
|
|
494
|
-
return {
|
|
495
|
-
hard,
|
|
496
|
-
...soft !== undefined ? { soft } : {},
|
|
497
|
-
...block !== undefined ? { block } : {}
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
function parseStepSubagent(value, path, errors) {
|
|
501
|
-
if (value === undefined)
|
|
502
|
-
return;
|
|
503
|
-
if (typeof value === "string") {
|
|
504
|
-
const agent2 = readString(value, path, errors, {
|
|
505
|
-
pattern: SUBAGENT_RUNTIME_NAME_PATTERN
|
|
506
|
-
}) ?? DEFAULT_STEP_SUBAGENT.agent;
|
|
507
|
-
return { ...DEFAULT_STEP_SUBAGENT, agent: agent2 };
|
|
508
|
-
}
|
|
509
|
-
if (!isJsonObject(value)) {
|
|
510
|
-
errors.push(`${path}: expected an agent profile name or object`);
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
rejectUnknownKeys(value, [
|
|
514
|
-
"agent",
|
|
515
|
-
"context",
|
|
516
|
-
"model",
|
|
517
|
-
"timeoutMs",
|
|
518
|
-
"turnBudget",
|
|
519
|
-
"toolBudget",
|
|
520
|
-
"artifacts",
|
|
521
|
-
"retryToolFailures"
|
|
522
|
-
], path, errors);
|
|
523
|
-
const agent = value.agent === undefined ? DEFAULT_STEP_SUBAGENT.agent : readString(value.agent, `${path}.agent`, errors, {
|
|
524
|
-
pattern: SUBAGENT_RUNTIME_NAME_PATTERN
|
|
525
|
-
}) ?? DEFAULT_STEP_SUBAGENT.agent;
|
|
526
|
-
const contextValue = value.context === undefined ? DEFAULT_STEP_SUBAGENT.context : readString(value.context, `${path}.context`, errors);
|
|
527
|
-
const context = DEFAULT_STEP_SUBAGENT.context;
|
|
528
|
-
if (contextValue !== "fresh") {
|
|
529
|
-
errors.push(`${path}.context: expected fresh`);
|
|
530
|
-
}
|
|
531
|
-
const model = value.model === undefined ? undefined : readString(value.model, `${path}.model`, errors, {
|
|
532
|
-
pattern: RESOURCE_SELECTOR_PATTERN
|
|
533
|
-
});
|
|
534
|
-
const timeoutMs = readInteger(value.timeoutMs, DEFAULT_STEP_SUBAGENT.timeoutMs, `${path}.timeoutMs`, errors, { min: 1000, max: 86400000 });
|
|
535
|
-
const turnBudget = parseSubagentTurnBudget(value.turnBudget, `${path}.turnBudget`, errors);
|
|
536
|
-
const toolBudget = parseSubagentToolBudget(value.toolBudget, `${path}.toolBudget`, errors);
|
|
537
|
-
const artifacts = readBoolean(value.artifacts, DEFAULT_STEP_SUBAGENT.artifacts, `${path}.artifacts`, errors);
|
|
538
|
-
const retryToolFailures = readBoolean(value.retryToolFailures, DEFAULT_STEP_SUBAGENT.retryToolFailures, `${path}.retryToolFailures`, errors);
|
|
539
|
-
return {
|
|
540
|
-
agent,
|
|
541
|
-
context,
|
|
542
|
-
...model ? { model } : {},
|
|
543
|
-
timeoutMs,
|
|
544
|
-
...turnBudget ? { turnBudget } : {},
|
|
545
|
-
...toolBudget ? { toolBudget } : {},
|
|
546
|
-
artifacts,
|
|
547
|
-
retryToolFailures
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
function parseSubagentPermissionCeiling(value, path, errors) {
|
|
551
|
-
if (!isJsonObject(value)) {
|
|
552
|
-
errors.push(`${path}: expected an object`);
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
rejectUnknownKeys(value, [
|
|
556
|
-
"agents",
|
|
557
|
-
"contexts",
|
|
558
|
-
"models",
|
|
559
|
-
"maxTimeoutMs",
|
|
560
|
-
"maxTurns",
|
|
561
|
-
"maxGraceTurns",
|
|
562
|
-
"maxToolCalls",
|
|
563
|
-
"artifacts",
|
|
564
|
-
"retryToolFailures"
|
|
565
|
-
], path, errors);
|
|
566
|
-
const agents = readStringList(value.agents, `${path}.agents`, errors, SUBAGENT_RUNTIME_NAME_PATTERN);
|
|
567
|
-
const contexts = readStringList(value.contexts, `${path}.contexts`, errors, /^fresh$/).filter((context) => context === "fresh");
|
|
568
|
-
const models = readStringList(value.models, `${path}.models`, errors, RESOURCE_SELECTOR_PATTERN);
|
|
569
|
-
if (agents.length === 0) {
|
|
570
|
-
errors.push(`${path}.agents: at least one subagent is required`);
|
|
571
|
-
}
|
|
572
|
-
if (contexts.length === 0) {
|
|
573
|
-
errors.push(`${path}.contexts: at least one context mode is required`);
|
|
574
|
-
}
|
|
575
|
-
[
|
|
576
|
-
"maxTimeoutMs",
|
|
577
|
-
"maxTurns",
|
|
578
|
-
"maxGraceTurns",
|
|
579
|
-
"maxToolCalls",
|
|
580
|
-
"artifacts"
|
|
581
|
-
].filter((field) => value[field] === undefined).forEach((field) => errors.push(`${path}.${field}: required`));
|
|
582
|
-
return {
|
|
583
|
-
agents,
|
|
584
|
-
contexts,
|
|
585
|
-
models,
|
|
586
|
-
maxTimeoutMs: readInteger(value.maxTimeoutMs, 0, `${path}.maxTimeoutMs`, errors, { min: 1000, max: 86400000 }),
|
|
587
|
-
maxTurns: readInteger(value.maxTurns, 0, `${path}.maxTurns`, errors, {
|
|
588
|
-
min: 1,
|
|
589
|
-
max: 1000
|
|
590
|
-
}),
|
|
591
|
-
maxGraceTurns: readInteger(value.maxGraceTurns, 0, `${path}.maxGraceTurns`, errors, { min: 0, max: 100 }),
|
|
592
|
-
maxToolCalls: readInteger(value.maxToolCalls, 0, `${path}.maxToolCalls`, errors, { min: 1, max: 1e5 }),
|
|
593
|
-
artifacts: readBoolean(value.artifacts, false, `${path}.artifacts`, errors),
|
|
594
|
-
retryToolFailures: readBoolean(value.retryToolFailures, false, `${path}.retryToolFailures`, errors)
|
|
595
|
-
};
|
|
596
|
-
}
|
|
597
|
-
|
|
598
382
|
// src/config/validation/settings.ts
|
|
599
383
|
function parsePermissionCeiling(value, path, errors) {
|
|
600
384
|
if (value === undefined)
|
|
@@ -603,7 +387,7 @@ function parsePermissionCeiling(value, path, errors) {
|
|
|
603
387
|
errors.push(`${path}: expected an object`);
|
|
604
388
|
return;
|
|
605
389
|
}
|
|
606
|
-
rejectUnknownKeys(value, ["tools", "mcp", "extensions", "skills", "bash"
|
|
390
|
+
rejectUnknownKeys(value, ["tools", "mcp", "extensions", "skills", "bash"], path, errors);
|
|
607
391
|
const permissions = parsePermissions({
|
|
608
392
|
...value.tools !== undefined ? { tools: value.tools } : {},
|
|
609
393
|
...value.mcp !== undefined ? { mcp: value.mcp } : {},
|
|
@@ -611,11 +395,7 @@ function parsePermissionCeiling(value, path, errors) {
|
|
|
611
395
|
...value.skills !== undefined ? { skills: value.skills } : {},
|
|
612
396
|
...value.bash !== undefined ? { bash: value.bash } : {}
|
|
613
397
|
}, path, errors);
|
|
614
|
-
|
|
615
|
-
return {
|
|
616
|
-
...permissions,
|
|
617
|
-
...subagent ? { subagent } : {}
|
|
618
|
-
};
|
|
398
|
+
return permissions;
|
|
619
399
|
}
|
|
620
400
|
function validateSettings(value) {
|
|
621
401
|
const errors = [];
|
|
@@ -738,6 +518,75 @@ function parseTransitions(value, path, errors) {
|
|
|
738
518
|
}
|
|
739
519
|
return transitions;
|
|
740
520
|
}
|
|
521
|
+
function parseArtifactContract(value, path, errors) {
|
|
522
|
+
if (value === undefined)
|
|
523
|
+
return;
|
|
524
|
+
if (!isJsonObject(value)) {
|
|
525
|
+
errors.push(`${path}: expected an object`);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
rejectUnknownKeys(value, [
|
|
529
|
+
"maxChars",
|
|
530
|
+
"requiredSubstrings",
|
|
531
|
+
"forbiddenSubstrings",
|
|
532
|
+
"equalOccurrenceGroups",
|
|
533
|
+
"onValidationFailure"
|
|
534
|
+
], path, errors);
|
|
535
|
+
if (value.maxChars === undefined) {
|
|
536
|
+
errors.push(`${path}.maxChars: expected an integer from 1 to 200000`);
|
|
537
|
+
}
|
|
538
|
+
const maxChars = readInteger(value.maxChars, 200000, `${path}.maxChars`, errors, { min: 1, max: 200000 });
|
|
539
|
+
const parseSubstrings = (field) => {
|
|
540
|
+
const values = readStringList(value[field], `${path}.${field}`, errors, /.+/);
|
|
541
|
+
if (values.length > 32) {
|
|
542
|
+
errors.push(`${path}.${field}: at most 32 values are allowed`);
|
|
543
|
+
}
|
|
544
|
+
values.forEach((substring, index) => {
|
|
545
|
+
if (substring.length > 1024) {
|
|
546
|
+
errors.push(`${path}.${field}[${index}]: exceeds 1024 characters`);
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
return values;
|
|
550
|
+
};
|
|
551
|
+
const equalOccurrenceGroups = (() => {
|
|
552
|
+
if (value.equalOccurrenceGroups === undefined)
|
|
553
|
+
return [];
|
|
554
|
+
if (!Array.isArray(value.equalOccurrenceGroups)) {
|
|
555
|
+
errors.push(`${path}.equalOccurrenceGroups: expected an array`);
|
|
556
|
+
return [];
|
|
557
|
+
}
|
|
558
|
+
if (value.equalOccurrenceGroups.length > 32) {
|
|
559
|
+
errors.push(`${path}.equalOccurrenceGroups: at most 32 groups are allowed`);
|
|
560
|
+
}
|
|
561
|
+
return value.equalOccurrenceGroups.reduce((groups, group, index) => {
|
|
562
|
+
const groupPath = `${path}.equalOccurrenceGroups[${index}]`;
|
|
563
|
+
const values = readStringList(group, groupPath, errors, /.+/);
|
|
564
|
+
if (values.length < 2) {
|
|
565
|
+
errors.push(`${groupPath}: at least two values are required`);
|
|
566
|
+
}
|
|
567
|
+
if (values.length > 32) {
|
|
568
|
+
errors.push(`${groupPath}: at most 32 values are allowed`);
|
|
569
|
+
}
|
|
570
|
+
values.forEach((substring, valueIndex) => {
|
|
571
|
+
if (substring.length > 1024) {
|
|
572
|
+
errors.push(`${groupPath}[${valueIndex}]: exceeds 1024 characters`);
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
return [...groups, values];
|
|
576
|
+
}, []);
|
|
577
|
+
})();
|
|
578
|
+
const onValidationFailure = value.onValidationFailure === undefined ? undefined : readString(value.onValidationFailure, `${path}.onValidationFailure`, errors);
|
|
579
|
+
if (onValidationFailure !== undefined && onValidationFailure !== "retry") {
|
|
580
|
+
errors.push(`${path}.onValidationFailure: expected retry`);
|
|
581
|
+
}
|
|
582
|
+
return {
|
|
583
|
+
maxChars,
|
|
584
|
+
requiredSubstrings: parseSubstrings("requiredSubstrings"),
|
|
585
|
+
forbiddenSubstrings: parseSubstrings("forbiddenSubstrings"),
|
|
586
|
+
equalOccurrenceGroups,
|
|
587
|
+
...onValidationFailure === "retry" ? { onValidationFailure } : {}
|
|
588
|
+
};
|
|
589
|
+
}
|
|
741
590
|
function parseGate(value, path, errors) {
|
|
742
591
|
if (value === undefined)
|
|
743
592
|
return;
|
|
@@ -750,7 +599,8 @@ function parseGate(value, path, errors) {
|
|
|
750
599
|
"submitOutcome",
|
|
751
600
|
"approvedOutcome",
|
|
752
601
|
"rejectedOutcome",
|
|
753
|
-
"timeoutMs"
|
|
602
|
+
"timeoutMs",
|
|
603
|
+
"artifactContract"
|
|
754
604
|
], path, errors);
|
|
755
605
|
const providerValue = value.provider === undefined ? "prompt" : readString(value.provider, `${path}.provider`, errors);
|
|
756
606
|
const provider = providerValue === "prompt" || providerValue === "plannotator" ? providerValue : undefined;
|
|
@@ -760,6 +610,7 @@ function parseGate(value, path, errors) {
|
|
|
760
610
|
const submitOutcome = readString(value.submitOutcome, `${path}.submitOutcome`, errors, { pattern: OUTCOME_PATTERN });
|
|
761
611
|
const approvedOutcome = readString(value.approvedOutcome, `${path}.approvedOutcome`, errors, { pattern: OUTCOME_PATTERN });
|
|
762
612
|
const rejectedOutcome = readString(value.rejectedOutcome, `${path}.rejectedOutcome`, errors, { pattern: OUTCOME_PATTERN });
|
|
613
|
+
const artifactContract = parseArtifactContract(value.artifactContract, `${path}.artifactContract`, errors);
|
|
763
614
|
if (provider === "prompt" && value.timeoutMs !== undefined) {
|
|
764
615
|
errors.push(`${path}.timeoutMs: only valid with provider "plannotator"`);
|
|
765
616
|
}
|
|
@@ -773,12 +624,14 @@ function parseGate(value, path, errors) {
|
|
|
773
624
|
provider,
|
|
774
625
|
submitOutcome,
|
|
775
626
|
approvedOutcome,
|
|
776
|
-
rejectedOutcome
|
|
627
|
+
rejectedOutcome,
|
|
628
|
+
...artifactContract ? { artifactContract } : {}
|
|
777
629
|
} : {
|
|
778
630
|
provider,
|
|
779
631
|
submitOutcome,
|
|
780
632
|
approvedOutcome,
|
|
781
633
|
rejectedOutcome,
|
|
634
|
+
...artifactContract ? { artifactContract } : {},
|
|
782
635
|
timeoutMs: readInteger(value.timeoutMs, 30000, `${path}.timeoutMs`, errors, { min: 1000, max: 30000 })
|
|
783
636
|
};
|
|
784
637
|
}
|
|
@@ -841,7 +694,7 @@ function parseWorkflowStep(value, stepId, path, errors) {
|
|
|
841
694
|
rejectUnknownKeys(value, [
|
|
842
695
|
"title",
|
|
843
696
|
"prompt",
|
|
844
|
-
"
|
|
697
|
+
"agent",
|
|
845
698
|
"permissions",
|
|
846
699
|
"requires",
|
|
847
700
|
"transitions",
|
|
@@ -850,7 +703,10 @@ function parseWorkflowStep(value, stepId, path, errors) {
|
|
|
850
703
|
], path, errors);
|
|
851
704
|
const title = value.title === undefined ? stepId : readString(value.title, `${path}.title`, errors);
|
|
852
705
|
const prompt = parsePrompt(value.prompt, `${path}.prompt`, errors);
|
|
853
|
-
const
|
|
706
|
+
const agentName = value.agent === undefined ? undefined : readString(value.agent, `${path}.agent`, errors, {
|
|
707
|
+
pattern: AGENT_PROFILE_NAME_PATTERN
|
|
708
|
+
});
|
|
709
|
+
const agent = agentName ? { name: agentName } : undefined;
|
|
854
710
|
const permissions = parsePermissions(value.permissions, `${path}.permissions`, errors);
|
|
855
711
|
const requires = parseRequirements(value.requires, permissions, `${path}.requires`, errors);
|
|
856
712
|
const transitions = parseTransitions(value.transitions, `${path}.transitions`, errors);
|
|
@@ -866,6 +722,9 @@ function parseWorkflowStep(value, stepId, path, errors) {
|
|
|
866
722
|
if (Object.hasOwn(transitions, gate.submitOutcome)) {
|
|
867
723
|
errors.push(`${path}.transitions: submitOutcome is handled by the gate and must not be a transition`);
|
|
868
724
|
}
|
|
725
|
+
if (gate.artifactContract?.onValidationFailure === "retry" && !Object.hasOwn(transitions, "retry")) {
|
|
726
|
+
errors.push(`${path}.transitions: artifact-contract retry requires a "retry" transition`);
|
|
727
|
+
}
|
|
869
728
|
}
|
|
870
729
|
if (workspace) {
|
|
871
730
|
workspace.bindOn.forEach((outcome) => {
|
|
@@ -879,7 +738,7 @@ function parseWorkflowStep(value, stepId, path, errors) {
|
|
|
879
738
|
return {
|
|
880
739
|
title,
|
|
881
740
|
prompt,
|
|
882
|
-
...
|
|
741
|
+
...agent ? { agent } : {},
|
|
883
742
|
permissions,
|
|
884
743
|
requires,
|
|
885
744
|
transitions,
|
|
@@ -928,9 +787,6 @@ function validateWorkspaceGraph(steps, errors) {
|
|
|
928
787
|
if (index > 0) {
|
|
929
788
|
errors.push(`${workspacePath}: only one workspace-binding step is allowed; "${firstBinderId}" also configures workspace binding`);
|
|
930
789
|
}
|
|
931
|
-
if (!step.subagent) {
|
|
932
|
-
errors.push(`${workspacePath}: workspace binding requires a subagent`);
|
|
933
|
-
}
|
|
934
790
|
if (step.gate) {
|
|
935
791
|
errors.push(`${workspacePath}: workspace binding is not allowed on a gated step`);
|
|
936
792
|
}
|
|
@@ -942,10 +798,10 @@ function validateWorkspaceGraph(steps, errors) {
|
|
|
942
798
|
}
|
|
943
799
|
return target && Object.hasOwn(steps, target) ? [target] : [];
|
|
944
800
|
});
|
|
945
|
-
validateWorkspaceDescendants(steps, downstream
|
|
801
|
+
validateWorkspaceDescendants(steps, downstream);
|
|
946
802
|
});
|
|
947
803
|
}
|
|
948
|
-
function validateWorkspaceDescendants(steps, initialStepIds
|
|
804
|
+
function validateWorkspaceDescendants(steps, initialStepIds) {
|
|
949
805
|
const pending = [...initialStepIds];
|
|
950
806
|
const visited = new Set;
|
|
951
807
|
while (pending.length > 0) {
|
|
@@ -956,9 +812,6 @@ function validateWorkspaceDescendants(steps, initialStepIds, errors) {
|
|
|
956
812
|
const step = steps[stepId];
|
|
957
813
|
if (!step)
|
|
958
814
|
continue;
|
|
959
|
-
if (!step.subagent) {
|
|
960
|
-
errors.push(`workflow.steps.${stepId}.subagent: every nonterminal step reachable after workspace binding must use a subagent`);
|
|
961
|
-
}
|
|
962
815
|
Object.values(step.transitions).forEach((target) => {
|
|
963
816
|
if (target !== "$done" && target !== "$pause" && Object.hasOwn(steps, target) && !visited.has(target)) {
|
|
964
817
|
pending.push(target);
|
|
@@ -1396,1684 +1249,147 @@ function registerHarnessCommands(pi, controller) {
|
|
|
1396
1249
|
}
|
|
1397
1250
|
}
|
|
1398
1251
|
|
|
1399
|
-
// src/
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1252
|
+
// src/harness/catalog.ts
|
|
1253
|
+
function createEmptyCatalog() {
|
|
1254
|
+
return {
|
|
1255
|
+
workflows: new Map,
|
|
1256
|
+
settings: DEFAULT_SETTINGS,
|
|
1257
|
+
diagnostics: [],
|
|
1258
|
+
userDirectory: ""
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
function formatCatalogDiagnostics(catalog) {
|
|
1262
|
+
const shownDiagnostics = catalog.diagnostics.slice(0, 3).map((diagnostic) => `${diagnostic.path}: ${diagnostic.message}`);
|
|
1263
|
+
const remainingCount = catalog.diagnostics.length - shownDiagnostics.length;
|
|
1403
1264
|
return [
|
|
1404
|
-
|
|
1405
|
-
...
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
...
|
|
1412
|
-
`Post-completion watchdog warning: ${diagnostic.postCompletionWarning}`
|
|
1413
|
-
] : [],
|
|
1414
|
-
...diagnostic.correlation === "latest-before-completion" ? [
|
|
1415
|
-
"Correlation: latest failed tool call before successful structured_output; terminal text did not identify the call"
|
|
1416
|
-
] : [],
|
|
1417
|
-
...hasSuccessfulOutputCorrelation ? [
|
|
1418
|
-
"Correlation: terminal error text came from a successful tool result before the final structured_output"
|
|
1419
|
-
] : []
|
|
1265
|
+
...shownDiagnostics,
|
|
1266
|
+
...remainingCount > 0 ? [`${remainingCount} more diagnostic(s)`] : []
|
|
1267
|
+
].join(`
|
|
1268
|
+
`);
|
|
1269
|
+
}
|
|
1270
|
+
function parseAvailableSkills(systemPrompt) {
|
|
1271
|
+
const sections = [
|
|
1272
|
+
...systemPrompt.matchAll(/<available_skills>([\s\S]*?)<\/available_skills>/g)
|
|
1420
1273
|
];
|
|
1274
|
+
const section = sections.at(-1)?.[1] ?? "";
|
|
1275
|
+
return [...section.matchAll(/<name>([^<]+)<\/name>/g)].flatMap((match) => {
|
|
1276
|
+
const name = match[1]?.trim();
|
|
1277
|
+
return name ? [{ name }] : [];
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// src/harness/dependencies.ts
|
|
1282
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
1283
|
+
import { constants as constants2, mkdtempSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1284
|
+
import { lstat as lstat2, open as open2, rm } from "node:fs/promises";
|
|
1285
|
+
import { tmpdir } from "node:os";
|
|
1286
|
+
import { join as join4 } from "node:path";
|
|
1287
|
+
|
|
1288
|
+
// src/integrations/plannotator-responses.ts
|
|
1289
|
+
var isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1290
|
+
var errorText = (value, fallback) => typeof value.error === "string" && value.error.trim() ? value.error : fallback;
|
|
1291
|
+
var normalizePlannotatorStartResponse = (value) => {
|
|
1292
|
+
if (!isRecord(value)) {
|
|
1293
|
+
return {
|
|
1294
|
+
status: "error",
|
|
1295
|
+
error: "Plannotator returned an invalid response"
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
if (value.status === "unavailable") {
|
|
1299
|
+
return {
|
|
1300
|
+
status: "unavailable",
|
|
1301
|
+
error: errorText(value, "Plannotator is unavailable")
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
if (value.status === "error") {
|
|
1305
|
+
return {
|
|
1306
|
+
status: "error",
|
|
1307
|
+
error: errorText(value, "Plannotator failed")
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
const result = isRecord(value.result) ? value.result : undefined;
|
|
1311
|
+
if (value.status === "handled" && result?.status === "pending" && typeof result.reviewId === "string") {
|
|
1312
|
+
return {
|
|
1313
|
+
status: "handled",
|
|
1314
|
+
result: { status: "pending", reviewId: result.reviewId }
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
return {
|
|
1318
|
+
status: "error",
|
|
1319
|
+
error: "Plannotator returned an invalid start result"
|
|
1320
|
+
};
|
|
1421
1321
|
};
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
};
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
const command = argumentsValue.command ?? argumentsValue.cmd;
|
|
1458
|
-
if (typeof command === "string" && command.trim()) {
|
|
1459
|
-
return boundedDiagnosticText(command.trim());
|
|
1322
|
+
var normalizePlannotatorStatusResponse = (value, requestedReviewId) => {
|
|
1323
|
+
if (!isRecord(value)) {
|
|
1324
|
+
return {
|
|
1325
|
+
status: "error",
|
|
1326
|
+
error: "Plannotator returned an invalid response"
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
if (value.status === "unavailable") {
|
|
1330
|
+
return {
|
|
1331
|
+
status: "unavailable",
|
|
1332
|
+
error: errorText(value, "Plannotator is unavailable")
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
if (value.status === "error") {
|
|
1336
|
+
return {
|
|
1337
|
+
status: "error",
|
|
1338
|
+
error: errorText(value, "Plannotator failed")
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
const result = isRecord(value.result) ? value.result : undefined;
|
|
1342
|
+
if (value.status !== "handled" || !result) {
|
|
1343
|
+
return {
|
|
1344
|
+
status: "error",
|
|
1345
|
+
error: "Plannotator returned an invalid status result"
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
if (result.status === "pending" || result.status === "missing") {
|
|
1349
|
+
return { status: "handled", result: { status: result.status } };
|
|
1350
|
+
}
|
|
1351
|
+
if (result.status === "completed" && typeof result.reviewId === "string" && typeof result.approved === "boolean") {
|
|
1352
|
+
if (result.reviewId !== requestedReviewId) {
|
|
1353
|
+
return {
|
|
1354
|
+
status: "error",
|
|
1355
|
+
error: "Plannotator returned a result for a different review"
|
|
1356
|
+
};
|
|
1460
1357
|
}
|
|
1358
|
+
return {
|
|
1359
|
+
status: "handled",
|
|
1360
|
+
result: {
|
|
1361
|
+
status: "completed",
|
|
1362
|
+
reviewId: result.reviewId,
|
|
1363
|
+
approved: result.approved,
|
|
1364
|
+
feedback: typeof result.feedback === "string" ? result.feedback : ""
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1461
1367
|
}
|
|
1462
|
-
return
|
|
1368
|
+
return {
|
|
1369
|
+
status: "error",
|
|
1370
|
+
error: "Plannotator returned an invalid status result"
|
|
1371
|
+
};
|
|
1463
1372
|
};
|
|
1464
|
-
var
|
|
1465
|
-
if (!
|
|
1373
|
+
var parsePlannotatorResult = (value) => {
|
|
1374
|
+
if (!isRecord(value))
|
|
1466
1375
|
return;
|
|
1467
|
-
if (
|
|
1376
|
+
if (typeof value.reviewId !== "string" || typeof value.approved !== "boolean") {
|
|
1468
1377
|
return;
|
|
1469
1378
|
}
|
|
1470
|
-
return
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
const lines = value.split(/\r?\n/).map(normalizeDiagnosticText).filter((line) => line.length >= 8);
|
|
1476
|
-
return [...new Set([normalizedValue, ...lines])].filter((fragment) => fragment.length >= 8);
|
|
1379
|
+
return {
|
|
1380
|
+
reviewId: value.reviewId,
|
|
1381
|
+
approved: value.approved,
|
|
1382
|
+
feedback: typeof value.feedback === "string" ? value.feedback : ""
|
|
1383
|
+
};
|
|
1477
1384
|
};
|
|
1478
1385
|
|
|
1479
|
-
// src/integrations/
|
|
1480
|
-
var
|
|
1481
|
-
var
|
|
1482
|
-
var
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
var transcriptWarningContent = (entry) => {
|
|
1487
|
-
if (entry.type !== "custom_message" || entry.customType !== WATCHDOG_WARNING_TYPE) {
|
|
1488
|
-
return;
|
|
1489
|
-
}
|
|
1490
|
-
const details = entry.details;
|
|
1491
|
-
if (isDiagnosticRecord(details)) {
|
|
1492
|
-
const fields = [
|
|
1493
|
-
typeof details.summary === "string" ? details.summary : undefined,
|
|
1494
|
-
typeof details.evidence === "string" ? details.evidence : undefined,
|
|
1495
|
-
typeof details.recommendedAction === "string" ? `Recommended action: ${details.recommendedAction}` : undefined
|
|
1496
|
-
].filter((field) => Boolean(field?.trim()));
|
|
1497
|
-
if (fields.length > 0)
|
|
1498
|
-
return boundedWarningField(fields.join(" "));
|
|
1499
|
-
}
|
|
1500
|
-
return typeof entry.content === "string" && entry.content.trim() ? boundedWarningField(entry.content) : "The child emitted an unresolved watchdog warning after completion.";
|
|
1501
|
-
};
|
|
1502
|
-
var isBenignTerminalAssistant = (message) => message.stopReason === "stop" && message.errorMessage === undefined && Array.isArray(message.content) && message.content.length === 1 && isDiagnosticRecord(message.content[0]) && message.content[0].type === "text" && message.content[0].text === "";
|
|
1503
|
-
var parseFailureTranscript = (transcript) => {
|
|
1504
|
-
const calls = new Map;
|
|
1505
|
-
const recordedCalls = [];
|
|
1506
|
-
const diagnostics = [];
|
|
1507
|
-
const successfulResults = [];
|
|
1508
|
-
const successfulCompletions = [];
|
|
1509
|
-
const transcriptWarnings = [];
|
|
1510
|
-
const recordedMessages = [];
|
|
1511
|
-
const resultCallIds = new Set;
|
|
1512
|
-
let hasValidFalsePositiveProof = true;
|
|
1513
|
-
let lastInteractionOrder = 0;
|
|
1514
|
-
let order = 0;
|
|
1515
|
-
for (const line of transcript.split(`
|
|
1516
|
-
`)) {
|
|
1517
|
-
order += 1;
|
|
1518
|
-
if (!line.trim())
|
|
1519
|
-
continue;
|
|
1520
|
-
let entry;
|
|
1521
|
-
try {
|
|
1522
|
-
entry = JSON.parse(line);
|
|
1523
|
-
} catch {
|
|
1524
|
-
hasValidFalsePositiveProof = false;
|
|
1525
|
-
continue;
|
|
1526
|
-
}
|
|
1527
|
-
if (isDiagnosticRecord(entry)) {
|
|
1528
|
-
const warning = transcriptWarningContent(entry);
|
|
1529
|
-
if (warning) {
|
|
1530
|
-
transcriptWarnings.push({ order, content: warning });
|
|
1531
|
-
lastInteractionOrder = order;
|
|
1532
|
-
continue;
|
|
1533
|
-
}
|
|
1534
|
-
}
|
|
1535
|
-
if (!isDiagnosticRecord(entry) || entry.type !== "message")
|
|
1536
|
-
continue;
|
|
1537
|
-
const message = entry.message;
|
|
1538
|
-
if (!isDiagnosticRecord(message)) {
|
|
1539
|
-
hasValidFalsePositiveProof = false;
|
|
1540
|
-
continue;
|
|
1541
|
-
}
|
|
1542
|
-
recordedMessages.push({ order, value: message });
|
|
1543
|
-
if (message.role === "assistant" && (typeof message.errorMessage === "string" && message.errorMessage.trim().length > 0 || message.stopReason === "error" || message.stopReason === "aborted")) {
|
|
1544
|
-
hasValidFalsePositiveProof = false;
|
|
1545
|
-
}
|
|
1546
|
-
if (message.role === "assistant") {
|
|
1547
|
-
if (!Array.isArray(message.content)) {
|
|
1548
|
-
hasValidFalsePositiveProof = false;
|
|
1549
|
-
continue;
|
|
1550
|
-
}
|
|
1551
|
-
if (!isBenignTerminalAssistant(message)) {
|
|
1552
|
-
lastInteractionOrder = order;
|
|
1553
|
-
}
|
|
1554
|
-
const toolCalls = message.content.filter((item) => isDiagnosticRecord(item) && item.type === "toolCall" && typeof item.id === "string" && typeof item.name === "string");
|
|
1555
|
-
for (const item of message.content) {
|
|
1556
|
-
if (!isDiagnosticRecord(item) || item.type !== "toolCall" || typeof item.id !== "string" || typeof item.name !== "string") {
|
|
1557
|
-
if (isDiagnosticRecord(item) && item.type === "toolCall") {
|
|
1558
|
-
hasValidFalsePositiveProof = false;
|
|
1559
|
-
}
|
|
1560
|
-
continue;
|
|
1561
|
-
}
|
|
1562
|
-
if (calls.has(item.id))
|
|
1563
|
-
hasValidFalsePositiveProof = false;
|
|
1564
|
-
const call = diagnosticToolCallText({
|
|
1565
|
-
tool: item.name,
|
|
1566
|
-
argumentsValue: item.arguments
|
|
1567
|
-
});
|
|
1568
|
-
const isExclusiveCompletion = toolCalls.length === 1 && message.content.every((contentItem) => isDiagnosticRecord(contentItem) && (contentItem.type === "thinking" || contentItem.type === "text" || contentItem.type === "toolCall"));
|
|
1569
|
-
const completionValue = item.name === "structured_output" && isExclusiveCompletion ? structuredCompletionValue(item.arguments) : undefined;
|
|
1570
|
-
const recordedCall = {
|
|
1571
|
-
id: item.id,
|
|
1572
|
-
order,
|
|
1573
|
-
tool: item.name,
|
|
1574
|
-
...call ? { call } : {},
|
|
1575
|
-
...completionValue ? { completionValue } : {}
|
|
1576
|
-
};
|
|
1577
|
-
calls.set(item.id, recordedCall);
|
|
1578
|
-
recordedCalls.push(recordedCall);
|
|
1579
|
-
}
|
|
1580
|
-
continue;
|
|
1581
|
-
}
|
|
1582
|
-
if (message.role !== "toolResult" || typeof message.toolName !== "string") {
|
|
1583
|
-
if (message.role === "toolResult")
|
|
1584
|
-
hasValidFalsePositiveProof = false;
|
|
1585
|
-
continue;
|
|
1586
|
-
}
|
|
1587
|
-
lastInteractionOrder = order;
|
|
1588
|
-
if (typeof message.toolCallId !== "string" || typeof message.isError !== "boolean" || !Array.isArray(message.content) || resultCallIds.has(message.toolCallId)) {
|
|
1589
|
-
hasValidFalsePositiveProof = false;
|
|
1590
|
-
}
|
|
1591
|
-
if (typeof message.toolCallId === "string") {
|
|
1592
|
-
resultCallIds.add(message.toolCallId);
|
|
1593
|
-
}
|
|
1594
|
-
const recorded = typeof message.toolCallId === "string" ? calls.get(message.toolCallId) : undefined;
|
|
1595
|
-
const doesCallMatchResult = recorded?.tool === message.toolName;
|
|
1596
|
-
if (!doesCallMatchResult)
|
|
1597
|
-
hasValidFalsePositiveProof = false;
|
|
1598
|
-
if (message.toolName === "structured_output" && message.isError === false && doesCallMatchResult && recorded.completionValue) {
|
|
1599
|
-
successfulCompletions.push({
|
|
1600
|
-
order,
|
|
1601
|
-
value: recorded.completionValue
|
|
1602
|
-
});
|
|
1603
|
-
}
|
|
1604
|
-
if (message.isError === false && message.toolName !== "structured_output" && doesCallMatchResult) {
|
|
1605
|
-
const output2 = diagnosticTextContent(message.content);
|
|
1606
|
-
const detectorOutput = firstDiagnosticTextContent(message.content);
|
|
1607
|
-
successfulResults.push({
|
|
1608
|
-
order,
|
|
1609
|
-
tool: message.toolName,
|
|
1610
|
-
...recorded.call ? { call: recorded.call } : {},
|
|
1611
|
-
...output2 ? { output: output2 } : {},
|
|
1612
|
-
...detectorOutput !== undefined ? { detectorOutput } : {}
|
|
1613
|
-
});
|
|
1614
|
-
}
|
|
1615
|
-
if (message.isError !== true)
|
|
1616
|
-
continue;
|
|
1617
|
-
const output = diagnosticTextContent(message.content);
|
|
1618
|
-
diagnostics.push({
|
|
1619
|
-
tool: message.toolName,
|
|
1620
|
-
...doesCallMatchResult && recorded.call ? { call: recorded.call } : {},
|
|
1621
|
-
...output ? { output } : {},
|
|
1622
|
-
...doesCallMatchResult && typeof message.toolCallId === "string" ? { callId: message.toolCallId } : {},
|
|
1623
|
-
order
|
|
1624
|
-
});
|
|
1625
|
-
}
|
|
1626
|
-
return {
|
|
1627
|
-
recordedCalls,
|
|
1628
|
-
diagnostics,
|
|
1629
|
-
successfulResults,
|
|
1630
|
-
successfulCompletions,
|
|
1631
|
-
transcriptWarnings,
|
|
1632
|
-
recordedMessages,
|
|
1633
|
-
resultCallIds,
|
|
1634
|
-
hasValidFalsePositiveProof,
|
|
1635
|
-
lastInteractionOrder
|
|
1636
|
-
};
|
|
1637
|
-
};
|
|
1638
|
-
|
|
1639
|
-
// src/integrations/subagents/hidden-bash-failure.ts
|
|
1640
|
-
var HIDDEN_BASH_FATAL_PATTERNS = [
|
|
1641
|
-
/command not found/i,
|
|
1642
|
-
/permission denied/i,
|
|
1643
|
-
/no such file or directory/i,
|
|
1644
|
-
/segmentation fault/i,
|
|
1645
|
-
/killed|terminated/i,
|
|
1646
|
-
/out of memory/i,
|
|
1647
|
-
/connection refused/i,
|
|
1648
|
-
/timeout/i
|
|
1649
|
-
];
|
|
1650
|
-
var HIDDEN_BASH_EXIT_PATTERN = /exit(?:ed)?\s*(?:with\s*)?(?:code|status)?\s*[:\s]?\s*(\d+)/i;
|
|
1651
|
-
var lastAssistantTextIndex = (messages) => {
|
|
1652
|
-
for (let index = messages.length - 1;index >= 0; index -= 1) {
|
|
1653
|
-
const message = messages[index]?.value;
|
|
1654
|
-
const hasAssistantText = message?.role === "assistant" && Array.isArray(message.content) && message.content.some((item) => isDiagnosticRecord(item) && item.type === "text" && typeof item.text === "string" && item.text.trim().length > 0);
|
|
1655
|
-
if (hasAssistantText)
|
|
1656
|
-
return index;
|
|
1657
|
-
}
|
|
1658
|
-
return -1;
|
|
1659
|
-
};
|
|
1660
|
-
var hiddenExitCode = (output) => {
|
|
1661
|
-
const exitCodeText = output.match(HIDDEN_BASH_EXIT_PATTERN)?.[1];
|
|
1662
|
-
const exitCode = exitCodeText === undefined ? undefined : Number.parseInt(exitCodeText, 10);
|
|
1663
|
-
if (exitCode !== undefined && exitCode !== 0)
|
|
1664
|
-
return exitCode;
|
|
1665
|
-
return HIDDEN_BASH_FATAL_PATTERNS.some((pattern) => pattern.test(output)) ? 1 : undefined;
|
|
1666
|
-
};
|
|
1667
|
-
var reproduceHiddenBashFalsePositive = (messages, successfulResults) => {
|
|
1668
|
-
const assistantTextIndex = lastAssistantTextIndex(messages);
|
|
1669
|
-
const scanStart = assistantTextIndex >= 0 ? assistantTextIndex + 1 : 0;
|
|
1670
|
-
for (let index = messages.length - 1;index >= scanStart; index -= 1) {
|
|
1671
|
-
const recordedMessage = messages[index];
|
|
1672
|
-
const message = recordedMessage?.value;
|
|
1673
|
-
if (!recordedMessage || message?.role !== "toolResult" || message.toolName !== "bash" || message.isError !== false) {
|
|
1674
|
-
continue;
|
|
1675
|
-
}
|
|
1676
|
-
const output = firstDiagnosticTextContent(message.content);
|
|
1677
|
-
if (output === undefined)
|
|
1678
|
-
continue;
|
|
1679
|
-
const detectedExitCode = hiddenExitCode(output);
|
|
1680
|
-
if (detectedExitCode === undefined)
|
|
1681
|
-
continue;
|
|
1682
|
-
const result = successfulResults.find((candidate) => candidate.order === recordedMessage.order && candidate.tool === "bash" && candidate.detectorOutput === output);
|
|
1683
|
-
if (!result)
|
|
1684
|
-
return;
|
|
1685
|
-
return {
|
|
1686
|
-
result,
|
|
1687
|
-
terminalError: `bash failed (exit ${detectedExitCode}): ${output.slice(0, 200)}`
|
|
1688
|
-
};
|
|
1689
|
-
}
|
|
1690
|
-
return;
|
|
1691
|
-
};
|
|
1692
|
-
|
|
1693
|
-
// src/policy/bash-authorization.ts
|
|
1694
|
-
import { basename } from "node:path";
|
|
1695
|
-
|
|
1696
|
-
// src/policy/restricted-command.ts
|
|
1697
|
-
var UNQUOTED_SHELL_METACHARACTERS = new Set([
|
|
1698
|
-
";",
|
|
1699
|
-
"&",
|
|
1700
|
-
"|",
|
|
1701
|
-
"<",
|
|
1702
|
-
">",
|
|
1703
|
-
`
|
|
1704
|
-
`,
|
|
1705
|
-
"\r",
|
|
1706
|
-
"`",
|
|
1707
|
-
"$",
|
|
1708
|
-
"(",
|
|
1709
|
-
")",
|
|
1710
|
-
"{",
|
|
1711
|
-
"}",
|
|
1712
|
-
"#",
|
|
1713
|
-
"\x00"
|
|
1714
|
-
]);
|
|
1715
|
-
var PATHNAME_EXPANSION_CHARACTERS = new Set([
|
|
1716
|
-
"*",
|
|
1717
|
-
"?",
|
|
1718
|
-
"[",
|
|
1719
|
-
"]",
|
|
1720
|
-
"~"
|
|
1721
|
-
]);
|
|
1722
|
-
var invalidCharacter = (character) => {
|
|
1723
|
-
if (character === `
|
|
1724
|
-
` || character === "\r" || character === "\x00") {
|
|
1725
|
-
return "multiline and null characters are not allowed";
|
|
1726
|
-
}
|
|
1727
|
-
return;
|
|
1728
|
-
};
|
|
1729
|
-
var tokenizeRestrictedCommand = (command) => {
|
|
1730
|
-
if (!command.trim())
|
|
1731
|
-
return { error: "empty Bash command" };
|
|
1732
|
-
const tokens = [];
|
|
1733
|
-
let token = "";
|
|
1734
|
-
let quote;
|
|
1735
|
-
let isEscaping = false;
|
|
1736
|
-
let isTokenStarted = false;
|
|
1737
|
-
for (const character of command) {
|
|
1738
|
-
if (quote === "'") {
|
|
1739
|
-
const error = invalidCharacter(character);
|
|
1740
|
-
if (error)
|
|
1741
|
-
return { error };
|
|
1742
|
-
if (character === "'") {
|
|
1743
|
-
quote = undefined;
|
|
1744
|
-
} else {
|
|
1745
|
-
token += character;
|
|
1746
|
-
}
|
|
1747
|
-
isTokenStarted = true;
|
|
1748
|
-
continue;
|
|
1749
|
-
}
|
|
1750
|
-
if (quote === '"') {
|
|
1751
|
-
const error = invalidCharacter(character);
|
|
1752
|
-
if (error)
|
|
1753
|
-
return { error };
|
|
1754
|
-
if (character === '"') {
|
|
1755
|
-
quote = undefined;
|
|
1756
|
-
} else if (character === "$" || character === "`" || character === "\\") {
|
|
1757
|
-
return {
|
|
1758
|
-
error: "substitutions and escapes are not allowed inside double quotes"
|
|
1759
|
-
};
|
|
1760
|
-
} else {
|
|
1761
|
-
token += character;
|
|
1762
|
-
}
|
|
1763
|
-
isTokenStarted = true;
|
|
1764
|
-
continue;
|
|
1765
|
-
}
|
|
1766
|
-
if (isEscaping) {
|
|
1767
|
-
const error = invalidCharacter(character);
|
|
1768
|
-
if (error)
|
|
1769
|
-
return { error };
|
|
1770
|
-
token += character;
|
|
1771
|
-
isEscaping = false;
|
|
1772
|
-
isTokenStarted = true;
|
|
1773
|
-
continue;
|
|
1774
|
-
}
|
|
1775
|
-
if (character === "\\") {
|
|
1776
|
-
isEscaping = true;
|
|
1777
|
-
isTokenStarted = true;
|
|
1778
|
-
continue;
|
|
1779
|
-
}
|
|
1780
|
-
if (character === "'" || character === '"') {
|
|
1781
|
-
quote = character;
|
|
1782
|
-
isTokenStarted = true;
|
|
1783
|
-
continue;
|
|
1784
|
-
}
|
|
1785
|
-
if (UNQUOTED_SHELL_METACHARACTERS.has(character)) {
|
|
1786
|
-
return {
|
|
1787
|
-
error: "shell operators, substitutions, expansions, and comments are not allowed"
|
|
1788
|
-
};
|
|
1789
|
-
}
|
|
1790
|
-
if (PATHNAME_EXPANSION_CHARACTERS.has(character)) {
|
|
1791
|
-
return {
|
|
1792
|
-
error: "unquoted pathname and tilde expansion are not allowed"
|
|
1793
|
-
};
|
|
1794
|
-
}
|
|
1795
|
-
if (/\s/u.test(character)) {
|
|
1796
|
-
if (isTokenStarted) {
|
|
1797
|
-
tokens.push(token);
|
|
1798
|
-
token = "";
|
|
1799
|
-
isTokenStarted = false;
|
|
1800
|
-
}
|
|
1801
|
-
continue;
|
|
1802
|
-
}
|
|
1803
|
-
token += character;
|
|
1804
|
-
isTokenStarted = true;
|
|
1805
|
-
}
|
|
1806
|
-
if (isEscaping)
|
|
1807
|
-
return { error: "trailing Bash escape is not allowed" };
|
|
1808
|
-
if (quote)
|
|
1809
|
-
return { error: "unterminated Bash quote" };
|
|
1810
|
-
if (isTokenStarted)
|
|
1811
|
-
tokens.push(token);
|
|
1812
|
-
return tokens.length > 0 ? { tokens } : { error: "empty Bash command" };
|
|
1813
|
-
};
|
|
1814
|
-
|
|
1815
|
-
// src/policy/bash-authorization.ts
|
|
1816
|
-
var SHELL_WRAPPERS = new Set([
|
|
1817
|
-
"bash",
|
|
1818
|
-
"builtin",
|
|
1819
|
-
"command",
|
|
1820
|
-
"env",
|
|
1821
|
-
"exec",
|
|
1822
|
-
"fish",
|
|
1823
|
-
"sh",
|
|
1824
|
-
"time",
|
|
1825
|
-
"xargs",
|
|
1826
|
-
"zsh"
|
|
1827
|
-
]);
|
|
1828
|
-
var ENVIRONMENT_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/u;
|
|
1829
|
-
var reject = (reason) => ({
|
|
1830
|
-
allowed: false,
|
|
1831
|
-
reason
|
|
1832
|
-
});
|
|
1833
|
-
var matchesRule = (tokens, rule) => tokens[0] === rule.executable && rule.argsPrefix.every((expected, index) => tokens[index + 1] === expected);
|
|
1834
|
-
var authorizeBash = (command, permission) => {
|
|
1835
|
-
if (permission.mode === "unrestricted") {
|
|
1836
|
-
return { allowed: true };
|
|
1837
|
-
}
|
|
1838
|
-
if (permission.mode === "deny") {
|
|
1839
|
-
return reject("Bash is disabled for this workflow step");
|
|
1840
|
-
}
|
|
1841
|
-
const parsed = tokenizeRestrictedCommand(command);
|
|
1842
|
-
if (!parsed.tokens)
|
|
1843
|
-
return reject(parsed.error);
|
|
1844
|
-
const executable = parsed.tokens[0] ?? "";
|
|
1845
|
-
if (SHELL_WRAPPERS.has(basename(executable))) {
|
|
1846
|
-
return reject(`shell wrapper "${executable}" is not allowed in restricted mode`);
|
|
1847
|
-
}
|
|
1848
|
-
if (ENVIRONMENT_ASSIGNMENT.test(executable)) {
|
|
1849
|
-
return reject("environment assignments are not allowed in restricted mode");
|
|
1850
|
-
}
|
|
1851
|
-
const rule = permission.allow.find((candidate) => matchesRule(parsed.tokens, candidate));
|
|
1852
|
-
if (!rule) {
|
|
1853
|
-
return reject("command does not match this step's Bash allow-list");
|
|
1854
|
-
}
|
|
1855
|
-
return { allowed: true, tokens: [...parsed.tokens] };
|
|
1856
|
-
};
|
|
1857
|
-
// src/integrations/subagents/replay-safety.ts
|
|
1858
|
-
var REPLAY_SAFE_TOOLS = new Set([
|
|
1859
|
-
"find",
|
|
1860
|
-
"grep",
|
|
1861
|
-
"ls",
|
|
1862
|
-
"read",
|
|
1863
|
-
"structured_output"
|
|
1864
|
-
]);
|
|
1865
|
-
var isPreExecutionBashFailure = (output, rejectionReason) => Boolean(output && rejectionReason && output.toLowerCase().includes(rejectionReason.toLowerCase()));
|
|
1866
|
-
var isReplaySafeToolCall = ({
|
|
1867
|
-
call,
|
|
1868
|
-
diagnostics,
|
|
1869
|
-
bashPermission
|
|
1870
|
-
}) => {
|
|
1871
|
-
const tool = call.tool.toLowerCase();
|
|
1872
|
-
if (REPLAY_SAFE_TOOLS.has(tool))
|
|
1873
|
-
return true;
|
|
1874
|
-
if (tool !== "bash" || !call.call)
|
|
1875
|
-
return false;
|
|
1876
|
-
if (!bashPermission) {
|
|
1877
|
-
return false;
|
|
1878
|
-
}
|
|
1879
|
-
const authorization = authorizeBash(call.call, bashPermission);
|
|
1880
|
-
if (authorization.allowed)
|
|
1881
|
-
return false;
|
|
1882
|
-
const failure = diagnostics.find((diagnostic) => diagnostic.callId === call.id);
|
|
1883
|
-
return isPreExecutionBashFailure(failure?.output, authorization.reason);
|
|
1884
|
-
};
|
|
1885
|
-
var isFailureTranscriptReplaySafe = ({
|
|
1886
|
-
calls,
|
|
1887
|
-
diagnostics,
|
|
1888
|
-
isCompleteTranscript
|
|
1889
|
-
}) => isCompleteTranscript && calls.length > 0 && calls.every((call) => isReplaySafeToolCall({ call, diagnostics }));
|
|
1890
|
-
|
|
1891
|
-
// src/integrations/subagents/failure-correlation.ts
|
|
1892
|
-
var diagnosticMatchesTerminalError = (diagnostic, terminalError) => {
|
|
1893
|
-
if (!diagnostic.output)
|
|
1894
|
-
return false;
|
|
1895
|
-
const detail = terminalError.match(/\b[a-z][\w-]* failed(?:\s*\([^)]*\))?\s*:\s*([\s\S]+)/i)?.[1] ?? terminalError;
|
|
1896
|
-
const outputFragments = comparableDiagnosticFragments(diagnostic.output);
|
|
1897
|
-
const errorFragments = comparableDiagnosticFragments(`${terminalError}
|
|
1898
|
-
${detail}`);
|
|
1899
|
-
return outputFragments.some((output) => errorFragments.some((error) => output.includes(error) || error.includes(output)));
|
|
1900
|
-
};
|
|
1901
|
-
var latestMatching = (diagnostics, predicate) => {
|
|
1902
|
-
for (let index = diagnostics.length - 1;index >= 0; index -= 1) {
|
|
1903
|
-
const diagnostic = diagnostics[index];
|
|
1904
|
-
if (diagnostic && predicate(diagnostic))
|
|
1905
|
-
return diagnostic;
|
|
1906
|
-
}
|
|
1907
|
-
return;
|
|
1908
|
-
};
|
|
1909
|
-
var latestWarningAfter = (warnings, order) => {
|
|
1910
|
-
for (let index = warnings.length - 1;index >= 0; index -= 1) {
|
|
1911
|
-
const warning = warnings[index];
|
|
1912
|
-
if (warning && warning.order > order)
|
|
1913
|
-
return warning;
|
|
1914
|
-
}
|
|
1915
|
-
return;
|
|
1916
|
-
};
|
|
1917
|
-
var finalCompletion = (completions, latestFailureOrder, lastInteractionOrder, allowCompletionProof) => {
|
|
1918
|
-
if (!allowCompletionProof || completions.length !== 1)
|
|
1919
|
-
return;
|
|
1920
|
-
const completion = completions[0];
|
|
1921
|
-
return completion && completion.order > latestFailureOrder && completion.order === lastInteractionOrder ? completion : undefined;
|
|
1922
|
-
};
|
|
1923
|
-
var publicDiagnostic = (diagnostic, diagnostics, recordedCalls, successfulCompletions, postCompletionWarning, lastInteractionOrder, allowCompletionProof, correlation) => {
|
|
1924
|
-
const result = {
|
|
1925
|
-
tool: diagnostic.tool,
|
|
1926
|
-
...diagnostic.call ? { call: diagnostic.call } : {},
|
|
1927
|
-
...diagnostic.output ? { output: diagnostic.output } : {}
|
|
1928
|
-
};
|
|
1929
|
-
const latestFailureOrder = diagnostics.at(-1)?.order ?? diagnostic.order;
|
|
1930
|
-
const completion = finalCompletion(successfulCompletions, latestFailureOrder, lastInteractionOrder, allowCompletionProof);
|
|
1931
|
-
return {
|
|
1932
|
-
...result,
|
|
1933
|
-
...postCompletionWarning ? { postCompletionWarning: postCompletionWarning.content } : {},
|
|
1934
|
-
...isFailureTranscriptReplaySafe({
|
|
1935
|
-
calls: recordedCalls,
|
|
1936
|
-
diagnostics,
|
|
1937
|
-
isCompleteTranscript: allowCompletionProof
|
|
1938
|
-
}) ? { replaySafe: true } : {},
|
|
1939
|
-
...completion ? {
|
|
1940
|
-
completionAfterFailure: true,
|
|
1941
|
-
completionValue: completion.value
|
|
1942
|
-
} : {},
|
|
1943
|
-
...correlation ? { correlation } : {}
|
|
1944
|
-
};
|
|
1945
|
-
};
|
|
1946
|
-
var hiddenFalsePositiveDiagnostic = (expectedTool, terminalError, allowCompletionProof, transcript) => {
|
|
1947
|
-
const {
|
|
1948
|
-
diagnostics,
|
|
1949
|
-
hasValidFalsePositiveProof,
|
|
1950
|
-
lastInteractionOrder,
|
|
1951
|
-
recordedCalls,
|
|
1952
|
-
recordedMessages,
|
|
1953
|
-
resultCallIds,
|
|
1954
|
-
successfulCompletions,
|
|
1955
|
-
successfulResults,
|
|
1956
|
-
transcriptWarnings
|
|
1957
|
-
} = transcript;
|
|
1958
|
-
const completionOrder = successfulCompletions.at(-1)?.order ?? 0;
|
|
1959
|
-
const postCompletionWarning = latestWarningAfter(transcriptWarnings, completionOrder);
|
|
1960
|
-
const falsePositive = reproduceHiddenBashFalsePositive(recordedMessages, successfulResults);
|
|
1961
|
-
const hasLaterFailure = falsePositive !== undefined && diagnostics.some((diagnostic) => diagnostic.order > falsePositive.result.order);
|
|
1962
|
-
const completion = falsePositive ? finalCompletion(successfulCompletions, Math.max(falsePositive.result.order, diagnostics.at(-1)?.order ?? falsePositive.result.order), lastInteractionOrder, allowCompletionProof) : undefined;
|
|
1963
|
-
if (!hasValidFalsePositiveProof || recordedCalls.length !== resultCallIds.size || !recordedCalls.every((call) => resultCallIds.has(call.id)) || recordedCalls.filter((call) => call.tool === "structured_output").length !== 1 || expectedTool?.toLowerCase() !== "bash" || !falsePositive || hasLaterFailure || postCompletionWarning !== undefined || terminalError !== falsePositive.terminalError || !completion) {
|
|
1964
|
-
return;
|
|
1965
|
-
}
|
|
1966
|
-
const successfulOutput = falsePositive.result;
|
|
1967
|
-
return {
|
|
1968
|
-
tool: successfulOutput.tool,
|
|
1969
|
-
...successfulOutput.call ? { call: successfulOutput.call } : {},
|
|
1970
|
-
...successfulOutput.output ? { output: successfulOutput.output } : {},
|
|
1971
|
-
completionAfterFailure: true,
|
|
1972
|
-
completionValue: completion.value,
|
|
1973
|
-
transcriptToolCount: recordedCalls.length,
|
|
1974
|
-
transcriptTurnCount: recordedMessages.filter(({ value }) => value.role === "assistant").length,
|
|
1975
|
-
correlation: "successful-output-before-completion"
|
|
1976
|
-
};
|
|
1977
|
-
};
|
|
1978
|
-
var parseToolFailureDiagnostic = (transcript, expectedTool, terminalError, allowCompletionProof = true) => {
|
|
1979
|
-
const parsed = parseFailureTranscript(transcript);
|
|
1980
|
-
const {
|
|
1981
|
-
diagnostics,
|
|
1982
|
-
lastInteractionOrder,
|
|
1983
|
-
recordedCalls,
|
|
1984
|
-
successfulCompletions,
|
|
1985
|
-
transcriptWarnings
|
|
1986
|
-
} = parsed;
|
|
1987
|
-
const completionOrder = successfulCompletions.at(-1)?.order ?? 0;
|
|
1988
|
-
const postCompletionWarning = latestWarningAfter(transcriptWarnings, completionOrder);
|
|
1989
|
-
const matchesExpectedTool = (diagnostic) => expectedTool === undefined || diagnostic.tool.toLowerCase() === expectedTool.toLowerCase();
|
|
1990
|
-
let selected;
|
|
1991
|
-
if (terminalError) {
|
|
1992
|
-
selected = latestMatching(diagnostics, (diagnostic) => matchesExpectedTool(diagnostic) && diagnosticMatchesTerminalError(diagnostic, terminalError));
|
|
1993
|
-
if (!selected) {
|
|
1994
|
-
const fallback = latestMatching(diagnostics, matchesExpectedTool);
|
|
1995
|
-
const latestFailureOrder = diagnostics.at(-1)?.order;
|
|
1996
|
-
if (fallback && latestFailureOrder !== undefined && finalCompletion(successfulCompletions, latestFailureOrder, lastInteractionOrder, allowCompletionProof)) {
|
|
1997
|
-
return publicDiagnostic(fallback, diagnostics, recordedCalls, successfulCompletions, postCompletionWarning, lastInteractionOrder, allowCompletionProof, "latest-before-completion");
|
|
1998
|
-
}
|
|
1999
|
-
}
|
|
2000
|
-
if (!selected) {
|
|
2001
|
-
const falsePositive = hiddenFalsePositiveDiagnostic(expectedTool, terminalError, allowCompletionProof, parsed);
|
|
2002
|
-
if (falsePositive)
|
|
2003
|
-
return falsePositive;
|
|
2004
|
-
}
|
|
2005
|
-
} else {
|
|
2006
|
-
selected = latestMatching(diagnostics, matchesExpectedTool);
|
|
2007
|
-
}
|
|
2008
|
-
if (!selected && postCompletionWarning) {
|
|
2009
|
-
selected = latestMatching(diagnostics, matchesExpectedTool) ?? diagnostics.at(-1);
|
|
2010
|
-
if (!selected) {
|
|
2011
|
-
return {
|
|
2012
|
-
tool: expectedTool ?? "subagent",
|
|
2013
|
-
postCompletionWarning: postCompletionWarning.content
|
|
2014
|
-
};
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
return selected ? publicDiagnostic(selected, diagnostics, recordedCalls, successfulCompletions, postCompletionWarning, lastInteractionOrder, allowCompletionProof) : undefined;
|
|
2018
|
-
};
|
|
2019
|
-
// src/integrations/subagents/replay-audit.ts
|
|
2020
|
-
var initialDelegationTask = (transcript) => {
|
|
2021
|
-
for (const line of transcript.split(`
|
|
2022
|
-
`)) {
|
|
2023
|
-
if (!line.trim())
|
|
2024
|
-
continue;
|
|
2025
|
-
let entry;
|
|
2026
|
-
try {
|
|
2027
|
-
entry = JSON.parse(line);
|
|
2028
|
-
} catch {
|
|
2029
|
-
continue;
|
|
2030
|
-
}
|
|
2031
|
-
if (!isDiagnosticRecord(entry) || entry.type !== "message")
|
|
2032
|
-
continue;
|
|
2033
|
-
const message = entry.message;
|
|
2034
|
-
if (!isDiagnosticRecord(message) || message.role !== "user")
|
|
2035
|
-
continue;
|
|
2036
|
-
if (!Array.isArray(message.content))
|
|
2037
|
-
return;
|
|
2038
|
-
const textParts = message.content.flatMap((item) => isDiagnosticRecord(item) && item.type === "text" && typeof item.text === "string" ? [item.text] : []);
|
|
2039
|
-
return textParts.length === 1 ? textParts[0] : undefined;
|
|
2040
|
-
}
|
|
2041
|
-
return;
|
|
2042
|
-
};
|
|
2043
|
-
var parseDelegationReplayAudit = (transcript, expectation, isCompleteTranscript = true) => {
|
|
2044
|
-
const calls = new Map;
|
|
2045
|
-
const recordedCalls = [];
|
|
2046
|
-
const diagnostics = [];
|
|
2047
|
-
const resultCallIds = new Set;
|
|
2048
|
-
let isStructurallyValid = true;
|
|
2049
|
-
let order = 0;
|
|
2050
|
-
for (const line of transcript.split(`
|
|
2051
|
-
`)) {
|
|
2052
|
-
order += 1;
|
|
2053
|
-
if (!line.trim())
|
|
2054
|
-
continue;
|
|
2055
|
-
let entry;
|
|
2056
|
-
try {
|
|
2057
|
-
entry = JSON.parse(line);
|
|
2058
|
-
} catch {
|
|
2059
|
-
isStructurallyValid = false;
|
|
2060
|
-
continue;
|
|
2061
|
-
}
|
|
2062
|
-
if (!isDiagnosticRecord(entry) || entry.type !== "message")
|
|
2063
|
-
continue;
|
|
2064
|
-
const message = entry.message;
|
|
2065
|
-
if (!isDiagnosticRecord(message)) {
|
|
2066
|
-
isStructurallyValid = false;
|
|
2067
|
-
continue;
|
|
2068
|
-
}
|
|
2069
|
-
if (message.role === "assistant") {
|
|
2070
|
-
if (!Array.isArray(message.content)) {
|
|
2071
|
-
isStructurallyValid = false;
|
|
2072
|
-
continue;
|
|
2073
|
-
}
|
|
2074
|
-
for (const item of message.content) {
|
|
2075
|
-
if (!isDiagnosticRecord(item) || item.type !== "toolCall")
|
|
2076
|
-
continue;
|
|
2077
|
-
if (typeof item.id !== "string" || typeof item.name !== "string" || calls.has(item.id)) {
|
|
2078
|
-
isStructurallyValid = false;
|
|
2079
|
-
continue;
|
|
2080
|
-
}
|
|
2081
|
-
const call = diagnosticToolCallText({
|
|
2082
|
-
tool: item.name,
|
|
2083
|
-
argumentsValue: item.arguments
|
|
2084
|
-
});
|
|
2085
|
-
const recordedCall = {
|
|
2086
|
-
id: item.id,
|
|
2087
|
-
order,
|
|
2088
|
-
tool: item.name,
|
|
2089
|
-
...call ? { call } : {}
|
|
2090
|
-
};
|
|
2091
|
-
calls.set(item.id, recordedCall);
|
|
2092
|
-
recordedCalls.push(recordedCall);
|
|
2093
|
-
}
|
|
2094
|
-
continue;
|
|
2095
|
-
}
|
|
2096
|
-
if (message.role !== "toolResult")
|
|
2097
|
-
continue;
|
|
2098
|
-
if (typeof message.toolCallId !== "string" || typeof message.toolName !== "string" || typeof message.isError !== "boolean" || !Array.isArray(message.content) || resultCallIds.has(message.toolCallId)) {
|
|
2099
|
-
isStructurallyValid = false;
|
|
2100
|
-
continue;
|
|
2101
|
-
}
|
|
2102
|
-
resultCallIds.add(message.toolCallId);
|
|
2103
|
-
const recorded = calls.get(message.toolCallId);
|
|
2104
|
-
if (recorded?.tool !== message.toolName) {
|
|
2105
|
-
isStructurallyValid = false;
|
|
2106
|
-
continue;
|
|
2107
|
-
}
|
|
2108
|
-
if (message.isError) {
|
|
2109
|
-
const output = diagnosticTextContent(message.content);
|
|
2110
|
-
diagnostics.push({
|
|
2111
|
-
tool: message.toolName,
|
|
2112
|
-
callId: message.toolCallId,
|
|
2113
|
-
order,
|
|
2114
|
-
...recorded.call ? { call: recorded.call } : {},
|
|
2115
|
-
...output ? { output } : {}
|
|
2116
|
-
});
|
|
2117
|
-
}
|
|
2118
|
-
}
|
|
2119
|
-
return {
|
|
2120
|
-
replaySafe: isCompleteTranscript && isStructurallyValid && initialDelegationTask(transcript) === expectation.task && recordedCalls.every((call) => isReplaySafeToolCall({
|
|
2121
|
-
call,
|
|
2122
|
-
diagnostics,
|
|
2123
|
-
bashPermission: expectation.bashPermission
|
|
2124
|
-
})),
|
|
2125
|
-
toolCount: recordedCalls.length
|
|
2126
|
-
};
|
|
2127
|
-
};
|
|
2128
|
-
// src/integrations/subagents/session-diagnostics.ts
|
|
2129
|
-
import { constants } from "node:fs";
|
|
2130
|
-
import { lstat, open, realpath as realpath2 } from "node:fs/promises";
|
|
2131
|
-
import {
|
|
2132
|
-
basename as basename2,
|
|
2133
|
-
dirname as dirname2,
|
|
2134
|
-
isAbsolute as isAbsolute3,
|
|
2135
|
-
join as join4,
|
|
2136
|
-
relative as relative2,
|
|
2137
|
-
resolve as resolve3,
|
|
2138
|
-
sep as sep2
|
|
2139
|
-
} from "node:path";
|
|
2140
|
-
var SESSION_FILE_NAME = "session.jsonl";
|
|
2141
|
-
var SESSION_RUN_DIRECTORY = /^run-\d+$/;
|
|
2142
|
-
var SESSION_FILE_SUFFIX = ".jsonl";
|
|
2143
|
-
var MAX_SESSION_TAIL_BYTES = 1024 * 1024;
|
|
2144
|
-
var DEFAULT_DIAGNOSTIC_DEPENDENCIES = {
|
|
2145
|
-
fileSystem: {
|
|
2146
|
-
inspect: lstat,
|
|
2147
|
-
realPath: realpath2,
|
|
2148
|
-
openReadOnlyNoFollow: (path) => open(path, constants.O_RDONLY | constants.O_NOFOLLOW)
|
|
2149
|
-
}
|
|
2150
|
-
};
|
|
2151
|
-
var isSessionFilePath = (path) => isAbsolute3(path) && !path.includes("\x00") && basename2(path) === SESSION_FILE_NAME && SESSION_RUN_DIRECTORY.test(basename2(dirname2(path)));
|
|
2152
|
-
var pathIsWithin = (root, candidate) => {
|
|
2153
|
-
const relativePath = relative2(resolve3(root), resolve3(candidate));
|
|
2154
|
-
return relativePath !== "" && relativePath !== ".." && !relativePath.startsWith(`..${sep2}`) && !isAbsolute3(relativePath);
|
|
2155
|
-
};
|
|
2156
|
-
var isValidSessionIdentity = (identity) => identity.runId.length > 0 && !identity.runId.includes("\x00") && basename2(identity.runId) === identity.runId && identity.runId !== "." && identity.runId !== ".." && Number.isSafeInteger(identity.childIndex) && identity.childIndex >= 0;
|
|
2157
|
-
var readStableTail = async (handle) => {
|
|
2158
|
-
const opened = await handle.stat();
|
|
2159
|
-
if (!opened.isFile())
|
|
2160
|
-
return;
|
|
2161
|
-
const bytesToRead = Math.min(opened.size, MAX_SESSION_TAIL_BYTES);
|
|
2162
|
-
if (bytesToRead === 0)
|
|
2163
|
-
return { content: "", truncated: false };
|
|
2164
|
-
const start = opened.size - bytesToRead;
|
|
2165
|
-
const buffer = Buffer.alloc(bytesToRead);
|
|
2166
|
-
const { bytesRead } = await handle.read(buffer, 0, bytesToRead, start);
|
|
2167
|
-
const afterRead = await handle.stat();
|
|
2168
|
-
const didFileChange = afterRead.dev !== opened.dev || afterRead.ino !== opened.ino || afterRead.size !== opened.size || afterRead.mtimeMs !== opened.mtimeMs;
|
|
2169
|
-
if (didFileChange)
|
|
2170
|
-
return;
|
|
2171
|
-
let content = buffer.subarray(0, bytesRead).toString("utf8");
|
|
2172
|
-
if (start > 0) {
|
|
2173
|
-
const firstNewline = content.indexOf(`
|
|
2174
|
-
`);
|
|
2175
|
-
content = firstNewline === -1 ? "" : content.slice(firstNewline + 1);
|
|
2176
|
-
}
|
|
2177
|
-
return { content, truncated: start > 0 };
|
|
2178
|
-
};
|
|
2179
|
-
var readContainedSessionTail = async ({
|
|
2180
|
-
sessionFile,
|
|
2181
|
-
trustedRoot,
|
|
2182
|
-
identity,
|
|
2183
|
-
dependencies
|
|
2184
|
-
}) => {
|
|
2185
|
-
const expectedSessionFile = resolve3(trustedRoot, identity.runId, `run-${identity.childIndex}`, SESSION_FILE_NAME);
|
|
2186
|
-
if (!isSessionFilePath(sessionFile) || !isAbsolute3(trustedRoot) || trustedRoot.includes("\x00") || !pathIsWithin(trustedRoot, sessionFile) || !isValidSessionIdentity(identity) || resolve3(sessionFile) !== expectedSessionFile) {
|
|
2187
|
-
return;
|
|
2188
|
-
}
|
|
2189
|
-
const runDirectory = resolve3(trustedRoot, identity.runId);
|
|
2190
|
-
const childDirectory = resolve3(runDirectory, `run-${identity.childIndex}`);
|
|
2191
|
-
const resolvedSessionFile = resolve3(sessionFile);
|
|
2192
|
-
const [runDirectoryInfo, childDirectoryInfo, inspected] = await Promise.all([
|
|
2193
|
-
dependencies.fileSystem.inspect(runDirectory),
|
|
2194
|
-
dependencies.fileSystem.inspect(childDirectory),
|
|
2195
|
-
dependencies.fileSystem.inspect(resolvedSessionFile)
|
|
2196
|
-
]);
|
|
2197
|
-
if (runDirectoryInfo.isSymbolicLink() || !runDirectoryInfo.isDirectory() || childDirectoryInfo.isSymbolicLink() || !childDirectoryInfo.isDirectory() || inspected.isSymbolicLink() || !inspected.isFile()) {
|
|
2198
|
-
return;
|
|
2199
|
-
}
|
|
2200
|
-
const [canonicalRoot, canonicalSessionFile] = await Promise.all([
|
|
2201
|
-
dependencies.fileSystem.realPath(trustedRoot),
|
|
2202
|
-
dependencies.fileSystem.realPath(resolvedSessionFile)
|
|
2203
|
-
]);
|
|
2204
|
-
const canonicalExpectedSessionFile = resolve3(canonicalRoot, identity.runId, `run-${identity.childIndex}`, SESSION_FILE_NAME);
|
|
2205
|
-
if (!pathIsWithin(canonicalRoot, canonicalSessionFile) || canonicalSessionFile !== canonicalExpectedSessionFile) {
|
|
2206
|
-
return;
|
|
2207
|
-
}
|
|
2208
|
-
const handle = await dependencies.fileSystem.openReadOnlyNoFollow(canonicalSessionFile);
|
|
2209
|
-
try {
|
|
2210
|
-
return await readStableTail(handle);
|
|
2211
|
-
} finally {
|
|
2212
|
-
await handle.close();
|
|
2213
|
-
}
|
|
2214
|
-
};
|
|
2215
|
-
var deriveSubagentSessionRoot = (parentSessionFile) => {
|
|
2216
|
-
if (!parentSessionFile || !isAbsolute3(parentSessionFile) || parentSessionFile.includes("\x00")) {
|
|
2217
|
-
return;
|
|
2218
|
-
}
|
|
2219
|
-
const parentName = basename2(parentSessionFile);
|
|
2220
|
-
if (!parentName.endsWith(SESSION_FILE_SUFFIX) || parentName === SESSION_FILE_SUFFIX) {
|
|
2221
|
-
return;
|
|
2222
|
-
}
|
|
2223
|
-
return join4(dirname2(parentSessionFile), parentName.slice(0, -SESSION_FILE_SUFFIX.length));
|
|
2224
|
-
};
|
|
2225
|
-
var readToolFailureDiagnostic = async (sessionFile, trustedRoot, identity, expectedTool, terminalError, dependencies = DEFAULT_DIAGNOSTIC_DEPENDENCIES) => {
|
|
2226
|
-
if (!sessionFile || !trustedRoot || !identity)
|
|
2227
|
-
return;
|
|
2228
|
-
try {
|
|
2229
|
-
const tail = await readContainedSessionTail({
|
|
2230
|
-
sessionFile,
|
|
2231
|
-
trustedRoot,
|
|
2232
|
-
identity,
|
|
2233
|
-
dependencies
|
|
2234
|
-
});
|
|
2235
|
-
return parseToolFailureDiagnostic(tail?.content ?? "", expectedTool, terminalError, tail?.truncated !== true);
|
|
2236
|
-
} catch {
|
|
2237
|
-
return;
|
|
2238
|
-
}
|
|
2239
|
-
};
|
|
2240
|
-
var auditCompletedDelegationTranscript = async (sessionFile, trustedRoot, identity, dependencies = DEFAULT_DIAGNOSTIC_DEPENDENCIES) => {
|
|
2241
|
-
if (!sessionFile || !trustedRoot || !identity) {
|
|
2242
|
-
return {
|
|
2243
|
-
verified: false,
|
|
2244
|
-
reason: "completed response has no trusted child transcript identity"
|
|
2245
|
-
};
|
|
2246
|
-
}
|
|
2247
|
-
try {
|
|
2248
|
-
const tail = await readContainedSessionTail({
|
|
2249
|
-
sessionFile,
|
|
2250
|
-
trustedRoot,
|
|
2251
|
-
identity,
|
|
2252
|
-
dependencies
|
|
2253
|
-
});
|
|
2254
|
-
if (!tail) {
|
|
2255
|
-
return {
|
|
2256
|
-
verified: false,
|
|
2257
|
-
reason: "completed child transcript is missing, unstable, or untrusted"
|
|
2258
|
-
};
|
|
2259
|
-
}
|
|
2260
|
-
const parsed = parseFailureTranscript(tail.content);
|
|
2261
|
-
const completion = parsed.successfulCompletions.at(-1);
|
|
2262
|
-
if (!completion) {
|
|
2263
|
-
return {
|
|
2264
|
-
verified: false,
|
|
2265
|
-
reason: "completed child transcript does not contain a successful structured_output result"
|
|
2266
|
-
};
|
|
2267
|
-
}
|
|
2268
|
-
const warning = parsed.transcriptWarnings.find((candidate) => candidate.order > completion.order);
|
|
2269
|
-
if (warning)
|
|
2270
|
-
return { verified: true, warning: warning.content };
|
|
2271
|
-
if (!parsed.hasValidFalsePositiveProof || completion.order !== parsed.lastInteractionOrder) {
|
|
2272
|
-
return {
|
|
2273
|
-
verified: false,
|
|
2274
|
-
reason: "completed child transcript has malformed or later terminal interactions"
|
|
2275
|
-
};
|
|
2276
|
-
}
|
|
2277
|
-
return { verified: true };
|
|
2278
|
-
} catch {
|
|
2279
|
-
return {
|
|
2280
|
-
verified: false,
|
|
2281
|
-
reason: "completed child transcript could not be read safely"
|
|
2282
|
-
};
|
|
2283
|
-
}
|
|
2284
|
-
};
|
|
2285
|
-
var readDelegationReplayAudit = async (sessionFile, trustedRoot, identity, expectation, dependencies = DEFAULT_DIAGNOSTIC_DEPENDENCIES) => {
|
|
2286
|
-
if (!sessionFile || !trustedRoot || !identity)
|
|
2287
|
-
return;
|
|
2288
|
-
try {
|
|
2289
|
-
const tail = await readContainedSessionTail({
|
|
2290
|
-
sessionFile,
|
|
2291
|
-
trustedRoot,
|
|
2292
|
-
identity,
|
|
2293
|
-
dependencies
|
|
2294
|
-
});
|
|
2295
|
-
return tail ? parseDelegationReplayAudit(tail.content, expectation, !tail.truncated) : undefined;
|
|
2296
|
-
} catch {
|
|
2297
|
-
return;
|
|
2298
|
-
}
|
|
2299
|
-
};
|
|
2300
|
-
// src/integrations/subagents/child-policy-envelope.ts
|
|
2301
|
-
import { basename as basename4, dirname as dirname5, resolve as resolve6 } from "node:path";
|
|
2302
|
-
|
|
2303
|
-
// src/integrations/subagents/child-policy-paths.ts
|
|
2304
|
-
import { tmpdir } from "node:os";
|
|
2305
|
-
import { basename as basename3, dirname as dirname3, relative as relative3, resolve as resolve4 } from "node:path";
|
|
2306
|
-
var RESULT_FILE_NAME = "result.json";
|
|
2307
|
-
var CAPABILITY_FILE_NAME = "capability";
|
|
2308
|
-
var RESULT_DIRECTORY_PREFIX = "pi-workflows-step-";
|
|
2309
|
-
var DEFAULT_CHILD_POLICY_ENVIRONMENT = {
|
|
2310
|
-
temporaryDirectory: tmpdir
|
|
2311
|
-
};
|
|
2312
|
-
var isSafeStepFilePath = ({
|
|
2313
|
-
path,
|
|
2314
|
-
expectedName,
|
|
2315
|
-
environment
|
|
2316
|
-
}) => {
|
|
2317
|
-
const temporaryRoot = resolve4(environment.temporaryDirectory());
|
|
2318
|
-
const candidate = resolve4(path);
|
|
2319
|
-
const relativePath = relative3(temporaryRoot, candidate);
|
|
2320
|
-
return relativePath !== "" && !relativePath.startsWith("..") && !relativePath.includes("\x00") && basename3(candidate) === expectedName && basename3(dirname3(candidate)).startsWith(RESULT_DIRECTORY_PREFIX);
|
|
2321
|
-
};
|
|
2322
|
-
var isSafeStepResultPath = (path, environment = DEFAULT_CHILD_POLICY_ENVIRONMENT) => isSafeStepFilePath({
|
|
2323
|
-
path,
|
|
2324
|
-
expectedName: RESULT_FILE_NAME,
|
|
2325
|
-
environment
|
|
2326
|
-
});
|
|
2327
|
-
var isSafeStepCapabilityPath = (path, environment = DEFAULT_CHILD_POLICY_ENVIRONMENT) => isSafeStepFilePath({
|
|
2328
|
-
path,
|
|
2329
|
-
expectedName: CAPABILITY_FILE_NAME,
|
|
2330
|
-
environment
|
|
2331
|
-
});
|
|
2332
|
-
|
|
2333
|
-
// src/integrations/subagents/child-policy-validation.ts
|
|
2334
|
-
import { dirname as dirname4, isAbsolute as isAbsolute5, resolve as resolve5 } from "node:path";
|
|
2335
|
-
|
|
2336
|
-
// src/integrations/subagents/child-policy-sections.ts
|
|
2337
|
-
import { isAbsolute as isAbsolute4, win32 as win322 } from "node:path";
|
|
2338
|
-
var isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2339
|
-
var isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
2340
|
-
var hasOnlyKeys = (value, allowed) => Object.keys(value).every((key) => allowed.has(key));
|
|
2341
|
-
var isStepPermissions = (value) => {
|
|
2342
|
-
if (!isRecord(value) || !isRecord(value.bash))
|
|
2343
|
-
return false;
|
|
2344
|
-
const bash = value.bash;
|
|
2345
|
-
const bashRules = Array.isArray(bash.allow) ? bash.allow : undefined;
|
|
2346
|
-
const isValidMode = bash.mode === "deny" || bash.mode === "allow-list" || bash.mode === "unrestricted";
|
|
2347
|
-
const hasValidRules = bashRules !== undefined && bashRules.every((rule) => isRecord(rule) && hasOnlyKeys(rule, new Set(["executable", "argsPrefix"])) && typeof rule.executable === "string" && isStringArray(rule.argsPrefix));
|
|
2348
|
-
return hasOnlyKeys(value, new Set(["tools", "mcp", "extensions", "skills", "bash"])) && hasOnlyKeys(bash, new Set(["mode", "allow"])) && isStringArray(value.tools) && isStringArray(value.mcp) && isStringArray(value.extensions) && isStringArray(value.skills) && isValidMode && hasValidRules && (bash.mode !== "allow-list" || bashRules.length > 0);
|
|
2349
|
-
};
|
|
2350
|
-
var parsePermissions2 = (value) => {
|
|
2351
|
-
if (!isStepPermissions(value.permissions)) {
|
|
2352
|
-
throw new Error("child policy permissions are invalid");
|
|
2353
|
-
}
|
|
2354
|
-
return { permissions: value.permissions };
|
|
2355
|
-
};
|
|
2356
|
-
var parseOutcomes = (value) => {
|
|
2357
|
-
const outcomes = value.outcomes;
|
|
2358
|
-
if (!isStringArray(outcomes) || outcomes.length === 0 || new Set(outcomes).size !== outcomes.length) {
|
|
2359
|
-
throw new Error("child policy outcomes are invalid");
|
|
2360
|
-
}
|
|
2361
|
-
const pauseOutcomes = value.pauseOutcomes;
|
|
2362
|
-
if (!isStringArray(pauseOutcomes) || new Set(pauseOutcomes).size !== pauseOutcomes.length || pauseOutcomes.some((outcome) => !outcomes.includes(outcome))) {
|
|
2363
|
-
throw new Error("child policy pause outcomes are invalid");
|
|
2364
|
-
}
|
|
2365
|
-
const summaryMaxChars = value.summaryMaxChars;
|
|
2366
|
-
if (typeof summaryMaxChars !== "number" || !Number.isInteger(summaryMaxChars) || summaryMaxChars < 100 || summaryMaxChars > 50000) {
|
|
2367
|
-
throw new Error("child policy summaryMaxChars is invalid");
|
|
2368
|
-
}
|
|
2369
|
-
const gateSubmitOutcome = value.gateSubmitOutcome;
|
|
2370
|
-
if (gateSubmitOutcome !== undefined && (typeof gateSubmitOutcome !== "string" || !outcomes.includes(gateSubmitOutcome))) {
|
|
2371
|
-
throw new Error("child policy gate outcome is invalid");
|
|
2372
|
-
}
|
|
2373
|
-
return {
|
|
2374
|
-
outcomes,
|
|
2375
|
-
pauseOutcomes,
|
|
2376
|
-
summaryMaxChars,
|
|
2377
|
-
...gateSubmitOutcome === undefined ? {} : { gateSubmitOutcome }
|
|
2378
|
-
};
|
|
2379
|
-
};
|
|
2380
|
-
var parseWorkspace2 = (value, outcomes) => {
|
|
2381
|
-
if (value.workspace === undefined)
|
|
2382
|
-
return {};
|
|
2383
|
-
if (!isRecord(value.workspace) || !hasOnlyKeys(value.workspace, new Set(["bindOn", "allowedRoots"]))) {
|
|
2384
|
-
throw new Error("child policy workspace is invalid");
|
|
2385
|
-
}
|
|
2386
|
-
const bindOn = value.workspace.bindOn;
|
|
2387
|
-
const allowedRoots = value.workspace.allowedRoots;
|
|
2388
|
-
if (!isStringArray(bindOn) || bindOn.length === 0 || new Set(bindOn).size !== bindOn.length || bindOn.some((outcome) => !outcomes.includes(outcome))) {
|
|
2389
|
-
throw new Error("child policy workspace bindOn outcomes are invalid");
|
|
2390
|
-
}
|
|
2391
|
-
if (!isStringArray(allowedRoots) || allowedRoots.length === 0 || allowedRoots.length > MAX_WORKSPACE_ALLOWED_ROOTS || new Set(allowedRoots).size !== allowedRoots.length || allowedRoots.some((root) => !root.trim() || root !== root.trim() || root.length > MAX_WORKSPACE_PATH_CHARS || root.includes("\x00") || win322.parse(root).root !== "" && !isAbsolute4(root))) {
|
|
2392
|
-
throw new Error("child policy workspace allowed roots are invalid");
|
|
2393
|
-
}
|
|
2394
|
-
return { workspace: { bindOn, allowedRoots } };
|
|
2395
|
-
};
|
|
2396
|
-
var parseChildPolicySections = (value) => {
|
|
2397
|
-
const outcomeSections = parseOutcomes(value);
|
|
2398
|
-
return {
|
|
2399
|
-
...parsePermissions2(value),
|
|
2400
|
-
...outcomeSections,
|
|
2401
|
-
...parseWorkspace2(value, outcomeSections.outcomes)
|
|
2402
|
-
};
|
|
2403
|
-
};
|
|
2404
|
-
|
|
2405
|
-
// src/integrations/subagents/child-policy-validation.ts
|
|
2406
|
-
var POLICY_DIGEST_PATTERN = /^[a-f0-9]{64}$/;
|
|
2407
|
-
var CAPABILITY_TOKEN_PATTERN = /^[a-f0-9]{64}$/;
|
|
2408
|
-
var POLICY_KEYS = new Set([
|
|
2409
|
-
"version",
|
|
2410
|
-
"requestId",
|
|
2411
|
-
"agent",
|
|
2412
|
-
"workflowId",
|
|
2413
|
-
"runId",
|
|
2414
|
-
"stepId",
|
|
2415
|
-
"stepTitle",
|
|
2416
|
-
"cwd",
|
|
2417
|
-
"policyDigest",
|
|
2418
|
-
"capabilityPath",
|
|
2419
|
-
"capabilityToken",
|
|
2420
|
-
"resultPath",
|
|
2421
|
-
"permissions",
|
|
2422
|
-
"outcomes",
|
|
2423
|
-
"pauseOutcomes",
|
|
2424
|
-
"summaryMaxChars",
|
|
2425
|
-
"gateSubmitOutcome",
|
|
2426
|
-
"workspace"
|
|
2427
|
-
]);
|
|
2428
|
-
var isRecord2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2429
|
-
var requiredString = (value, field) => {
|
|
2430
|
-
const candidate = value[field];
|
|
2431
|
-
if (typeof candidate !== "string" || !candidate) {
|
|
2432
|
-
throw new Error(`child policy ${field} must be a non-empty string`);
|
|
2433
|
-
}
|
|
2434
|
-
return candidate;
|
|
2435
|
-
};
|
|
2436
|
-
var isSubagentRuntimeName = (name) => Boolean(name && SUBAGENT_RUNTIME_NAME_PATTERN.test(name));
|
|
2437
|
-
var rejectUnknownProperties = (value) => {
|
|
2438
|
-
const unknownKey = Object.keys(value).find((key) => !POLICY_KEYS.has(key));
|
|
2439
|
-
if (unknownKey) {
|
|
2440
|
-
throw new Error(`child policy has unknown property "${unknownKey}"`);
|
|
2441
|
-
}
|
|
2442
|
-
};
|
|
2443
|
-
var parseIdentityAndPaths = (value, environment) => {
|
|
2444
|
-
const requestId = requiredString(value, "requestId");
|
|
2445
|
-
const agent = requiredString(value, "agent");
|
|
2446
|
-
const workflowId = requiredString(value, "workflowId");
|
|
2447
|
-
const runId = requiredString(value, "runId");
|
|
2448
|
-
const stepId = requiredString(value, "stepId");
|
|
2449
|
-
const stepTitle = requiredString(value, "stepTitle");
|
|
2450
|
-
const cwd = requiredString(value, "cwd");
|
|
2451
|
-
const policyDigest = requiredString(value, "policyDigest");
|
|
2452
|
-
const capabilityPath = requiredString(value, "capabilityPath");
|
|
2453
|
-
const capabilityToken = requiredString(value, "capabilityToken");
|
|
2454
|
-
const resultPath = requiredString(value, "resultPath");
|
|
2455
|
-
if (value.version !== 1)
|
|
2456
|
-
throw new Error("unsupported child policy version");
|
|
2457
|
-
if (!isAbsolute5(cwd)) {
|
|
2458
|
-
throw new Error("child policy cwd must be an absolute path");
|
|
2459
|
-
}
|
|
2460
|
-
if (!POLICY_DIGEST_PATTERN.test(policyDigest)) {
|
|
2461
|
-
throw new Error("child policy digest is invalid");
|
|
2462
|
-
}
|
|
2463
|
-
if (!isSubagentRuntimeName(agent)) {
|
|
2464
|
-
throw new Error("child policy agent is not a valid subagent runtime name");
|
|
2465
|
-
}
|
|
2466
|
-
if (!CAPABILITY_TOKEN_PATTERN.test(capabilityToken)) {
|
|
2467
|
-
throw new Error("child policy capability token is invalid");
|
|
2468
|
-
}
|
|
2469
|
-
if (!isSafeStepCapabilityPath(capabilityPath, environment)) {
|
|
2470
|
-
throw new Error("child policy capability path is outside its temporary directory");
|
|
2471
|
-
}
|
|
2472
|
-
if (!isSafeStepResultPath(resultPath, environment)) {
|
|
2473
|
-
throw new Error("child policy result path is outside its temporary directory");
|
|
2474
|
-
}
|
|
2475
|
-
if (dirname4(resolve5(capabilityPath)) !== dirname4(resolve5(resultPath))) {
|
|
2476
|
-
throw new Error("child policy files must share one temporary directory");
|
|
2477
|
-
}
|
|
2478
|
-
return {
|
|
2479
|
-
version: 1,
|
|
2480
|
-
requestId,
|
|
2481
|
-
agent,
|
|
2482
|
-
workflowId,
|
|
2483
|
-
runId,
|
|
2484
|
-
stepId,
|
|
2485
|
-
stepTitle,
|
|
2486
|
-
cwd,
|
|
2487
|
-
policyDigest,
|
|
2488
|
-
capabilityPath,
|
|
2489
|
-
capabilityToken,
|
|
2490
|
-
resultPath
|
|
2491
|
-
};
|
|
2492
|
-
};
|
|
2493
|
-
var parseChildPolicy = (value, environment = DEFAULT_CHILD_POLICY_ENVIRONMENT) => {
|
|
2494
|
-
if (!isRecord2(value))
|
|
2495
|
-
throw new Error("child policy must be an object");
|
|
2496
|
-
rejectUnknownProperties(value);
|
|
2497
|
-
return {
|
|
2498
|
-
...parseIdentityAndPaths(value, environment),
|
|
2499
|
-
...parseChildPolicySections(value)
|
|
2500
|
-
};
|
|
2501
|
-
};
|
|
2502
|
-
|
|
2503
|
-
// src/integrations/subagents/child-policy-envelope.ts
|
|
2504
|
-
var CHILD_POLICY_OPEN = "<pi-workflows-policy-v1>";
|
|
2505
|
-
var CHILD_POLICY_CLOSE = "</pi-workflows-policy-v1>";
|
|
2506
|
-
var UPSTREAM_TASK_PREFIX = "Task: ";
|
|
2507
|
-
var UPSTREAM_TASK_FILE_OPEN = '<file name="';
|
|
2508
|
-
var UPSTREAM_TASK_FILE_HEADER_CLOSE = `">
|
|
2509
|
-
`;
|
|
2510
|
-
var UPSTREAM_TASK_FILE_CLOSE = `
|
|
2511
|
-
</file>
|
|
2512
|
-
`;
|
|
2513
|
-
var UPSTREAM_TASK_DIRECTORY_PREFIX = "pi-subagent-";
|
|
2514
|
-
var encodeChildPolicy = (policy) => {
|
|
2515
|
-
const encoded = Buffer.from(JSON.stringify(policy), "utf8").toString("base64url");
|
|
2516
|
-
return `${CHILD_POLICY_OPEN}${encoded}${CHILD_POLICY_CLOSE}`;
|
|
2517
|
-
};
|
|
2518
|
-
var unwrapTaskFile = ({
|
|
2519
|
-
text,
|
|
2520
|
-
environment
|
|
2521
|
-
}) => {
|
|
2522
|
-
if (!text.startsWith(UPSTREAM_TASK_FILE_OPEN) || !text.endsWith(UPSTREAM_TASK_FILE_CLOSE)) {
|
|
2523
|
-
return;
|
|
2524
|
-
}
|
|
2525
|
-
const pathStart = UPSTREAM_TASK_FILE_OPEN.length;
|
|
2526
|
-
const headerEnd = text.indexOf(UPSTREAM_TASK_FILE_HEADER_CLOSE, pathStart);
|
|
2527
|
-
if (headerEnd === -1)
|
|
2528
|
-
return;
|
|
2529
|
-
const taskFilePath = text.slice(pathStart, headerEnd);
|
|
2530
|
-
const taskDirectory = dirname5(resolve6(taskFilePath));
|
|
2531
|
-
const isExpectedTaskFile = basename4(taskFilePath) === "task.md" && basename4(taskDirectory).startsWith(UPSTREAM_TASK_DIRECTORY_PREFIX) && dirname5(taskDirectory) === resolve6(environment.temporaryDirectory());
|
|
2532
|
-
if (!isExpectedTaskFile)
|
|
2533
|
-
return;
|
|
2534
|
-
const bodyStart = headerEnd + UPSTREAM_TASK_FILE_HEADER_CLOSE.length;
|
|
2535
|
-
const body = text.slice(bodyStart, -UPSTREAM_TASK_FILE_CLOSE.length);
|
|
2536
|
-
if (!body.startsWith(`${UPSTREAM_TASK_PREFIX}${CHILD_POLICY_OPEN}`)) {
|
|
2537
|
-
return;
|
|
2538
|
-
}
|
|
2539
|
-
return body.slice(UPSTREAM_TASK_PREFIX.length);
|
|
2540
|
-
};
|
|
2541
|
-
var unwrapUpstreamTask = ({
|
|
2542
|
-
text,
|
|
2543
|
-
environment
|
|
2544
|
-
}) => {
|
|
2545
|
-
if (text.startsWith(CHILD_POLICY_OPEN))
|
|
2546
|
-
return text;
|
|
2547
|
-
if (text.startsWith(`${UPSTREAM_TASK_PREFIX}${CHILD_POLICY_OPEN}`)) {
|
|
2548
|
-
return text.slice(UPSTREAM_TASK_PREFIX.length);
|
|
2549
|
-
}
|
|
2550
|
-
return unwrapTaskFile({ text, environment });
|
|
2551
|
-
};
|
|
2552
|
-
var decodePolicy = (encoded) => {
|
|
2553
|
-
try {
|
|
2554
|
-
return JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
2555
|
-
} catch {
|
|
2556
|
-
throw new Error("delegated task child policy cannot be decoded");
|
|
2557
|
-
}
|
|
2558
|
-
};
|
|
2559
|
-
var extractChildPolicy = (text, environment = DEFAULT_CHILD_POLICY_ENVIRONMENT) => {
|
|
2560
|
-
const taskWithPolicy = unwrapUpstreamTask({ text, environment });
|
|
2561
|
-
if (taskWithPolicy === undefined)
|
|
2562
|
-
return;
|
|
2563
|
-
const payloadStart = CHILD_POLICY_OPEN.length;
|
|
2564
|
-
const payloadEnd = taskWithPolicy.indexOf(CHILD_POLICY_CLOSE, payloadStart);
|
|
2565
|
-
const hasNestedEnvelope = taskWithPolicy.indexOf(CHILD_POLICY_OPEN, payloadStart) !== -1;
|
|
2566
|
-
if (payloadEnd === -1 || hasNestedEnvelope) {
|
|
2567
|
-
throw new Error("delegated task contains an invalid child policy envelope");
|
|
2568
|
-
}
|
|
2569
|
-
const encoded = taskWithPolicy.slice(payloadStart, payloadEnd);
|
|
2570
|
-
const task = taskWithPolicy.slice(payloadEnd + CHILD_POLICY_CLOSE.length).trim();
|
|
2571
|
-
if (!task)
|
|
2572
|
-
throw new Error("delegated task is empty after policy extraction");
|
|
2573
|
-
return {
|
|
2574
|
-
policy: parseChildPolicy(decodePolicy(encoded), environment),
|
|
2575
|
-
task
|
|
2576
|
-
};
|
|
2577
|
-
};
|
|
2578
|
-
// src/runtime/step-result.ts
|
|
2579
|
-
import { isAbsolute as isAbsolute6 } from "node:path";
|
|
2580
|
-
var MAX_ARTIFACT_CHARS = 200000;
|
|
2581
|
-
var RESULT_KEYS = new Set([
|
|
2582
|
-
"version",
|
|
2583
|
-
"policyDigest",
|
|
2584
|
-
"outcome",
|
|
2585
|
-
"summary",
|
|
2586
|
-
"artifact",
|
|
2587
|
-
"workspace"
|
|
2588
|
-
]);
|
|
2589
|
-
var isObject = (value) => {
|
|
2590
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2591
|
-
};
|
|
2592
|
-
function parseResultWorkspace(value, outcome, policy) {
|
|
2593
|
-
const requiresWorkspace = policy.workspace?.bindOn.includes(outcome) === true;
|
|
2594
|
-
if (!requiresWorkspace) {
|
|
2595
|
-
if (value !== undefined) {
|
|
2596
|
-
throw new Error("workflow step workspace is forbidden for this outcome");
|
|
2597
|
-
}
|
|
2598
|
-
return;
|
|
2599
|
-
}
|
|
2600
|
-
if (!isObject(value)) {
|
|
2601
|
-
throw new Error(`workflow step outcome "${outcome}" requires workspace.cwd`);
|
|
2602
|
-
}
|
|
2603
|
-
const unknownKey = Object.keys(value).find((key) => key !== "cwd");
|
|
2604
|
-
if (unknownKey) {
|
|
2605
|
-
throw new Error(`workflow step workspace has unknown property "${unknownKey}"`);
|
|
2606
|
-
}
|
|
2607
|
-
if (typeof value.cwd !== "string") {
|
|
2608
|
-
throw new Error("workflow step workspace cwd must be a string");
|
|
2609
|
-
}
|
|
2610
|
-
const cwd = value.cwd;
|
|
2611
|
-
if (!cwd || cwd.includes("\x00") || !isAbsolute6(cwd)) {
|
|
2612
|
-
throw new Error("workflow step workspace cwd must be an absolute path");
|
|
2613
|
-
}
|
|
2614
|
-
if (cwd.length > MAX_WORKSPACE_PATH_CHARS) {
|
|
2615
|
-
throw new Error(`workflow step workspace cwd exceeds ${MAX_WORKSPACE_PATH_CHARS} characters`);
|
|
2616
|
-
}
|
|
2617
|
-
return { cwd };
|
|
2618
|
-
}
|
|
2619
|
-
function parseWorkflowStepResult(value, policy) {
|
|
2620
|
-
if (!isObject(value)) {
|
|
2621
|
-
throw new Error("workflow step result must be an object");
|
|
2622
|
-
}
|
|
2623
|
-
const unknownKey = Object.keys(value).find((key) => !RESULT_KEYS.has(key));
|
|
2624
|
-
if (unknownKey) {
|
|
2625
|
-
throw new Error(`workflow step result has unknown property "${unknownKey}"`);
|
|
2626
|
-
}
|
|
2627
|
-
if (value.version !== 1) {
|
|
2628
|
-
throw new Error("unsupported workflow step result version");
|
|
2629
|
-
}
|
|
2630
|
-
if (value.policyDigest !== policy.policyDigest) {
|
|
2631
|
-
throw new Error("workflow step result does not match the active policy");
|
|
2632
|
-
}
|
|
2633
|
-
if (typeof value.outcome !== "string" || !policy.outcomes.includes(value.outcome)) {
|
|
2634
|
-
throw new Error(`workflow step returned invalid outcome "${String(value.outcome)}"`);
|
|
2635
|
-
}
|
|
2636
|
-
if (typeof value.summary !== "string") {
|
|
2637
|
-
throw new Error("workflow step summary must be a string");
|
|
2638
|
-
}
|
|
2639
|
-
const summary = value.summary.trim();
|
|
2640
|
-
if (!summary) {
|
|
2641
|
-
throw new Error("workflow step summary must not be empty");
|
|
2642
|
-
}
|
|
2643
|
-
if (summary.length > policy.summaryMaxChars) {
|
|
2644
|
-
throw new Error(`workflow step summary exceeds ${policy.summaryMaxChars} characters`);
|
|
2645
|
-
}
|
|
2646
|
-
if (value.artifact !== undefined && typeof value.artifact !== "string") {
|
|
2647
|
-
throw new Error("workflow step artifact must be a string");
|
|
2648
|
-
}
|
|
2649
|
-
const artifact = typeof value.artifact === "string" ? value.artifact : undefined;
|
|
2650
|
-
if (artifact !== undefined && artifact.length > MAX_ARTIFACT_CHARS) {
|
|
2651
|
-
throw new Error(`workflow step artifact exceeds ${MAX_ARTIFACT_CHARS} characters`);
|
|
2652
|
-
}
|
|
2653
|
-
if (value.outcome === policy.gateSubmitOutcome && (!artifact || !artifact.trim())) {
|
|
2654
|
-
throw new Error("workflow gate outcome requires a non-empty artifact");
|
|
2655
|
-
}
|
|
2656
|
-
const workspace = parseResultWorkspace(value.workspace, value.outcome, policy);
|
|
2657
|
-
return {
|
|
2658
|
-
version: 1,
|
|
2659
|
-
policyDigest: policy.policyDigest,
|
|
2660
|
-
outcome: value.outcome,
|
|
2661
|
-
summary,
|
|
2662
|
-
...artifact !== undefined ? { artifact } : {},
|
|
2663
|
-
...workspace ? { workspace } : {}
|
|
2664
|
-
};
|
|
2665
|
-
}
|
|
2666
|
-
|
|
2667
|
-
// src/integrations/subagents/delegated-result.ts
|
|
2668
|
-
var parseDelegatedStepResult = (value, policy) => {
|
|
2669
|
-
try {
|
|
2670
|
-
return parseWorkflowStepResult(value, {
|
|
2671
|
-
policyDigest: policy.policyDigest,
|
|
2672
|
-
outcomes: [...policy.outcomes],
|
|
2673
|
-
summaryMaxChars: policy.summaryMaxChars,
|
|
2674
|
-
...policy.gateSubmitOutcome ? { gateSubmitOutcome: policy.gateSubmitOutcome } : {},
|
|
2675
|
-
...policy.workspace ? { workspace: policy.workspace } : {}
|
|
2676
|
-
});
|
|
2677
|
-
} catch (error) {
|
|
2678
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2679
|
-
throw new Error(message.replaceAll("workflow step", "delegated step"), {
|
|
2680
|
-
cause: error
|
|
2681
|
-
});
|
|
2682
|
-
}
|
|
2683
|
-
};
|
|
2684
|
-
// src/integrations/subagents/protocol-events.ts
|
|
2685
|
-
var SUBAGENT_DELEGATION_PROTOCOL_VERSION = 1;
|
|
2686
|
-
var SUBAGENT_DELEGATION_REQUEST_EVENT = "prompt-template:subagent:request";
|
|
2687
|
-
var SUBAGENT_DELEGATION_STARTED_EVENT = "prompt-template:subagent:started";
|
|
2688
|
-
var SUBAGENT_DELEGATION_UPDATE_EVENT = "prompt-template:subagent:update";
|
|
2689
|
-
var SUBAGENT_DELEGATION_RESPONSE_EVENT = "prompt-template:subagent:response";
|
|
2690
|
-
var SUBAGENT_DELEGATION_CANCEL_EVENT = "prompt-template:subagent:cancel";
|
|
2691
|
-
// src/harness/delegation-recovery-validation.ts
|
|
2692
|
-
function completionMatchesResult(diagnostic, result, policy) {
|
|
2693
|
-
const value = diagnostic.completionValue;
|
|
2694
|
-
if (!value)
|
|
2695
|
-
return false;
|
|
2696
|
-
const expectedKeys = [
|
|
2697
|
-
"outcome",
|
|
2698
|
-
"summary",
|
|
2699
|
-
...result.artifact === undefined ? [] : ["artifact"],
|
|
2700
|
-
...result.workspace === undefined ? [] : ["workspace"]
|
|
2701
|
-
].sort();
|
|
2702
|
-
const actualKeys = Object.keys(value).sort();
|
|
2703
|
-
if (actualKeys.length !== expectedKeys.length || !actualKeys.every((key, index) => key === expectedKeys[index])) {
|
|
2704
|
-
return false;
|
|
2705
|
-
}
|
|
2706
|
-
try {
|
|
2707
|
-
const completion = parseDelegatedStepResult({
|
|
2708
|
-
...value,
|
|
2709
|
-
version: 1,
|
|
2710
|
-
policyDigest: policy.policyDigest
|
|
2711
|
-
}, policy);
|
|
2712
|
-
return completion.outcome === result.outcome && completion.summary === result.summary && completion.artifact === result.artifact && completion.workspace?.cwd === result.workspace?.cwd;
|
|
2713
|
-
} catch {
|
|
2714
|
-
return false;
|
|
2715
|
-
}
|
|
2716
|
-
}
|
|
2717
|
-
function recoveredProjectionError(active, response, diagnostic) {
|
|
2718
|
-
if (response.agent !== active.agent) {
|
|
2719
|
-
return `terminal agent identity is ${JSON.stringify(response.agent)}; expected ${JSON.stringify(active.agent)}`;
|
|
2720
|
-
}
|
|
2721
|
-
if (response.childIndex !== 0) {
|
|
2722
|
-
return `terminal child index is ${JSON.stringify(response.childIndex)}; expected 0`;
|
|
2723
|
-
}
|
|
2724
|
-
if (typeof response.exitCode !== "number" || !Number.isSafeInteger(response.exitCode) || response.exitCode <= 0) {
|
|
2725
|
-
return `terminal exit code is ${JSON.stringify(response.exitCode)}; expected a positive safe integer`;
|
|
2726
|
-
}
|
|
2727
|
-
const execution = response.execution;
|
|
2728
|
-
if (!execution)
|
|
2729
|
-
return "terminal response has no execution projection";
|
|
2730
|
-
if (execution.status !== "failed" || execution.success) {
|
|
2731
|
-
return `execution projection is ${JSON.stringify({
|
|
2732
|
-
status: execution.status,
|
|
2733
|
-
success: execution.success
|
|
2734
|
-
})}; expected failed/false`;
|
|
2735
|
-
}
|
|
2736
|
-
if (execution.exitCode !== response.exitCode) {
|
|
2737
|
-
return `execution exit code ${JSON.stringify(execution.exitCode)} does not match terminal exit code ${JSON.stringify(response.exitCode)}`;
|
|
2738
|
-
}
|
|
2739
|
-
if (typeof response.error !== "string" || !response.error || typeof execution.error !== "string" || execution.error !== response.error) {
|
|
2740
|
-
return "terminal and execution errors are missing or do not match exactly";
|
|
2741
|
-
}
|
|
2742
|
-
const warnings = response.warnings;
|
|
2743
|
-
if (warnings !== undefined && (!Array.isArray(warnings) || warnings.some((warning) => typeof warning !== "string" || warning.trim().length > 0))) {
|
|
2744
|
-
return `terminal response contains warning evidence: ${JSON.stringify(warnings)}`;
|
|
2745
|
-
}
|
|
2746
|
-
if (diagnostic.transcriptToolCount !== undefined && response.toolCount !== undefined && response.toolCount !== diagnostic.transcriptToolCount) {
|
|
2747
|
-
return `terminal tool count ${response.toolCount} does not match transcript tool count ${diagnostic.transcriptToolCount}`;
|
|
2748
|
-
}
|
|
2749
|
-
if (diagnostic.transcriptTurnCount !== undefined && response.turns !== undefined && response.turns !== diagnostic.transcriptTurnCount) {
|
|
2750
|
-
return `terminal turn count ${response.turns} does not match transcript turn count ${diagnostic.transcriptTurnCount}`;
|
|
2751
|
-
}
|
|
2752
|
-
const toolFailure = response.error.match(/^\s*([a-z][\w-]*) failed\s*\(exit\s+(\d+)\)\s*:/i);
|
|
2753
|
-
if (!toolFailure) {
|
|
2754
|
-
return 'terminal error is not a recognized "<tool> failed (exit N): <detail>" failure';
|
|
2755
|
-
}
|
|
2756
|
-
const terminalTool = toolFailure[1];
|
|
2757
|
-
const terminalExitCodeText = toolFailure[2];
|
|
2758
|
-
if (!terminalTool || !terminalExitCodeText) {
|
|
2759
|
-
return "terminal error does not contain a complete tool failure projection";
|
|
2760
|
-
}
|
|
2761
|
-
const terminalExitCode = Number(terminalExitCodeText);
|
|
2762
|
-
if (terminalTool.toLowerCase() !== diagnostic.tool.toLowerCase() || terminalExitCode !== response.exitCode) {
|
|
2763
|
-
return `terminal tool/exit ${JSON.stringify({
|
|
2764
|
-
tool: terminalTool,
|
|
2765
|
-
exitCode: terminalExitCode
|
|
2766
|
-
})} does not match the correlated failure ${JSON.stringify({
|
|
2767
|
-
tool: diagnostic.tool,
|
|
2768
|
-
exitCode: response.exitCode
|
|
2769
|
-
})}`;
|
|
2770
|
-
}
|
|
2771
|
-
const unsafeFlag = [
|
|
2772
|
-
["interrupted", execution.interrupted],
|
|
2773
|
-
["timedOut", execution.timedOut],
|
|
2774
|
-
["stopped", execution.stopped],
|
|
2775
|
-
["detached", execution.detached]
|
|
2776
|
-
].find(([, isEnabled]) => isEnabled === true)?.[0];
|
|
2777
|
-
return unsafeFlag ? `execution projection reports ${unsafeFlag}=true` : undefined;
|
|
2778
|
-
}
|
|
2779
|
-
|
|
2780
|
-
// src/harness/delegation-retry-policy.ts
|
|
2781
|
-
var MAX_FAILURE_FIELD_CHARS = 1600;
|
|
2782
|
-
var MAX_DELEGATION_RECOVERY_ATTEMPTS = 2;
|
|
2783
|
-
var normalizedFingerprintField = (value) => value?.trim().replaceAll(/\s+/g, " ") || undefined;
|
|
2784
|
-
function delegationFailureFingerprint(failure) {
|
|
2785
|
-
return digest({
|
|
2786
|
-
status: failure.status,
|
|
2787
|
-
exitCode: failure.exitCode,
|
|
2788
|
-
error: normalizedFingerprintField(failure.error),
|
|
2789
|
-
recoveryBlocker: failure.recoveryBlocker,
|
|
2790
|
-
replayToolCount: failure.replayAudit?.toolCount,
|
|
2791
|
-
diagnostic: failure.diagnostic ? {
|
|
2792
|
-
tool: normalizedFingerprintField(failure.diagnostic.tool),
|
|
2793
|
-
call: normalizedFingerprintField(failure.diagnostic.call),
|
|
2794
|
-
output: normalizedFingerprintField(failure.diagnostic.output),
|
|
2795
|
-
postCompletionWarning: normalizedFingerprintField(failure.diagnostic.postCompletionWarning)
|
|
2796
|
-
} : undefined
|
|
2797
|
-
});
|
|
2798
|
-
}
|
|
2799
|
-
function boundedFailureField(value) {
|
|
2800
|
-
if (value.length <= MAX_FAILURE_FIELD_CHARS)
|
|
2801
|
-
return value;
|
|
2802
|
-
const marker = "… [truncated] …";
|
|
2803
|
-
const available = MAX_FAILURE_FIELD_CHARS - marker.length - 2;
|
|
2804
|
-
const startLength = Math.ceil(available / 2);
|
|
2805
|
-
const endLength = Math.floor(available / 2);
|
|
2806
|
-
return `${value.slice(0, startLength)}
|
|
2807
|
-
${marker}
|
|
2808
|
-
${value.slice(-endLength)}`;
|
|
2809
|
-
}
|
|
2810
|
-
function isRetryableTerminalFailure(failure) {
|
|
2811
|
-
if (failure.recoveryBlocker !== undefined)
|
|
2812
|
-
return false;
|
|
2813
|
-
if (failure.diagnostic?.postCompletionWarning !== undefined)
|
|
2814
|
-
return false;
|
|
2815
|
-
if (failure.status === "timed_out" || failure.status === "turn_budget_exhausted" || failure.status === "tool_budget_exhausted") {
|
|
2816
|
-
return true;
|
|
2817
|
-
}
|
|
2818
|
-
if (failure.status !== "failed" && failure.status !== "structured_output_failed") {
|
|
2819
|
-
return false;
|
|
2820
|
-
}
|
|
2821
|
-
return failure.error !== undefined || Number.isSafeInteger(failure.exitCode) && failure.exitCode !== 0;
|
|
2822
|
-
}
|
|
2823
|
-
function isSafeToRetryDelegation(policy, isReplayExplicitlyAuthorized, replayAudit) {
|
|
2824
|
-
return replayAudit?.replaySafe === true && (isReplayExplicitlyAuthorized || policy.permissions.bash.mode === "deny");
|
|
2825
|
-
}
|
|
2826
|
-
function rejectedRecoveryReason(failure, error) {
|
|
2827
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
2828
|
-
return `${failure.reason}
|
|
2829
|
-
Recovery rejected: ${boundedFailureField(detail)}`;
|
|
2830
|
-
}
|
|
2831
|
-
|
|
2832
|
-
// src/harness/delegation-failure.ts
|
|
2833
|
-
function nonEmptyTerminalError(response) {
|
|
2834
|
-
return [response.error, response.execution?.error].find((error) => typeof error === "string" && error.trim().length > 0);
|
|
2835
|
-
}
|
|
2836
|
-
function nonzeroTerminalExitCode(response) {
|
|
2837
|
-
return [response.exitCode, response.execution?.exitCode].find((exitCode) => typeof exitCode === "number" && Number.isSafeInteger(exitCode) && exitCode !== 0);
|
|
2838
|
-
}
|
|
2839
|
-
function hasContradictoryCompletion(response) {
|
|
2840
|
-
return response.status === "completed" && (nonEmptyTerminalError(response) !== undefined || nonzeroTerminalExitCode(response) !== undefined);
|
|
2841
|
-
}
|
|
2842
|
-
function recoveryBlocker(response) {
|
|
2843
|
-
const execution = response.execution;
|
|
2844
|
-
const fileMutation = response.effects?.fileMutation;
|
|
2845
|
-
if (fileMutation?.attempted === true || fileMutation?.status === "observed") {
|
|
2846
|
-
return "reported-mutation";
|
|
2847
|
-
}
|
|
2848
|
-
if (execution?.detached === true || execution?.status === "detached") {
|
|
2849
|
-
return "detached";
|
|
2850
|
-
}
|
|
2851
|
-
if (execution?.stopped === true || execution?.status === "stopped") {
|
|
2852
|
-
return "stopped";
|
|
2853
|
-
}
|
|
2854
|
-
if (response.status === "interrupted" || execution?.interrupted === true || execution?.status === "paused") {
|
|
2855
|
-
return "interrupted";
|
|
2856
|
-
}
|
|
2857
|
-
if (response.status === "cancelled")
|
|
2858
|
-
return "cancelled";
|
|
2859
|
-
if (execution?.timedOut === true && response.status !== "timed_out") {
|
|
2860
|
-
return "inconsistent-timeout";
|
|
2861
|
-
}
|
|
2862
|
-
return;
|
|
2863
|
-
}
|
|
2864
|
-
function validateReplayAudit(response, replayAudit) {
|
|
2865
|
-
if (!replayAudit)
|
|
2866
|
-
return;
|
|
2867
|
-
if (response.toolCount !== undefined && response.toolCount !== replayAudit.toolCount) {
|
|
2868
|
-
return { ...replayAudit, replaySafe: false };
|
|
2869
|
-
}
|
|
2870
|
-
return replayAudit;
|
|
2871
|
-
}
|
|
2872
|
-
function createDelegationFailureActions(dependencies) {
|
|
2873
|
-
const responseIdentity = (active, response) => {
|
|
2874
|
-
if (response.childIndex !== 0 || typeof response.runId !== "string" || response.agent !== undefined && response.agent !== active.agent) {
|
|
2875
|
-
return;
|
|
2876
|
-
}
|
|
2877
|
-
return { runId: response.runId, childIndex: 0 };
|
|
2878
|
-
};
|
|
2879
|
-
const completedResponseAudit = async (active, response) => {
|
|
2880
|
-
if (!dependencies.auditCompletedDelegationTranscript) {
|
|
2881
|
-
return {
|
|
2882
|
-
verified: false,
|
|
2883
|
-
reason: "completed transcript auditing is unavailable"
|
|
2884
|
-
};
|
|
2885
|
-
}
|
|
2886
|
-
return dependencies.auditCompletedDelegationTranscript(response.sessionFile, active.trustedSessionRoot, responseIdentity(active, response));
|
|
2887
|
-
};
|
|
2888
|
-
const describeDelegationFailure = async (active, response) => {
|
|
2889
|
-
const blocker = recoveryBlocker(response);
|
|
2890
|
-
const terminalError = nonEmptyTerminalError(response);
|
|
2891
|
-
const error = terminalError ?? "The subagent returned no terminal error details.";
|
|
2892
|
-
const identity = responseIdentity(active, response);
|
|
2893
|
-
const [diagnostic, replayAudit] = await Promise.all([
|
|
2894
|
-
dependencies.readToolFailureDiagnostic(response.sessionFile, active.trustedSessionRoot, identity, failedToolName(terminalError), terminalError),
|
|
2895
|
-
dependencies.readDelegationReplayAudit(response.sessionFile, active.trustedSessionRoot, identity, {
|
|
2896
|
-
task: active.transcriptTask,
|
|
2897
|
-
bashPermission: active.policy.permissions.bash
|
|
2898
|
-
})
|
|
2899
|
-
]);
|
|
2900
|
-
const validatedReplayAudit = validateReplayAudit(response, replayAudit);
|
|
2901
|
-
const exitCode = nonzeroTerminalExitCode(response) ?? response.exitCode ?? response.execution?.exitCode;
|
|
2902
|
-
const reason = [
|
|
2903
|
-
hasContradictoryCompletion(response) ? `Subagent "${active.agent}" reported terminal failure signals with completed status.` : `Subagent "${active.agent}" ${response.status.replaceAll("_", " ")}.`,
|
|
2904
|
-
...diagnostic ? formatToolFailureDiagnostic(diagnostic) : [],
|
|
2905
|
-
...exitCode !== undefined ? [`Subagent exit code: ${exitCode}`] : [],
|
|
2906
|
-
...blocker ? [`Automatic recovery blocked by: ${blocker.replaceAll("-", " ")}`] : [],
|
|
2907
|
-
`Terminal error: ${boundedFailureField(error)}`,
|
|
2908
|
-
...diagnostic && response.sessionFile ? [
|
|
2909
|
-
`Diagnostic session: ${boundedFailureField(response.sessionFile.replaceAll(/\s+/g, " "))}`
|
|
2910
|
-
] : []
|
|
2911
|
-
].join(`
|
|
2912
|
-
`);
|
|
2913
|
-
return {
|
|
2914
|
-
reason,
|
|
2915
|
-
status: response.status,
|
|
2916
|
-
...terminalError ? { error: terminalError } : {},
|
|
2917
|
-
...exitCode !== undefined ? { exitCode } : {},
|
|
2918
|
-
...blocker ? { recoveryBlocker: blocker } : {},
|
|
2919
|
-
...diagnostic ? { diagnostic } : {},
|
|
2920
|
-
...validatedReplayAudit ? { replayAudit: validatedReplayAudit } : {}
|
|
2921
|
-
};
|
|
2922
|
-
};
|
|
2923
|
-
return {
|
|
2924
|
-
delegationFailureFingerprint,
|
|
2925
|
-
hasContradictoryCompletion,
|
|
2926
|
-
completedResponseAudit,
|
|
2927
|
-
isRetryableTerminalFailure,
|
|
2928
|
-
isSafeToRetryDelegation,
|
|
2929
|
-
rejectedRecoveryReason,
|
|
2930
|
-
completionMatchesResult,
|
|
2931
|
-
recoveredProjectionError,
|
|
2932
|
-
describeDelegationFailure
|
|
2933
|
-
};
|
|
2934
|
-
}
|
|
2935
|
-
|
|
2936
|
-
// src/harness/catalog.ts
|
|
2937
|
-
function createEmptyCatalog() {
|
|
2938
|
-
return {
|
|
2939
|
-
workflows: new Map,
|
|
2940
|
-
settings: DEFAULT_SETTINGS,
|
|
2941
|
-
diagnostics: [],
|
|
2942
|
-
userDirectory: ""
|
|
2943
|
-
};
|
|
2944
|
-
}
|
|
2945
|
-
function formatCatalogDiagnostics(catalog) {
|
|
2946
|
-
const shownDiagnostics = catalog.diagnostics.slice(0, 3).map((diagnostic) => `${diagnostic.path}: ${diagnostic.message}`);
|
|
2947
|
-
const remainingCount = catalog.diagnostics.length - shownDiagnostics.length;
|
|
2948
|
-
return [
|
|
2949
|
-
...shownDiagnostics,
|
|
2950
|
-
...remainingCount > 0 ? [`${remainingCount} more diagnostic(s)`] : []
|
|
2951
|
-
].join(`
|
|
2952
|
-
`);
|
|
2953
|
-
}
|
|
2954
|
-
function parseAvailableSkills(systemPrompt) {
|
|
2955
|
-
const sections = [
|
|
2956
|
-
...systemPrompt.matchAll(/<available_skills>([\s\S]*?)<\/available_skills>/g)
|
|
2957
|
-
];
|
|
2958
|
-
const section = sections.at(-1)?.[1] ?? "";
|
|
2959
|
-
return [...section.matchAll(/<name>([^<]+)<\/name>/g)].flatMap((match) => {
|
|
2960
|
-
const name = match[1]?.trim();
|
|
2961
|
-
return name ? [{ name }] : [];
|
|
2962
|
-
});
|
|
2963
|
-
}
|
|
2964
|
-
|
|
2965
|
-
// src/harness/dependencies.ts
|
|
2966
|
-
import { randomBytes, randomUUID } from "node:crypto";
|
|
2967
|
-
import { constants as constants3, mkdtempSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2968
|
-
import { lstat as lstat3, open as open3, rm } from "node:fs/promises";
|
|
2969
|
-
import { tmpdir as tmpdir2 } from "node:os";
|
|
2970
|
-
import { join as join5 } from "node:path";
|
|
2971
|
-
|
|
2972
|
-
// src/integrations/plannotator-responses.ts
|
|
2973
|
-
var isRecord3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2974
|
-
var errorText = (value, fallback) => typeof value.error === "string" && value.error.trim() ? value.error : fallback;
|
|
2975
|
-
var normalizePlannotatorStartResponse = (value) => {
|
|
2976
|
-
if (!isRecord3(value)) {
|
|
2977
|
-
return {
|
|
2978
|
-
status: "error",
|
|
2979
|
-
error: "Plannotator returned an invalid response"
|
|
2980
|
-
};
|
|
2981
|
-
}
|
|
2982
|
-
if (value.status === "unavailable") {
|
|
2983
|
-
return {
|
|
2984
|
-
status: "unavailable",
|
|
2985
|
-
error: errorText(value, "Plannotator is unavailable")
|
|
2986
|
-
};
|
|
2987
|
-
}
|
|
2988
|
-
if (value.status === "error") {
|
|
2989
|
-
return {
|
|
2990
|
-
status: "error",
|
|
2991
|
-
error: errorText(value, "Plannotator failed")
|
|
2992
|
-
};
|
|
2993
|
-
}
|
|
2994
|
-
const result = isRecord3(value.result) ? value.result : undefined;
|
|
2995
|
-
if (value.status === "handled" && result?.status === "pending" && typeof result.reviewId === "string") {
|
|
2996
|
-
return {
|
|
2997
|
-
status: "handled",
|
|
2998
|
-
result: { status: "pending", reviewId: result.reviewId }
|
|
2999
|
-
};
|
|
3000
|
-
}
|
|
3001
|
-
return {
|
|
3002
|
-
status: "error",
|
|
3003
|
-
error: "Plannotator returned an invalid start result"
|
|
3004
|
-
};
|
|
3005
|
-
};
|
|
3006
|
-
var normalizePlannotatorStatusResponse = (value, requestedReviewId) => {
|
|
3007
|
-
if (!isRecord3(value)) {
|
|
3008
|
-
return {
|
|
3009
|
-
status: "error",
|
|
3010
|
-
error: "Plannotator returned an invalid response"
|
|
3011
|
-
};
|
|
3012
|
-
}
|
|
3013
|
-
if (value.status === "unavailable") {
|
|
3014
|
-
return {
|
|
3015
|
-
status: "unavailable",
|
|
3016
|
-
error: errorText(value, "Plannotator is unavailable")
|
|
3017
|
-
};
|
|
3018
|
-
}
|
|
3019
|
-
if (value.status === "error") {
|
|
3020
|
-
return {
|
|
3021
|
-
status: "error",
|
|
3022
|
-
error: errorText(value, "Plannotator failed")
|
|
3023
|
-
};
|
|
3024
|
-
}
|
|
3025
|
-
const result = isRecord3(value.result) ? value.result : undefined;
|
|
3026
|
-
if (value.status !== "handled" || !result) {
|
|
3027
|
-
return {
|
|
3028
|
-
status: "error",
|
|
3029
|
-
error: "Plannotator returned an invalid status result"
|
|
3030
|
-
};
|
|
3031
|
-
}
|
|
3032
|
-
if (result.status === "pending" || result.status === "missing") {
|
|
3033
|
-
return { status: "handled", result: { status: result.status } };
|
|
3034
|
-
}
|
|
3035
|
-
if (result.status === "completed" && typeof result.reviewId === "string" && typeof result.approved === "boolean") {
|
|
3036
|
-
if (result.reviewId !== requestedReviewId) {
|
|
3037
|
-
return {
|
|
3038
|
-
status: "error",
|
|
3039
|
-
error: "Plannotator returned a result for a different review"
|
|
3040
|
-
};
|
|
3041
|
-
}
|
|
3042
|
-
return {
|
|
3043
|
-
status: "handled",
|
|
3044
|
-
result: {
|
|
3045
|
-
status: "completed",
|
|
3046
|
-
reviewId: result.reviewId,
|
|
3047
|
-
approved: result.approved,
|
|
3048
|
-
feedback: typeof result.feedback === "string" ? result.feedback : ""
|
|
3049
|
-
}
|
|
3050
|
-
};
|
|
3051
|
-
}
|
|
3052
|
-
return {
|
|
3053
|
-
status: "error",
|
|
3054
|
-
error: "Plannotator returned an invalid status result"
|
|
3055
|
-
};
|
|
3056
|
-
};
|
|
3057
|
-
var parsePlannotatorResult = (value) => {
|
|
3058
|
-
if (!isRecord3(value))
|
|
3059
|
-
return;
|
|
3060
|
-
if (typeof value.reviewId !== "string" || typeof value.approved !== "boolean") {
|
|
3061
|
-
return;
|
|
3062
|
-
}
|
|
3063
|
-
return {
|
|
3064
|
-
reviewId: value.reviewId,
|
|
3065
|
-
approved: value.approved,
|
|
3066
|
-
feedback: typeof value.feedback === "string" ? value.feedback : ""
|
|
3067
|
-
};
|
|
3068
|
-
};
|
|
3069
|
-
|
|
3070
|
-
// src/integrations/plannotator-requests.ts
|
|
3071
|
-
var PLANNOTATOR_REQUEST_CHANNEL = "plannotator:request";
|
|
3072
|
-
var PLANNOTATOR_RESULT_CHANNEL = "plannotator:review-result";
|
|
3073
|
-
var DEFAULT_DEPENDENCIES = {
|
|
3074
|
-
scheduleTimeout: (callback, timeoutMs) => setTimeout(callback, timeoutMs),
|
|
3075
|
-
cancelTimeout: (handle) => {
|
|
3076
|
-
clearTimeout(handle);
|
|
1386
|
+
// src/integrations/plannotator-requests.ts
|
|
1387
|
+
var PLANNOTATOR_REQUEST_CHANNEL = "plannotator:request";
|
|
1388
|
+
var PLANNOTATOR_RESULT_CHANNEL = "plannotator:review-result";
|
|
1389
|
+
var DEFAULT_DEPENDENCIES = {
|
|
1390
|
+
scheduleTimeout: (callback, timeoutMs) => setTimeout(callback, timeoutMs),
|
|
1391
|
+
cancelTimeout: (handle) => {
|
|
1392
|
+
clearTimeout(handle);
|
|
3077
1393
|
}
|
|
3078
1394
|
};
|
|
3079
1395
|
var requestResponse = ({
|
|
@@ -3084,14 +1400,14 @@ var requestResponse = ({
|
|
|
3084
1400
|
normalize,
|
|
3085
1401
|
ignoreResponse,
|
|
3086
1402
|
dependencies
|
|
3087
|
-
}) => new Promise((
|
|
1403
|
+
}) => new Promise((resolve3) => {
|
|
3088
1404
|
let isSettled = false;
|
|
3089
1405
|
const finish = (response) => {
|
|
3090
1406
|
if (isSettled || ignoreResponse?.(response))
|
|
3091
1407
|
return;
|
|
3092
1408
|
isSettled = true;
|
|
3093
1409
|
dependencies.cancelTimeout(timer);
|
|
3094
|
-
|
|
1410
|
+
resolve3(normalize(response));
|
|
3095
1411
|
};
|
|
3096
1412
|
const timer = dependencies.scheduleTimeout(() => {
|
|
3097
1413
|
finish(timeoutResponse);
|
|
@@ -3103,8 +1419,8 @@ var requestResponse = ({
|
|
|
3103
1419
|
});
|
|
3104
1420
|
});
|
|
3105
1421
|
var MISSING_PLAN_CONTENT_RESPONSE = "Missing planContent for plan-review request.";
|
|
3106
|
-
var
|
|
3107
|
-
var isUnclaimedPlanReviewResponse = (response) =>
|
|
1422
|
+
var isRecord2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1423
|
+
var isUnclaimedPlanReviewResponse = (response) => isRecord2(response) && response.status === "error" && response.error === MISSING_PLAN_CONTENT_RESPONSE;
|
|
3108
1424
|
var singleConsumerPlanPayload = (planContent, origin) => {
|
|
3109
1425
|
let claimed = false;
|
|
3110
1426
|
return {
|
|
@@ -3149,229 +1465,190 @@ var requestPlannotatorReviewStatus = (events, requestId, reviewId, timeoutMs, de
|
|
|
3149
1465
|
dependencies
|
|
3150
1466
|
});
|
|
3151
1467
|
// src/integrations/prompt-gate.ts
|
|
3152
|
-
var APPROVE = "Approve";
|
|
3153
|
-
var REQUEST_CHANGES = "Request changes";
|
|
3154
|
-
var PAUSE = "Pause workflow";
|
|
3155
|
-
var selectionOptions = (signal) => signal ? [{ signal }] : [];
|
|
3156
|
-
var requestPromptGateReview = async (ui, title, artifact, signal) => {
|
|
3157
|
-
const choice = await ui.select(`${title}
|
|
3158
|
-
|
|
3159
|
-
${artifact}`, [APPROVE, REQUEST_CHANGES, PAUSE], ...selectionOptions(signal));
|
|
3160
|
-
if (choice === APPROVE) {
|
|
3161
|
-
return { status: "resolved", approved: true, feedback: "" };
|
|
3162
|
-
}
|
|
3163
|
-
if (choice !== REQUEST_CHANGES) {
|
|
3164
|
-
return { status: "dismissed" };
|
|
3165
|
-
}
|
|
3166
|
-
let feedback = await ui.input("Workflow review feedback", "Describe the required changes", ...selectionOptions(signal));
|
|
3167
|
-
while (feedback !== undefined && !feedback.trim()) {
|
|
3168
|
-
ui.notify("Feedback cannot be empty", "warning");
|
|
3169
|
-
feedback = await ui.input("Workflow review feedback", "Describe the required changes", ...selectionOptions(signal));
|
|
3170
|
-
}
|
|
3171
|
-
if (feedback === undefined)
|
|
3172
|
-
return { status: "dismissed" };
|
|
3173
|
-
return {
|
|
3174
|
-
status: "resolved",
|
|
3175
|
-
approved: false,
|
|
3176
|
-
feedback: feedback.trim()
|
|
3177
|
-
};
|
|
3178
|
-
};
|
|
3179
|
-
|
|
3180
|
-
// src/integrations/subagents/client-messages.ts
|
|
3181
|
-
var DELEGATION_STATUSES = new Set([
|
|
3182
|
-
"completed",
|
|
3183
|
-
"failed",
|
|
3184
|
-
"timed_out",
|
|
3185
|
-
"cancelled",
|
|
3186
|
-
"interrupted",
|
|
3187
|
-
"turn_budget_exhausted",
|
|
3188
|
-
"tool_budget_exhausted",
|
|
3189
|
-
"structured_output_failed",
|
|
3190
|
-
"acceptance_failed",
|
|
3191
|
-
"invalid_request",
|
|
3192
|
-
"unavailable_context"
|
|
3193
|
-
]);
|
|
3194
|
-
var isRecord5 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3195
|
-
var isDelegationStatus = (value) => DELEGATION_STATUSES.has(value);
|
|
3196
|
-
var isDelegationResponse = (value) => isRecord5(value) && value.version === SUBAGENT_DELEGATION_PROTOCOL_VERSION && typeof value.requestId === "string" && typeof value.status === "string" && isDelegationStatus(value.status);
|
|
3197
|
-
var isDelegationUpdate = (value) => isRecord5(value) && value.version === SUBAGENT_DELEGATION_PROTOCOL_VERSION && typeof value.requestId === "string";
|
|
3198
|
-
var requestIdOf = (value) => isRecord5(value) && typeof value.requestId === "string" ? value.requestId : undefined;
|
|
3199
|
-
var parseDelegationResponse = (value) => isDelegationResponse(value) ? value : undefined;
|
|
3200
|
-
var parseDelegationUpdate = (value) => isDelegationUpdate(value) ? value : undefined;
|
|
3201
|
-
|
|
3202
|
-
// src/integrations/subagents/client-delegation.ts
|
|
3203
|
-
var DEFAULT_CLIENT_DEPENDENCIES = {
|
|
3204
|
-
scheduleTimeout: (callback, timeoutMs) => setTimeout(callback, timeoutMs),
|
|
3205
|
-
cancelTimeout: (timer) => {
|
|
3206
|
-
clearTimeout(timer);
|
|
3207
|
-
}
|
|
3208
|
-
};
|
|
3209
|
-
function isUnsubscribe(value) {
|
|
3210
|
-
return typeof value === "function";
|
|
3211
|
-
}
|
|
3212
|
-
var createDelegation = ({
|
|
3213
|
-
events,
|
|
3214
|
-
request,
|
|
3215
|
-
options,
|
|
3216
|
-
dependencies,
|
|
3217
|
-
releaseActive
|
|
3218
|
-
}) => {
|
|
3219
|
-
let requestCancellation = () => {
|
|
3220
|
-
return;
|
|
3221
|
-
};
|
|
3222
|
-
let resolveTerminal = () => {
|
|
3223
|
-
return;
|
|
3224
|
-
};
|
|
3225
|
-
const terminal = new Promise((resolve7) => {
|
|
3226
|
-
resolveTerminal = resolve7;
|
|
3227
|
-
});
|
|
3228
|
-
const promise = new Promise((resolve7, reject2) => {
|
|
3229
|
-
let isSettled = false;
|
|
3230
|
-
let isCancellationRequested = false;
|
|
3231
|
-
const subscriptions = [];
|
|
3232
|
-
const subscribe = (event, handler) => {
|
|
3233
|
-
const unsubscribe = events.on(event, handler);
|
|
3234
|
-
if (isUnsubscribe(unsubscribe)) {
|
|
3235
|
-
subscriptions.push(() => {
|
|
3236
|
-
unsubscribe();
|
|
3237
|
-
});
|
|
3238
|
-
}
|
|
3239
|
-
};
|
|
3240
|
-
const stopLocalWatchers = () => {
|
|
3241
|
-
dependencies.cancelTimeout(startTimer);
|
|
3242
|
-
dependencies.cancelTimeout(overallTimer);
|
|
3243
|
-
options.signal?.removeEventListener("abort", abort);
|
|
3244
|
-
};
|
|
3245
|
-
const cleanup = () => {
|
|
3246
|
-
stopLocalWatchers();
|
|
3247
|
-
for (const unsubscribe of subscriptions)
|
|
3248
|
-
unsubscribe();
|
|
3249
|
-
releaseActive(request.requestId);
|
|
3250
|
-
};
|
|
3251
|
-
const finish = (result) => {
|
|
3252
|
-
if (isSettled)
|
|
3253
|
-
return;
|
|
3254
|
-
isSettled = true;
|
|
3255
|
-
cleanup();
|
|
3256
|
-
if ("response" in result)
|
|
3257
|
-
resolve7(result.response);
|
|
3258
|
-
else
|
|
3259
|
-
reject2(result.error);
|
|
3260
|
-
};
|
|
3261
|
-
const emitCancel = () => {
|
|
3262
|
-
if (isCancellationRequested || isSettled)
|
|
3263
|
-
return;
|
|
3264
|
-
isCancellationRequested = true;
|
|
3265
|
-
events.emit(SUBAGENT_DELEGATION_CANCEL_EVENT, {
|
|
3266
|
-
version: SUBAGENT_DELEGATION_PROTOCOL_VERSION,
|
|
3267
|
-
requestId: request.requestId
|
|
3268
|
-
});
|
|
3269
|
-
};
|
|
3270
|
-
const failAndCancel = (reason) => {
|
|
3271
|
-
if (isSettled)
|
|
3272
|
-
return;
|
|
3273
|
-
emitCancel();
|
|
3274
|
-
if (isSettled)
|
|
3275
|
-
return;
|
|
3276
|
-
isSettled = true;
|
|
3277
|
-
stopLocalWatchers();
|
|
3278
|
-
reject2(new Error(reason));
|
|
3279
|
-
};
|
|
3280
|
-
const abort = () => {
|
|
3281
|
-
failAndCancel("subagent delegation was cancelled");
|
|
3282
|
-
};
|
|
3283
|
-
requestCancellation = emitCancel;
|
|
3284
|
-
subscribe(SUBAGENT_DELEGATION_STARTED_EVENT, (data) => {
|
|
3285
|
-
if (requestIdOf(data) !== request.requestId)
|
|
3286
|
-
return;
|
|
3287
|
-
dependencies.cancelTimeout(startTimer);
|
|
3288
|
-
});
|
|
3289
|
-
subscribe(SUBAGENT_DELEGATION_UPDATE_EVENT, (data) => {
|
|
3290
|
-
const update = parseDelegationUpdate(data);
|
|
3291
|
-
if (!update || update.requestId !== request.requestId)
|
|
3292
|
-
return;
|
|
3293
|
-
options.onUpdate?.(update);
|
|
3294
|
-
});
|
|
3295
|
-
subscribe(SUBAGENT_DELEGATION_RESPONSE_EVENT, (data) => {
|
|
3296
|
-
const response = parseDelegationResponse(data);
|
|
3297
|
-
if (!response || response.requestId !== request.requestId)
|
|
3298
|
-
return;
|
|
3299
|
-
resolveTerminal();
|
|
3300
|
-
if (isSettled) {
|
|
3301
|
-
cleanup();
|
|
3302
|
-
options.onLateTerminal?.(response);
|
|
3303
|
-
return;
|
|
3304
|
-
}
|
|
3305
|
-
finish({ response });
|
|
3306
|
-
});
|
|
3307
|
-
const startTimer = dependencies.scheduleTimeout(() => {
|
|
3308
|
-
failAndCancel("pi-subagents did not accept the delegation request; verify it is installed and loaded");
|
|
3309
|
-
}, options.startTimeoutMs ?? 3000);
|
|
3310
|
-
const overallTimer = dependencies.scheduleTimeout(() => {
|
|
3311
|
-
failAndCancel("pi-subagents did not settle the delegation request before its deadline");
|
|
3312
|
-
}, (request.timeoutMs ?? 900000) + 5000);
|
|
3313
|
-
startTimer.unref();
|
|
3314
|
-
overallTimer.unref();
|
|
3315
|
-
options.signal?.addEventListener("abort", abort, { once: true });
|
|
3316
|
-
});
|
|
1468
|
+
var APPROVE = "Approve";
|
|
1469
|
+
var REQUEST_CHANGES = "Request changes";
|
|
1470
|
+
var PAUSE = "Pause workflow";
|
|
1471
|
+
var selectionOptions = (signal) => signal ? [{ signal }] : [];
|
|
1472
|
+
var requestPromptGateReview = async (ui, title, artifact, signal) => {
|
|
1473
|
+
const choice = await ui.select(`${title}
|
|
1474
|
+
|
|
1475
|
+
${artifact}`, [APPROVE, REQUEST_CHANGES, PAUSE], ...selectionOptions(signal));
|
|
1476
|
+
if (choice === APPROVE) {
|
|
1477
|
+
return { status: "resolved", approved: true, feedback: "" };
|
|
1478
|
+
}
|
|
1479
|
+
if (choice !== REQUEST_CHANGES) {
|
|
1480
|
+
return { status: "dismissed" };
|
|
1481
|
+
}
|
|
1482
|
+
let feedback = await ui.input("Workflow review feedback", "Describe the required changes", ...selectionOptions(signal));
|
|
1483
|
+
while (feedback !== undefined && !feedback.trim()) {
|
|
1484
|
+
ui.notify("Feedback cannot be empty", "warning");
|
|
1485
|
+
feedback = await ui.input("Workflow review feedback", "Describe the required changes", ...selectionOptions(signal));
|
|
1486
|
+
}
|
|
1487
|
+
if (feedback === undefined)
|
|
1488
|
+
return { status: "dismissed" };
|
|
3317
1489
|
return {
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
requestCancellation();
|
|
3322
|
-
},
|
|
3323
|
-
terminal
|
|
3324
|
-
},
|
|
3325
|
-
promise,
|
|
3326
|
-
start: () => {
|
|
3327
|
-
events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, request);
|
|
3328
|
-
}
|
|
1490
|
+
status: "resolved",
|
|
1491
|
+
approved: false,
|
|
1492
|
+
feedback: feedback.trim()
|
|
3329
1493
|
};
|
|
3330
1494
|
};
|
|
3331
1495
|
|
|
3332
1496
|
// src/integrations/subagents/client.ts
|
|
3333
|
-
|
|
1497
|
+
import { spawn } from "node:child_process";
|
|
1498
|
+
import { StringDecoder } from "node:string_decoder";
|
|
1499
|
+
var directWorkerCommand = (request) => [
|
|
1500
|
+
"--no-session",
|
|
1501
|
+
"--mode",
|
|
1502
|
+
"json",
|
|
1503
|
+
...request.model ? ["--model", request.model] : [],
|
|
1504
|
+
...request.thinking ? ["--thinking", request.thinking] : [],
|
|
1505
|
+
"--print",
|
|
1506
|
+
request.task
|
|
1507
|
+
];
|
|
1508
|
+
function directWorkerResponse(request, code, signal, stderr) {
|
|
1509
|
+
const status = code === 0 ? "completed" : signal ? "cancelled" : "failed";
|
|
1510
|
+
return {
|
|
1511
|
+
requestId: request.requestId,
|
|
1512
|
+
agent: request.agent,
|
|
1513
|
+
status,
|
|
1514
|
+
...code === null ? {} : { exitCode: code },
|
|
1515
|
+
...status !== "completed" && stderr.trim() ? { error: stderr.trim().slice(-4000) } : {}
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
var MAX_PROGRESS_DETAIL_CHARS = 480;
|
|
1519
|
+
var SECRET_KEY = /authorization|cookie|password|secret|token|api[-_]?key/i;
|
|
1520
|
+
function redactProgressValue(value, key = "") {
|
|
1521
|
+
if (SECRET_KEY.test(key))
|
|
1522
|
+
return "[redacted]";
|
|
1523
|
+
if (typeof value === "string") {
|
|
1524
|
+
return value.length > MAX_PROGRESS_DETAIL_CHARS ? `${value.slice(0, MAX_PROGRESS_DETAIL_CHARS - 1)}…` : value;
|
|
1525
|
+
}
|
|
1526
|
+
if (Array.isArray(value))
|
|
1527
|
+
return value.map((item) => redactProgressValue(item));
|
|
1528
|
+
if (value && typeof value === "object") {
|
|
1529
|
+
return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [
|
|
1530
|
+
entryKey,
|
|
1531
|
+
redactProgressValue(entryValue, entryKey)
|
|
1532
|
+
]));
|
|
1533
|
+
}
|
|
1534
|
+
return value;
|
|
1535
|
+
}
|
|
1536
|
+
function formatToolCall(toolName, args) {
|
|
1537
|
+
const rendered = JSON.stringify(redactProgressValue(args));
|
|
1538
|
+
return `call ${toolName} ${rendered}`.slice(0, MAX_PROGRESS_DETAIL_CHARS);
|
|
1539
|
+
}
|
|
1540
|
+
function workerProgressFromJsonLine(line, requestId, toolCount, responseText = "") {
|
|
1541
|
+
let event;
|
|
1542
|
+
try {
|
|
1543
|
+
const parsed = JSON.parse(line);
|
|
1544
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
1545
|
+
return { toolCount, responseText };
|
|
1546
|
+
event = parsed;
|
|
1547
|
+
} catch {
|
|
1548
|
+
return { toolCount, responseText };
|
|
1549
|
+
}
|
|
1550
|
+
if (event.type === "agent_start") {
|
|
1551
|
+
return {
|
|
1552
|
+
toolCount,
|
|
1553
|
+
responseText,
|
|
1554
|
+
update: { requestId, activity: "thinking", toolCount }
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
if (event.type === "message_start" && event.message?.role === "assistant") {
|
|
1558
|
+
return { toolCount, responseText: "" };
|
|
1559
|
+
}
|
|
1560
|
+
if (event.type === "message_update" && event.assistantMessageEvent?.type === "text_delta" && typeof event.assistantMessageEvent.delta === "string") {
|
|
1561
|
+
const nextResponseText = `${responseText}${event.assistantMessageEvent.delta}`.slice(-MAX_PROGRESS_DETAIL_CHARS);
|
|
1562
|
+
return {
|
|
1563
|
+
toolCount,
|
|
1564
|
+
responseText: nextResponseText,
|
|
1565
|
+
update: {
|
|
1566
|
+
requestId,
|
|
1567
|
+
activity: "responding",
|
|
1568
|
+
detail: `response: ${nextResponseText}`,
|
|
1569
|
+
toolCount
|
|
1570
|
+
}
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
if ((event.type === "tool_execution_start" || event.type === "tool_execution_update") && typeof event.toolName === "string") {
|
|
1574
|
+
const nextToolCount = event.type === "tool_execution_start" ? toolCount + 1 : toolCount;
|
|
1575
|
+
return {
|
|
1576
|
+
toolCount: nextToolCount,
|
|
1577
|
+
responseText,
|
|
1578
|
+
update: {
|
|
1579
|
+
requestId,
|
|
1580
|
+
currentTool: event.toolName,
|
|
1581
|
+
...event.type === "tool_execution_start" ? { detail: formatToolCall(event.toolName, event.args) } : {},
|
|
1582
|
+
toolCount: nextToolCount
|
|
1583
|
+
}
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
return { toolCount, responseText };
|
|
1587
|
+
}
|
|
1588
|
+
function createSubagentDelegationClient(spawnWorker = spawn) {
|
|
3334
1589
|
let active;
|
|
3335
1590
|
const delegate = (request, options = {}) => {
|
|
3336
1591
|
if (active) {
|
|
3337
|
-
return Promise.reject(new Error(`
|
|
1592
|
+
return Promise.reject(new Error(`workflow worker "${active.requestId}" is still active`));
|
|
3338
1593
|
}
|
|
3339
1594
|
if (options.signal?.aborted) {
|
|
3340
|
-
return Promise.reject(new Error("
|
|
3341
|
-
}
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
1595
|
+
return Promise.reject(new Error("workflow worker was cancelled"));
|
|
1596
|
+
}
|
|
1597
|
+
return new Promise((resolve3, reject) => {
|
|
1598
|
+
const child = spawnWorker("pi", [...directWorkerCommand(request)], {
|
|
1599
|
+
cwd: request.cwd,
|
|
1600
|
+
env: {
|
|
1601
|
+
...process.env,
|
|
1602
|
+
PI_WORKFLOWS_CHILD: "1",
|
|
1603
|
+
PI_WORKFLOWS_CHILD_AGENT: request.agent
|
|
1604
|
+
},
|
|
1605
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1606
|
+
});
|
|
1607
|
+
active = { requestId: request.requestId, process: child };
|
|
1608
|
+
let stderr = "";
|
|
1609
|
+
let stdoutBuffer = "";
|
|
1610
|
+
let toolCount = 0;
|
|
1611
|
+
let responseText = "";
|
|
1612
|
+
const stdoutDecoder = new StringDecoder("utf8");
|
|
1613
|
+
const consumeWorkerLines = () => {
|
|
1614
|
+
while (true) {
|
|
1615
|
+
const newline = stdoutBuffer.indexOf(`
|
|
1616
|
+
`);
|
|
1617
|
+
if (newline === -1)
|
|
1618
|
+
return;
|
|
1619
|
+
const line = stdoutBuffer.slice(0, newline);
|
|
1620
|
+
stdoutBuffer = stdoutBuffer.slice(newline + 1);
|
|
1621
|
+
const progress = workerProgressFromJsonLine(line, request.requestId, toolCount, responseText);
|
|
1622
|
+
toolCount = progress.toolCount;
|
|
1623
|
+
responseText = progress.responseText;
|
|
1624
|
+
if (progress.update)
|
|
1625
|
+
options.onUpdate?.(progress.update);
|
|
1626
|
+
}
|
|
1627
|
+
};
|
|
1628
|
+
const consumeWorkerOutput = (chunk) => {
|
|
1629
|
+
stdoutBuffer += stdoutDecoder.write(chunk);
|
|
1630
|
+
consumeWorkerLines();
|
|
3369
1631
|
};
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
1632
|
+
child.stdout.on("data", consumeWorkerOutput);
|
|
1633
|
+
child.stderr.on("data", (chunk) => {
|
|
1634
|
+
stderr += chunk.toString("utf8");
|
|
1635
|
+
});
|
|
1636
|
+
const abort = () => {
|
|
1637
|
+
child.kill("SIGTERM");
|
|
1638
|
+
};
|
|
1639
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
1640
|
+
child.once("error", (error) => {
|
|
1641
|
+
if (active?.process === child)
|
|
1642
|
+
active = undefined;
|
|
1643
|
+
reject(error);
|
|
1644
|
+
});
|
|
1645
|
+
child.once("close", (code, signal) => {
|
|
1646
|
+
stdoutBuffer += stdoutDecoder.end();
|
|
1647
|
+
consumeWorkerLines();
|
|
1648
|
+
if (active?.process === child)
|
|
1649
|
+
active = undefined;
|
|
1650
|
+
options.signal?.removeEventListener("abort", abort);
|
|
1651
|
+
resolve3(directWorkerResponse(request, code, signal, stderr));
|
|
3375
1652
|
});
|
|
3376
1653
|
});
|
|
3377
1654
|
};
|
|
@@ -3380,34 +1657,28 @@ function createSubagentDelegationClient(events, dependencies = DEFAULT_CLIENT_DE
|
|
|
3380
1657
|
return active?.requestId;
|
|
3381
1658
|
},
|
|
3382
1659
|
delegate,
|
|
3383
|
-
cancelActiveAndWait
|
|
1660
|
+
async cancelActiveAndWait() {
|
|
1661
|
+
const current = active;
|
|
1662
|
+
if (!current)
|
|
1663
|
+
return true;
|
|
1664
|
+
current.process.kill("SIGTERM");
|
|
1665
|
+
return new Promise((resolve3) => {
|
|
1666
|
+
current.process.once("close", () => {
|
|
1667
|
+
resolve3(true);
|
|
1668
|
+
});
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
3384
1671
|
};
|
|
3385
1672
|
}
|
|
3386
1673
|
|
|
3387
|
-
class SubagentDelegationClient {
|
|
3388
|
-
#controller;
|
|
3389
|
-
constructor(events, dependencies = DEFAULT_CLIENT_DEPENDENCIES) {
|
|
3390
|
-
this.#controller = createSubagentDelegationClient(events, dependencies);
|
|
3391
|
-
}
|
|
3392
|
-
get activeRequestId() {
|
|
3393
|
-
return this.#controller.activeRequestId;
|
|
3394
|
-
}
|
|
3395
|
-
delegate(request, options = {}) {
|
|
3396
|
-
return this.#controller.delegate(request, options);
|
|
3397
|
-
}
|
|
3398
|
-
cancelActiveAndWait(waitMs = 5000) {
|
|
3399
|
-
return this.#controller.cancelActiveAndWait(waitMs);
|
|
3400
|
-
}
|
|
3401
|
-
}
|
|
3402
|
-
|
|
3403
1674
|
// src/policy/completion-batch.ts
|
|
3404
|
-
var
|
|
1675
|
+
var isRecord3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3405
1676
|
var toolCalls = (message) => {
|
|
3406
|
-
if (!
|
|
1677
|
+
if (!isRecord3(message))
|
|
3407
1678
|
return [];
|
|
3408
1679
|
if (message.role !== "assistant" || !Array.isArray(message.content))
|
|
3409
1680
|
return [];
|
|
3410
|
-
return message.content.filter((item) =>
|
|
1681
|
+
return message.content.filter((item) => isRecord3(item) && item.type === "toolCall" && typeof item.id === "string" && typeof item.name === "string");
|
|
3411
1682
|
};
|
|
3412
1683
|
function invalidCompletionCallIds(message, completionTool) {
|
|
3413
1684
|
const calls = toolCalls(message);
|
|
@@ -3440,7 +1711,7 @@ var UNSUPPORTED_MCP_PROXY_FIELDS = [
|
|
|
3440
1711
|
"regex",
|
|
3441
1712
|
"includeSchemas"
|
|
3442
1713
|
];
|
|
3443
|
-
var
|
|
1714
|
+
var reject = (reason) => ({
|
|
3444
1715
|
allowed: false,
|
|
3445
1716
|
reason
|
|
3446
1717
|
});
|
|
@@ -3452,21 +1723,185 @@ var selectorAllows = (selectors, server, tool) => selectors.some((selector) => {
|
|
|
3452
1723
|
});
|
|
3453
1724
|
var authorizeMcpProxy = (input, selectors) => {
|
|
3454
1725
|
if (selectors.length === 0) {
|
|
3455
|
-
return
|
|
1726
|
+
return reject("MCP access is disabled for this workflow step");
|
|
3456
1727
|
}
|
|
3457
1728
|
const unsupportedMode = UNSUPPORTED_MCP_PROXY_FIELDS.find((field) => input[field] !== undefined);
|
|
3458
1729
|
if (unsupportedMode) {
|
|
3459
|
-
return
|
|
1730
|
+
return reject(`MCP proxy mode "${unsupportedMode}" is disabled; use an explicit server and tool`);
|
|
3460
1731
|
}
|
|
3461
1732
|
if (typeof input.server !== "string" || !input.server.trim()) {
|
|
3462
|
-
return
|
|
1733
|
+
return reject("MCP proxy calls must name an explicit server");
|
|
3463
1734
|
}
|
|
3464
1735
|
if (typeof input.tool !== "string" || !input.tool.trim()) {
|
|
3465
|
-
return
|
|
1736
|
+
return reject("MCP proxy calls must name an explicit tool");
|
|
3466
1737
|
}
|
|
3467
1738
|
const server = input.server.trim();
|
|
3468
1739
|
const tool = input.tool.trim();
|
|
3469
|
-
return selectorAllows(selectors, server, tool) ? { allowed: true } :
|
|
1740
|
+
return selectorAllows(selectors, server, tool) ? { allowed: true } : reject(`MCP tool "${server}/${tool}" is not allowed for this workflow step`);
|
|
1741
|
+
};
|
|
1742
|
+
// src/policy/bash-authorization.ts
|
|
1743
|
+
import { basename } from "node:path";
|
|
1744
|
+
|
|
1745
|
+
// src/policy/restricted-command.ts
|
|
1746
|
+
var UNQUOTED_SHELL_METACHARACTERS = new Set([
|
|
1747
|
+
";",
|
|
1748
|
+
"&",
|
|
1749
|
+
"|",
|
|
1750
|
+
"<",
|
|
1751
|
+
">",
|
|
1752
|
+
`
|
|
1753
|
+
`,
|
|
1754
|
+
"\r",
|
|
1755
|
+
"`",
|
|
1756
|
+
"$",
|
|
1757
|
+
"(",
|
|
1758
|
+
")",
|
|
1759
|
+
"{",
|
|
1760
|
+
"}",
|
|
1761
|
+
"#",
|
|
1762
|
+
"\x00"
|
|
1763
|
+
]);
|
|
1764
|
+
var PATHNAME_EXPANSION_CHARACTERS = new Set([
|
|
1765
|
+
"*",
|
|
1766
|
+
"?",
|
|
1767
|
+
"[",
|
|
1768
|
+
"]",
|
|
1769
|
+
"~"
|
|
1770
|
+
]);
|
|
1771
|
+
var invalidCharacter = (character) => {
|
|
1772
|
+
if (character === `
|
|
1773
|
+
` || character === "\r" || character === "\x00") {
|
|
1774
|
+
return "multiline and null characters are not allowed";
|
|
1775
|
+
}
|
|
1776
|
+
return;
|
|
1777
|
+
};
|
|
1778
|
+
var tokenizeRestrictedCommand = (command) => {
|
|
1779
|
+
if (!command.trim())
|
|
1780
|
+
return { error: "empty Bash command" };
|
|
1781
|
+
const tokens = [];
|
|
1782
|
+
let token = "";
|
|
1783
|
+
let quote;
|
|
1784
|
+
let isEscaping = false;
|
|
1785
|
+
let isTokenStarted = false;
|
|
1786
|
+
for (const character of command) {
|
|
1787
|
+
if (quote === "'") {
|
|
1788
|
+
const error = invalidCharacter(character);
|
|
1789
|
+
if (error)
|
|
1790
|
+
return { error };
|
|
1791
|
+
if (character === "'") {
|
|
1792
|
+
quote = undefined;
|
|
1793
|
+
} else {
|
|
1794
|
+
token += character;
|
|
1795
|
+
}
|
|
1796
|
+
isTokenStarted = true;
|
|
1797
|
+
continue;
|
|
1798
|
+
}
|
|
1799
|
+
if (quote === '"') {
|
|
1800
|
+
const error = invalidCharacter(character);
|
|
1801
|
+
if (error)
|
|
1802
|
+
return { error };
|
|
1803
|
+
if (character === '"') {
|
|
1804
|
+
quote = undefined;
|
|
1805
|
+
} else if (character === "$" || character === "`" || character === "\\") {
|
|
1806
|
+
return {
|
|
1807
|
+
error: "substitutions and escapes are not allowed inside double quotes"
|
|
1808
|
+
};
|
|
1809
|
+
} else {
|
|
1810
|
+
token += character;
|
|
1811
|
+
}
|
|
1812
|
+
isTokenStarted = true;
|
|
1813
|
+
continue;
|
|
1814
|
+
}
|
|
1815
|
+
if (isEscaping) {
|
|
1816
|
+
const error = invalidCharacter(character);
|
|
1817
|
+
if (error)
|
|
1818
|
+
return { error };
|
|
1819
|
+
token += character;
|
|
1820
|
+
isEscaping = false;
|
|
1821
|
+
isTokenStarted = true;
|
|
1822
|
+
continue;
|
|
1823
|
+
}
|
|
1824
|
+
if (character === "\\") {
|
|
1825
|
+
isEscaping = true;
|
|
1826
|
+
isTokenStarted = true;
|
|
1827
|
+
continue;
|
|
1828
|
+
}
|
|
1829
|
+
if (character === "'" || character === '"') {
|
|
1830
|
+
quote = character;
|
|
1831
|
+
isTokenStarted = true;
|
|
1832
|
+
continue;
|
|
1833
|
+
}
|
|
1834
|
+
if (UNQUOTED_SHELL_METACHARACTERS.has(character)) {
|
|
1835
|
+
return {
|
|
1836
|
+
error: "shell operators, substitutions, expansions, and comments are not allowed"
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
if (PATHNAME_EXPANSION_CHARACTERS.has(character)) {
|
|
1840
|
+
return {
|
|
1841
|
+
error: "unquoted pathname and tilde expansion are not allowed"
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
if (/\s/u.test(character)) {
|
|
1845
|
+
if (isTokenStarted) {
|
|
1846
|
+
tokens.push(token);
|
|
1847
|
+
token = "";
|
|
1848
|
+
isTokenStarted = false;
|
|
1849
|
+
}
|
|
1850
|
+
continue;
|
|
1851
|
+
}
|
|
1852
|
+
token += character;
|
|
1853
|
+
isTokenStarted = true;
|
|
1854
|
+
}
|
|
1855
|
+
if (isEscaping)
|
|
1856
|
+
return { error: "trailing Bash escape is not allowed" };
|
|
1857
|
+
if (quote)
|
|
1858
|
+
return { error: "unterminated Bash quote" };
|
|
1859
|
+
if (isTokenStarted)
|
|
1860
|
+
tokens.push(token);
|
|
1861
|
+
return tokens.length > 0 ? { tokens } : { error: "empty Bash command" };
|
|
1862
|
+
};
|
|
1863
|
+
|
|
1864
|
+
// src/policy/bash-authorization.ts
|
|
1865
|
+
var SHELL_WRAPPERS = new Set([
|
|
1866
|
+
"bash",
|
|
1867
|
+
"builtin",
|
|
1868
|
+
"command",
|
|
1869
|
+
"env",
|
|
1870
|
+
"exec",
|
|
1871
|
+
"fish",
|
|
1872
|
+
"sh",
|
|
1873
|
+
"time",
|
|
1874
|
+
"xargs",
|
|
1875
|
+
"zsh"
|
|
1876
|
+
]);
|
|
1877
|
+
var ENVIRONMENT_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/u;
|
|
1878
|
+
var reject2 = (reason) => ({
|
|
1879
|
+
allowed: false,
|
|
1880
|
+
reason
|
|
1881
|
+
});
|
|
1882
|
+
var matchesRule = (tokens, rule) => tokens[0] === rule.executable && rule.argsPrefix.every((expected, index) => tokens[index + 1] === expected);
|
|
1883
|
+
var authorizeBash = (command, permission) => {
|
|
1884
|
+
if (permission.mode === "unrestricted") {
|
|
1885
|
+
return { allowed: true };
|
|
1886
|
+
}
|
|
1887
|
+
if (permission.mode === "deny") {
|
|
1888
|
+
return reject2("Bash is disabled for this workflow step");
|
|
1889
|
+
}
|
|
1890
|
+
const parsed = tokenizeRestrictedCommand(command);
|
|
1891
|
+
if (!parsed.tokens)
|
|
1892
|
+
return reject2(parsed.error);
|
|
1893
|
+
const executable = parsed.tokens[0] ?? "";
|
|
1894
|
+
if (SHELL_WRAPPERS.has(basename(executable))) {
|
|
1895
|
+
return reject2(`shell wrapper "${executable}" is not allowed in restricted mode`);
|
|
1896
|
+
}
|
|
1897
|
+
if (ENVIRONMENT_ASSIGNMENT.test(executable)) {
|
|
1898
|
+
return reject2("environment assignments are not allowed in restricted mode");
|
|
1899
|
+
}
|
|
1900
|
+
const rule = permission.allow.find((candidate) => matchesRule(parsed.tokens, candidate));
|
|
1901
|
+
if (!rule) {
|
|
1902
|
+
return reject2("command does not match this step's Bash allow-list");
|
|
1903
|
+
}
|
|
1904
|
+
return { allowed: true, tokens: [...parsed.tokens] };
|
|
3470
1905
|
};
|
|
3471
1906
|
// src/policy/tool-selection.ts
|
|
3472
1907
|
var sourceText = (tool) => `${tool.sourceInfo?.source ?? ""}
|
|
@@ -3665,9 +2100,9 @@ var MAX_WORKFLOW_TRACE_CHARS = 2000000;
|
|
|
3665
2100
|
|
|
3666
2101
|
// src/step-log.ts
|
|
3667
2102
|
var REDACTED = "[redacted]";
|
|
3668
|
-
var
|
|
2103
|
+
var SECRET_KEY2 = /(?:^|[-_])(authorization|cookie|set-cookie|api[-_]?key|access[-_]?token|refresh[-_]?token|token|secret|password|passwd|credential|private[-_]?key|client[-_]?secret)(?:$|[-_])/i;
|
|
3669
2104
|
var LABELED_VALUE = /(^|[^A-Za-z0-9_])([A-Za-z0-9_-]*(?:authorization|proxy[-_]?authorization|cookie|set[-_]?cookie|api[-_]?key|access[-_]?token|refresh[-_]?token|client[-_]?secret|private[-_]?key|password|passwd|credential|token|secret)[A-Za-z0-9_-]*)\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s,;]+)/gim;
|
|
3670
|
-
var
|
|
2105
|
+
var isRecord4 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3671
2106
|
var sanitizeControls = (value) => {
|
|
3672
2107
|
let safe = "";
|
|
3673
2108
|
for (const character of value) {
|
|
@@ -3693,15 +2128,15 @@ var redactStructured = (value, depth = 0) => {
|
|
|
3693
2128
|
if (Array.isArray(value)) {
|
|
3694
2129
|
return value.map((item) => redactStructured(item, depth + 1));
|
|
3695
2130
|
}
|
|
3696
|
-
if (!
|
|
2131
|
+
if (!isRecord4(value)) {
|
|
3697
2132
|
return typeof value === "string" ? sanitizeStepLogText(value) : value;
|
|
3698
2133
|
}
|
|
3699
2134
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
3700
2135
|
key,
|
|
3701
|
-
|
|
2136
|
+
SECRET_KEY2.test(`-${key}-`) ? REDACTED : redactStructured(item, depth + 1)
|
|
3702
2137
|
]));
|
|
3703
2138
|
};
|
|
3704
|
-
var redactCommonCredentials = (value) => value.replaceAll(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, `$1 ${REDACTED}`).replace(LABELED_VALUE, (original, prefix, key) =>
|
|
2139
|
+
var redactCommonCredentials = (value) => value.replaceAll(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, `$1 ${REDACTED}`).replace(LABELED_VALUE, (original, prefix, key) => SECRET_KEY2.test(`-${key}-`) ? `${prefix}${key}=${REDACTED}` : original);
|
|
3705
2140
|
function redactStepLogText(value) {
|
|
3706
2141
|
return redactCommonCredentials(sanitizeStepLogText(value));
|
|
3707
2142
|
}
|
|
@@ -3723,7 +2158,7 @@ var contentText = (content) => {
|
|
|
3723
2158
|
if (!Array.isArray(content))
|
|
3724
2159
|
return "";
|
|
3725
2160
|
return content.flatMap((item) => {
|
|
3726
|
-
if (!
|
|
2161
|
+
if (!isRecord4(item))
|
|
3727
2162
|
return [];
|
|
3728
2163
|
if (item.type === "text" && typeof item.text === "string") {
|
|
3729
2164
|
return [item.text];
|
|
@@ -3737,24 +2172,24 @@ var contentText = (content) => {
|
|
|
3737
2172
|
`);
|
|
3738
2173
|
};
|
|
3739
2174
|
function textOnlyUserMessage(message) {
|
|
3740
|
-
if (!
|
|
2175
|
+
if (!isRecord4(message) || message.role !== "user")
|
|
3741
2176
|
return;
|
|
3742
2177
|
if (typeof message.content === "string")
|
|
3743
2178
|
return message.content;
|
|
3744
|
-
if (!Array.isArray(message.content) || message.content.some((item) => !
|
|
2179
|
+
if (!Array.isArray(message.content) || message.content.some((item) => !isRecord4(item) || item.type !== "text" || typeof item.text !== "string")) {
|
|
3745
2180
|
return;
|
|
3746
2181
|
}
|
|
3747
2182
|
return message.content.map((item) => item.text).join(`
|
|
3748
2183
|
`);
|
|
3749
2184
|
}
|
|
3750
2185
|
function stepLogLinesFromMessage(message) {
|
|
3751
|
-
if (!
|
|
2186
|
+
if (!isRecord4(message))
|
|
3752
2187
|
return [];
|
|
3753
2188
|
if (message.role === "assistant") {
|
|
3754
2189
|
if (!Array.isArray(message.content))
|
|
3755
2190
|
return [];
|
|
3756
2191
|
const contentLines = message.content.flatMap((item) => {
|
|
3757
|
-
if (!
|
|
2192
|
+
if (!isRecord4(item))
|
|
3758
2193
|
return [];
|
|
3759
2194
|
if (item.type === "text" && typeof item.text === "string") {
|
|
3760
2195
|
const text = redactStepLogText(item.text);
|
|
@@ -3866,8 +2301,97 @@ function registerMainStepPolicy({
|
|
|
3866
2301
|
reason: authorization.reason ?? "Tool blocked by main workflow policy"
|
|
3867
2302
|
};
|
|
3868
2303
|
}
|
|
3869
|
-
dependencies.freezeToolInput(event.input);
|
|
3870
|
-
});
|
|
2304
|
+
dependencies.freezeToolInput(event.input);
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
// src/runtime/step-result.ts
|
|
2309
|
+
import { isAbsolute as isAbsolute3 } from "node:path";
|
|
2310
|
+
var MAX_ARTIFACT_CHARS = 200000;
|
|
2311
|
+
var RESULT_KEYS = new Set([
|
|
2312
|
+
"version",
|
|
2313
|
+
"policyDigest",
|
|
2314
|
+
"outcome",
|
|
2315
|
+
"summary",
|
|
2316
|
+
"artifact",
|
|
2317
|
+
"workspace"
|
|
2318
|
+
]);
|
|
2319
|
+
var isObject = (value) => {
|
|
2320
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2321
|
+
};
|
|
2322
|
+
function parseResultWorkspace(value, outcome, policy) {
|
|
2323
|
+
const requiresWorkspace = policy.workspace?.bindOn.includes(outcome) === true;
|
|
2324
|
+
if (!requiresWorkspace) {
|
|
2325
|
+
if (value !== undefined) {
|
|
2326
|
+
throw new Error("workflow step workspace is forbidden for this outcome");
|
|
2327
|
+
}
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
if (!isObject(value)) {
|
|
2331
|
+
throw new Error(`workflow step outcome "${outcome}" requires workspace.cwd`);
|
|
2332
|
+
}
|
|
2333
|
+
const unknownKey = Object.keys(value).find((key) => key !== "cwd");
|
|
2334
|
+
if (unknownKey) {
|
|
2335
|
+
throw new Error(`workflow step workspace has unknown property "${unknownKey}"`);
|
|
2336
|
+
}
|
|
2337
|
+
if (typeof value.cwd !== "string") {
|
|
2338
|
+
throw new Error("workflow step workspace cwd must be a string");
|
|
2339
|
+
}
|
|
2340
|
+
const cwd = value.cwd;
|
|
2341
|
+
if (!cwd || cwd.includes("\x00") || !isAbsolute3(cwd)) {
|
|
2342
|
+
throw new Error("workflow step workspace cwd must be an absolute path");
|
|
2343
|
+
}
|
|
2344
|
+
if (cwd.length > MAX_WORKSPACE_PATH_CHARS) {
|
|
2345
|
+
throw new Error(`workflow step workspace cwd exceeds ${MAX_WORKSPACE_PATH_CHARS} characters`);
|
|
2346
|
+
}
|
|
2347
|
+
return { cwd };
|
|
2348
|
+
}
|
|
2349
|
+
function parseWorkflowStepResult(value, policy) {
|
|
2350
|
+
if (!isObject(value)) {
|
|
2351
|
+
throw new Error("workflow step result must be an object");
|
|
2352
|
+
}
|
|
2353
|
+
const unknownKey = Object.keys(value).find((key) => !RESULT_KEYS.has(key));
|
|
2354
|
+
if (unknownKey) {
|
|
2355
|
+
throw new Error(`workflow step result has unknown property "${unknownKey}"`);
|
|
2356
|
+
}
|
|
2357
|
+
if (value.version !== 1) {
|
|
2358
|
+
throw new Error("unsupported workflow step result version");
|
|
2359
|
+
}
|
|
2360
|
+
if (value.policyDigest !== policy.policyDigest) {
|
|
2361
|
+
throw new Error("workflow step result does not match the active policy");
|
|
2362
|
+
}
|
|
2363
|
+
if (typeof value.outcome !== "string" || !policy.outcomes.includes(value.outcome)) {
|
|
2364
|
+
throw new Error(`workflow step returned invalid outcome "${String(value.outcome)}"`);
|
|
2365
|
+
}
|
|
2366
|
+
if (typeof value.summary !== "string") {
|
|
2367
|
+
throw new Error("workflow step summary must be a string");
|
|
2368
|
+
}
|
|
2369
|
+
const summary = value.summary.trim();
|
|
2370
|
+
if (!summary) {
|
|
2371
|
+
throw new Error("workflow step summary must not be empty");
|
|
2372
|
+
}
|
|
2373
|
+
if (summary.length > policy.summaryMaxChars) {
|
|
2374
|
+
throw new Error(`workflow step summary exceeds ${policy.summaryMaxChars} characters`);
|
|
2375
|
+
}
|
|
2376
|
+
if (value.artifact !== undefined && typeof value.artifact !== "string") {
|
|
2377
|
+
throw new Error("workflow step artifact must be a string");
|
|
2378
|
+
}
|
|
2379
|
+
const artifact = typeof value.artifact === "string" ? value.artifact : undefined;
|
|
2380
|
+
if (artifact !== undefined && artifact.length > MAX_ARTIFACT_CHARS) {
|
|
2381
|
+
throw new Error(`workflow step artifact exceeds ${MAX_ARTIFACT_CHARS} characters`);
|
|
2382
|
+
}
|
|
2383
|
+
if (value.outcome === policy.gateSubmitOutcome && (!artifact || !artifact.trim())) {
|
|
2384
|
+
throw new Error("workflow gate outcome requires a non-empty artifact");
|
|
2385
|
+
}
|
|
2386
|
+
const workspace = parseResultWorkspace(value.workspace, value.outcome, policy);
|
|
2387
|
+
return {
|
|
2388
|
+
version: 1,
|
|
2389
|
+
policyDigest: policy.policyDigest,
|
|
2390
|
+
outcome: value.outcome,
|
|
2391
|
+
summary,
|
|
2392
|
+
...artifact !== undefined ? { artifact } : {},
|
|
2393
|
+
...workspace ? { workspace } : {}
|
|
2394
|
+
};
|
|
3871
2395
|
}
|
|
3872
2396
|
|
|
3873
2397
|
// src/runtime/main-step-runtime.ts
|
|
@@ -4296,7 +2820,8 @@ function formatWorkflowProgressStatus(snapshot, statusShortcutLabel) {
|
|
|
4296
2820
|
const { run, workflow } = snapshot;
|
|
4297
2821
|
const currentStep = formatStepName(stepTitle(workflow, run.currentStepId), run.currentStepId);
|
|
4298
2822
|
const activity = run.status === "awaiting-gate" ? "awaiting review" : "working";
|
|
4299
|
-
|
|
2823
|
+
const workerProgress = snapshot.execution?.kind === "subagent" ? ` · ${snapshot.execution.progress}` : "";
|
|
2824
|
+
return `${workflowStatusIcon(run, snapshot.now)} ${run.workflowId} · step ${currentStep} · ${activity}${workerProgress} · ${statusShortcutLabel}`;
|
|
4300
2825
|
}
|
|
4301
2826
|
// src/workflow-status/view.ts
|
|
4302
2827
|
import {
|
|
@@ -4440,6 +2965,46 @@ function attemptDisplayOrdinal(attempt, index, omittedAttempts) {
|
|
|
4440
2965
|
return index + 1;
|
|
4441
2966
|
return omittedAttempts + index + 1;
|
|
4442
2967
|
}
|
|
2968
|
+
function renderLiveWorkerSession(theme, snapshot, detail, width) {
|
|
2969
|
+
if (snapshot.execution?.kind !== "subagent")
|
|
2970
|
+
return [];
|
|
2971
|
+
const attempt = detail.attempts.at(-1);
|
|
2972
|
+
const events = snapshot.execution.activityLog ?? [];
|
|
2973
|
+
const eventLines = events.flatMap((event) => {
|
|
2974
|
+
const isToolCall = event.startsWith("call ");
|
|
2975
|
+
const isResponse = event.startsWith("response: ");
|
|
2976
|
+
const label = isToolCall ? "● Tool call" : isResponse ? "● Assistant" : "● Worker";
|
|
2977
|
+
const value = isResponse ? event.slice("response: ".length) : event;
|
|
2978
|
+
return [
|
|
2979
|
+
theme.bold(theme.fg(isToolCall ? "warning" : "accent", label)),
|
|
2980
|
+
...wrapPlain(value, width, theme),
|
|
2981
|
+
""
|
|
2982
|
+
];
|
|
2983
|
+
});
|
|
2984
|
+
return [
|
|
2985
|
+
"",
|
|
2986
|
+
theme.bold(theme.fg("accent", "Live Worker Session")),
|
|
2987
|
+
...keyValueLines(theme, "worker", snapshot.execution.agent, width, "accent"),
|
|
2988
|
+
...keyValueLines(theme, "state", snapshot.execution.progress, width, "accent"),
|
|
2989
|
+
"",
|
|
2990
|
+
theme.bold(theme.fg("accent", "● Input prompt")),
|
|
2991
|
+
...wrapPlain(attempt ? `${attempt.task}${attempt.taskTruncated ? `
|
|
2992
|
+
… [${attempt.omittedTaskChars ?? 0} prompt characters omitted from the bounded checkpoint trace]` : ""}` : "The worker prompt is being prepared.", width, theme),
|
|
2993
|
+
"",
|
|
2994
|
+
theme.bold(theme.fg("accent", "● Live message chain")),
|
|
2995
|
+
...eventLines.length > 0 ? eventLines : [theme.fg("muted", "Waiting for the worker to emit activity…"), ""]
|
|
2996
|
+
];
|
|
2997
|
+
}
|
|
2998
|
+
function renderLiveWorkerActivity(theme, snapshot, selectedIndex, width) {
|
|
2999
|
+
const entry = buildPathEntries(snapshot)[selectedIndex];
|
|
3000
|
+
const detail = selectedStepDetail(snapshot, selectedIndex);
|
|
3001
|
+
if (!entry?.isCurrent || !detail || snapshot.execution?.kind !== "subagent") {
|
|
3002
|
+
return boxed(theme, "Live Worker Session", width, [
|
|
3003
|
+
theme.fg("muted", "Live activity is available only for the active worker step.")
|
|
3004
|
+
], "borderAccent");
|
|
3005
|
+
}
|
|
3006
|
+
return boxed(theme, `Live Worker Session · ${entry.title} · visit ${entry.visit}`, width, renderLiveWorkerSession(theme, snapshot, detail, width - 4), "borderAccent");
|
|
3007
|
+
}
|
|
4443
3008
|
function renderStepDetail(theme, snapshot, selectedIndex, cache, width) {
|
|
4444
3009
|
const entries = buildPathEntries(snapshot);
|
|
4445
3010
|
const entry = entries[selectedIndex];
|
|
@@ -4487,9 +3052,9 @@ function renderStepDetail(theme, snapshot, selectedIndex, cache, width) {
|
|
|
4487
3052
|
}
|
|
4488
3053
|
|
|
4489
3054
|
// src/workflow-status/transcript-reader.ts
|
|
4490
|
-
import { constants
|
|
4491
|
-
import { lstat
|
|
4492
|
-
import { isAbsolute as
|
|
3055
|
+
import { constants } from "node:fs";
|
|
3056
|
+
import { lstat, open, realpath as realpath2 } from "node:fs/promises";
|
|
3057
|
+
import { isAbsolute as isAbsolute5, relative as relative2, resolve as resolve4, sep as sep2 } from "node:path";
|
|
4493
3058
|
|
|
4494
3059
|
// src/engine/create-run.ts
|
|
4495
3060
|
var createRun = (workflow, input, baselineTools, runId, now, cwd, iteration = 1) => {
|
|
@@ -4519,10 +3084,9 @@ var createRun = (workflow, input, baselineTools, runId, now, cwd, iteration = 1)
|
|
|
4519
3084
|
};
|
|
4520
3085
|
};
|
|
4521
3086
|
// src/engine/run-validation.ts
|
|
4522
|
-
import { isAbsolute as
|
|
3087
|
+
import { isAbsolute as isAbsolute4, resolve as resolve3 } from "node:path";
|
|
4523
3088
|
|
|
4524
3089
|
// src/engine/step-trace.ts
|
|
4525
|
-
import { isAbsolute as isAbsolute7, relative as relative4, resolve as resolve7, sep as sep3 } from "node:path";
|
|
4526
3090
|
var COMPACTED_FIELD_CHARS = 512;
|
|
4527
3091
|
var COMPACTED_LOG_CHARS = 4096;
|
|
4528
3092
|
function logChars(lines) {
|
|
@@ -4755,29 +3319,6 @@ function appendMainStepLog(run, requestId, lines, now) {
|
|
|
4755
3319
|
updatedAt: now
|
|
4756
3320
|
});
|
|
4757
3321
|
}
|
|
4758
|
-
function isSafeTranscriptReference(reference) {
|
|
4759
|
-
if (!isAbsolute7(reference.trustedRoot) || !isAbsolute7(reference.sessionFile) || reference.trustedRoot.includes("\x00") || reference.sessionFile.includes("\x00") || !reference.runId || reference.runId.includes("\x00") || reference.runId.includes("/") || reference.runId.includes("\\") || reference.runId === "." || reference.runId === ".." || !Number.isSafeInteger(reference.childIndex) || reference.childIndex < 0) {
|
|
4760
|
-
return false;
|
|
4761
|
-
}
|
|
4762
|
-
const expected = resolve7(reference.trustedRoot, reference.runId, `run-${reference.childIndex}`, "session.jsonl");
|
|
4763
|
-
const relativePath = relative4(resolve7(reference.trustedRoot), resolve7(reference.sessionFile));
|
|
4764
|
-
const isWithinTrustedRoot = relativePath !== "" && relativePath !== ".." && !relativePath.startsWith(`..${sep3}`) && !isAbsolute7(relativePath);
|
|
4765
|
-
return isWithinTrustedRoot && resolve7(reference.sessionFile) === expected;
|
|
4766
|
-
}
|
|
4767
|
-
function attachSubagentTranscript(run, requestId, reference, now) {
|
|
4768
|
-
if (!isSafeTranscriptReference(reference))
|
|
4769
|
-
return run;
|
|
4770
|
-
const attempts = run.currentStepAttempts;
|
|
4771
|
-
const index = attempts?.findIndex((attempt2) => attempt2.requestId === requestId && attempt2.kind === "subagent");
|
|
4772
|
-
if (index === undefined || index < 0 || !attempts)
|
|
4773
|
-
return run;
|
|
4774
|
-
const currentStepAttempts = [...attempts];
|
|
4775
|
-
const attempt = currentStepAttempts[index];
|
|
4776
|
-
if (!attempt || attempt.kind !== "subagent")
|
|
4777
|
-
return run;
|
|
4778
|
-
currentStepAttempts[index] = { ...attempt, transcript: reference };
|
|
4779
|
-
return compactRunTraceBudget({ ...run, currentStepAttempts, updatedAt: now });
|
|
4780
|
-
}
|
|
4781
3322
|
function attemptResult(result, workspaceCwd) {
|
|
4782
3323
|
const summary = result.summary.slice(0, MAX_STEP_TRACE_SUMMARY_CHARS);
|
|
4783
3324
|
const artifact = result.artifact?.slice(0, MAX_STEP_TRACE_ARTIFACT_CHARS);
|
|
@@ -4835,11 +3376,11 @@ function recordCurrentGateDecision(run, decision, now) {
|
|
|
4835
3376
|
}
|
|
4836
3377
|
|
|
4837
3378
|
// src/engine/run-validation.ts
|
|
4838
|
-
var
|
|
4839
|
-
var isAbsoluteCwd = (value) => typeof value === "string" && value.length > 0 &&
|
|
4840
|
-
var isGateApproval = (value) =>
|
|
4841
|
-
var isStepAttemptResult = (value) =>
|
|
4842
|
-
var isStepGateDecision = (value) =>
|
|
3379
|
+
var isRecord5 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3380
|
+
var isAbsoluteCwd = (value) => typeof value === "string" && value.length > 0 && isAbsolute4(value) && !value.includes("\x00");
|
|
3381
|
+
var isGateApproval = (value) => isRecord5(value) && typeof value.requestId === "string" && value.requestId.length > 0 && typeof value.artifact === "string" && value.artifact.trim().length > 0 && typeof value.feedback === "string" && value.feedback.length <= MAX_GATE_FEEDBACK_CHARS && typeof value.stepStructuralDigest === "string" && value.stepStructuralDigest.length > 0;
|
|
3382
|
+
var isStepAttemptResult = (value) => isRecord5(value) && typeof value.outcome === "string" && typeof value.summary === "string" && value.summary.length <= MAX_STEP_TRACE_SUMMARY_CHARS && (value.summaryTruncated === undefined || value.summaryTruncated === true) && (value.artifact === undefined || typeof value.artifact === "string") && (typeof value.artifact !== "string" || value.artifact.length <= MAX_STEP_TRACE_ARTIFACT_CHARS) && (value.artifactTruncated === undefined || value.artifactTruncated === true) && !(value.artifactTruncated === true && value.artifact === undefined) && (value.workspaceCwd === undefined || isAbsoluteCwd(value.workspaceCwd));
|
|
3383
|
+
var isStepGateDecision = (value) => isRecord5(value) && (value.provider === "prompt" || value.provider === "plannotator") && typeof value.requestId === "string" && value.requestId.length > 0 && typeof value.approved === "boolean" && typeof value.feedback === "string" && value.feedback.length <= MAX_STEP_TRACE_SUMMARY_CHARS && (value.feedbackTruncated === undefined || value.feedbackTruncated === true) && typeof value.resolvedAt === "number" && (value.reviewId === undefined || typeof value.reviewId === "string");
|
|
4843
3384
|
var isSafeTraceIdentityField = (value) => typeof value === "string" && value.length > 0 && !value.includes("\x00") && !value.includes("/") && !value.includes("\\") && value !== "." && value !== "..";
|
|
4844
3385
|
var hasValidMainStepLog = (value) => {
|
|
4845
3386
|
const log = value.log;
|
|
@@ -4855,7 +3396,7 @@ var hasValidMainStepLog = (value) => {
|
|
|
4855
3396
|
return value.logTruncated === true === (typeof value.omittedLogEvents === "number") && !(log === undefined && (value.logTruncated !== undefined || value.omittedLogEvents !== undefined));
|
|
4856
3397
|
};
|
|
4857
3398
|
var isStepExecutionAttempt = (value) => {
|
|
4858
|
-
if (!
|
|
3399
|
+
if (!isRecord5(value) || value.kind !== "main" && value.kind !== "subagent" || typeof value.requestId !== "string" || value.requestId.length === 0 || value.requestId.includes("\x00") || value.ordinal !== undefined && (!Number.isSafeInteger(value.ordinal) || value.ordinal <= 0) || typeof value.task !== "string" || value.task.trim().length === 0 || value.task.length > MAX_STEP_TRACE_TASK_CHARS || value.taskTruncated !== undefined && value.taskTruncated !== true || value.omittedTaskChars !== undefined && (!Number.isSafeInteger(value.omittedTaskChars) || value.omittedTaskChars <= 0) || value.taskTruncated === true !== (typeof value.omittedTaskChars === "number") || typeof value.startedAt !== "number" || value.result !== undefined && !isStepAttemptResult(value.result) || value.gateDecision !== undefined && !isStepGateDecision(value.gateDecision)) {
|
|
4859
3400
|
return false;
|
|
4860
3401
|
}
|
|
4861
3402
|
if (value.kind === "main") {
|
|
@@ -4869,13 +3410,13 @@ var isStepExecutionAttempt = (value) => {
|
|
|
4869
3410
|
}
|
|
4870
3411
|
if (value.transcript === undefined)
|
|
4871
3412
|
return true;
|
|
4872
|
-
if (!
|
|
3413
|
+
if (!isRecord5(value.transcript))
|
|
4873
3414
|
return false;
|
|
4874
3415
|
const transcript = value.transcript;
|
|
4875
3416
|
if (!isAbsoluteCwd(transcript.trustedRoot) || !isAbsoluteCwd(transcript.sessionFile) || !isSafeTraceIdentityField(transcript.runId) || !Number.isSafeInteger(transcript.childIndex) || transcript.childIndex < 0) {
|
|
4876
3417
|
return false;
|
|
4877
3418
|
}
|
|
4878
|
-
return
|
|
3419
|
+
return resolve3(transcript.trustedRoot, transcript.runId, `run-${String(transcript.childIndex)}`, "session.jsonl") === resolve3(transcript.sessionFile);
|
|
4879
3420
|
};
|
|
4880
3421
|
var isStepExecutionAttempts = (value) => Array.isArray(value) && value.length <= MAX_STEP_TRACE_ATTEMPTS && value.every(isStepExecutionAttempt) && new Set(value.map((attempt) => attempt.requestId)).size === value.length && value.every((attempt, index) => {
|
|
4881
3422
|
if (attempt.ordinal === undefined)
|
|
@@ -4883,10 +3424,10 @@ var isStepExecutionAttempts = (value) => Array.isArray(value) && value.length <=
|
|
|
4883
3424
|
const ordinal = attempt.ordinal;
|
|
4884
3425
|
return value.slice(0, index).every((earlier) => earlier.ordinal === undefined || earlier.ordinal < ordinal);
|
|
4885
3426
|
});
|
|
4886
|
-
var isStepHistoryEntry = (value) =>
|
|
4887
|
-
var isGateResolution = (value) =>
|
|
4888
|
-
var isPendingGate = (value) =>
|
|
4889
|
-
var isVisitCounts = (value) =>
|
|
3427
|
+
var isStepHistoryEntry = (value) => isRecord5(value) && typeof value.stepId === "string" && typeof value.stepDigest === "string" && typeof value.outcome === "string" && typeof value.summary === "string" && (value.workspaceCwd === undefined || isAbsoluteCwd(value.workspaceCwd)) && (value.artifact === undefined || typeof value.artifact === "string") && (value.approval === undefined || isGateApproval(value.approval) && value.artifact === value.approval.artifact) && (value.attempts === undefined || isStepExecutionAttempts(value.attempts)) && (value.omittedAttempts === undefined || Number.isSafeInteger(value.omittedAttempts) && value.omittedAttempts > 0) && typeof value.completedAt === "number";
|
|
3428
|
+
var isGateResolution = (value) => isRecord5(value) && typeof value.approved === "boolean" && typeof value.feedback === "string" && value.feedback.length <= MAX_GATE_FEEDBACK_CHARS && typeof value.resolvedAt === "number";
|
|
3429
|
+
var isPendingGate = (value) => isRecord5(value) && (value.provider === "prompt" || value.provider === "plannotator") && typeof value.requestId === "string" && value.requestId.length > 0 && typeof value.stepId === "string" && typeof value.artifact === "string" && (value.summary === undefined || typeof value.summary === "string") && typeof value.submittedOutcome === "string" && typeof value.requestedAt === "number" && (value.reviewId === undefined || typeof value.reviewId === "string") && (value.resolution === undefined || isGateResolution(value.resolution));
|
|
3430
|
+
var isVisitCounts = (value) => isRecord5(value) && Object.values(value).every((count) => typeof count === "number" && Number.isInteger(count) && count >= 0);
|
|
4890
3431
|
var isOptionalString = (value) => value === undefined || typeof value === "string";
|
|
4891
3432
|
var isOptionalResumeInput = (value) => value === undefined || typeof value === "string" && value.length <= MAX_RESUME_INPUT_CHARS;
|
|
4892
3433
|
var isOptionalIteration = (value) => value === undefined || Number.isSafeInteger(value) && value >= 1;
|
|
@@ -4911,7 +3452,7 @@ var hasValidWorkspaceState = (run, history) => {
|
|
|
4911
3452
|
return startCwd === undefined || cwd === startCwd;
|
|
4912
3453
|
};
|
|
4913
3454
|
var isWorkflowRun = (value) => {
|
|
4914
|
-
if (!
|
|
3455
|
+
if (!isRecord5(value))
|
|
4915
3456
|
return false;
|
|
4916
3457
|
const hasValidRequiredFields = value.stateVersion === RUN_STATE_VERSION && typeof value.runId === "string" && typeof value.workflowId === "string" && typeof value.workflowDigest === "string" && typeof value.input === "string" && isWorkflowRunStatus(value.status) && typeof value.currentStepId === "string" && typeof value.currentStepDigest === "string" && Array.isArray(value.baselineTools) && value.baselineTools.every((tool) => typeof tool === "string") && Array.isArray(value.history) && value.history.every(isStepHistoryEntry) && (value.currentStepAttempts === undefined || isStepExecutionAttempts(value.currentStepAttempts)) && (value.currentStepOmittedAttempts === undefined || Number.isSafeInteger(value.currentStepOmittedAttempts) && value.currentStepOmittedAttempts > 0) && isVisitCounts(value.visits) && typeof value.startedAt === "number" && typeof value.updatedAt === "number" && typeof value.lastSummary === "string" && typeof value.gateFeedback === "string" && value.gateFeedback.length <= MAX_GATE_FEEDBACK_CHARS;
|
|
4917
3458
|
if (!hasValidRequiredFields)
|
|
@@ -4929,18 +3470,18 @@ var isWorkflowRun = (value) => {
|
|
|
4929
3470
|
};
|
|
4930
3471
|
// src/workflow-status/transcript-reader.ts
|
|
4931
3472
|
var MAX_TRANSCRIPT_BYTES = 2 * 1024 * 1024;
|
|
4932
|
-
function
|
|
3473
|
+
function isRecord6(value) {
|
|
4933
3474
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4934
3475
|
}
|
|
4935
3476
|
function isWithin(root, candidate) {
|
|
4936
|
-
const pathFromRoot =
|
|
4937
|
-
return pathFromRoot !== "" && pathFromRoot !== ".." && !pathFromRoot.startsWith(`..${
|
|
3477
|
+
const pathFromRoot = relative2(resolve4(root), resolve4(candidate));
|
|
3478
|
+
return pathFromRoot !== "" && pathFromRoot !== ".." && !pathFromRoot.startsWith(`..${sep2}`) && !isAbsolute5(pathFromRoot);
|
|
4938
3479
|
}
|
|
4939
3480
|
function hasSafeIdentity(reference) {
|
|
4940
|
-
return
|
|
3481
|
+
return isAbsolute5(reference.trustedRoot) && isAbsolute5(reference.sessionFile) && !reference.trustedRoot.includes("\x00") && !reference.sessionFile.includes("\x00") && reference.runId.length > 0 && !reference.runId.includes("\x00") && !reference.runId.includes("/") && !reference.runId.includes("\\") && reference.runId !== "." && reference.runId !== ".." && Number.isSafeInteger(reference.childIndex) && reference.childIndex >= 0 && resolve4(reference.sessionFile) === resolve4(reference.trustedRoot, reference.runId, `run-${reference.childIndex}`, "session.jsonl");
|
|
4941
3482
|
}
|
|
4942
3483
|
function transcriptEntryLines(entry) {
|
|
4943
|
-
if (!
|
|
3484
|
+
if (!isRecord6(entry))
|
|
4944
3485
|
return [];
|
|
4945
3486
|
if (entry.type === "custom_message") {
|
|
4946
3487
|
const customType = typeof entry.customType === "string" ? entry.customType : "custom";
|
|
@@ -4948,7 +3489,7 @@ function transcriptEntryLines(entry) {
|
|
|
4948
3489
|
return content ? [`event ${sanitizeStepLogText(customType)}
|
|
4949
3490
|
${content}`] : [];
|
|
4950
3491
|
}
|
|
4951
|
-
if (entry.type !== "message" || !
|
|
3492
|
+
if (entry.type !== "message" || !isRecord6(entry.message))
|
|
4952
3493
|
return [];
|
|
4953
3494
|
return stepLogLinesFromMessage(entry.message);
|
|
4954
3495
|
}
|
|
@@ -4988,11 +3529,11 @@ async function readStepTranscript(reference) {
|
|
|
4988
3529
|
};
|
|
4989
3530
|
}
|
|
4990
3531
|
try {
|
|
4991
|
-
const runDirectory =
|
|
4992
|
-
const childDirectory =
|
|
3532
|
+
const runDirectory = resolve4(reference.trustedRoot, reference.runId);
|
|
3533
|
+
const childDirectory = resolve4(runDirectory, `run-${reference.childIndex}`);
|
|
4993
3534
|
const [runDirectoryInfo, childDirectoryInfo] = await Promise.all([
|
|
4994
|
-
|
|
4995
|
-
|
|
3535
|
+
lstat(runDirectory),
|
|
3536
|
+
lstat(childDirectory)
|
|
4996
3537
|
]);
|
|
4997
3538
|
if (runDirectoryInfo.isSymbolicLink() || !runDirectoryInfo.isDirectory() || childDirectoryInfo.isSymbolicLink() || !childDirectoryInfo.isDirectory()) {
|
|
4998
3539
|
return {
|
|
@@ -5000,7 +3541,7 @@ async function readStepTranscript(reference) {
|
|
|
5000
3541
|
reason: "The recorded child transcript directory identity is not trusted."
|
|
5001
3542
|
};
|
|
5002
3543
|
}
|
|
5003
|
-
const inspected = await
|
|
3544
|
+
const inspected = await lstat(reference.sessionFile);
|
|
5004
3545
|
if (inspected.isSymbolicLink() || !inspected.isFile()) {
|
|
5005
3546
|
return {
|
|
5006
3547
|
status: "unavailable",
|
|
@@ -5008,17 +3549,17 @@ async function readStepTranscript(reference) {
|
|
|
5008
3549
|
};
|
|
5009
3550
|
}
|
|
5010
3551
|
const [canonicalRoot, canonicalSession] = await Promise.all([
|
|
5011
|
-
|
|
5012
|
-
|
|
3552
|
+
realpath2(reference.trustedRoot),
|
|
3553
|
+
realpath2(reference.sessionFile)
|
|
5013
3554
|
]);
|
|
5014
|
-
const canonicalExpected =
|
|
3555
|
+
const canonicalExpected = resolve4(canonicalRoot, reference.runId, `run-${reference.childIndex}`, "session.jsonl");
|
|
5015
3556
|
if (!isWithin(canonicalRoot, canonicalSession) || canonicalSession !== canonicalExpected) {
|
|
5016
3557
|
return {
|
|
5017
3558
|
status: "unavailable",
|
|
5018
3559
|
reason: "The recorded child transcript does not match its trusted canonical identity."
|
|
5019
3560
|
};
|
|
5020
3561
|
}
|
|
5021
|
-
const handle = await
|
|
3562
|
+
const handle = await open(canonicalSession, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
5022
3563
|
try {
|
|
5023
3564
|
const before = await handle.stat();
|
|
5024
3565
|
if (!before.isFile()) {
|
|
@@ -5140,7 +3681,9 @@ class WorkflowStatusView {
|
|
|
5140
3681
|
return;
|
|
5141
3682
|
}
|
|
5142
3683
|
if (matchesKey(data, "escape")) {
|
|
5143
|
-
if (this.state.mode === "
|
|
3684
|
+
if (this.state.mode === "live") {
|
|
3685
|
+
this.showDetail();
|
|
3686
|
+
} else if (this.state.mode === "detail") {
|
|
5144
3687
|
this.showBoard();
|
|
5145
3688
|
} else {
|
|
5146
3689
|
this.close();
|
|
@@ -5150,7 +3693,14 @@ class WorkflowStatusView {
|
|
|
5150
3693
|
const pageSize = Math.max(1, this.state.viewportRows - 2);
|
|
5151
3694
|
const contentHeight = Math.max(1, this.state.viewportRows - 1);
|
|
5152
3695
|
const halfPageSize = Math.max(1, Math.floor(contentHeight / 2));
|
|
5153
|
-
if (this.state.mode === "detail") {
|
|
3696
|
+
if (this.state.mode === "detail" || this.state.mode === "live") {
|
|
3697
|
+
if (matchesKey(data, Key.tab)) {
|
|
3698
|
+
if (this.state.mode === "detail")
|
|
3699
|
+
this.showLive();
|
|
3700
|
+
else
|
|
3701
|
+
this.showDetail();
|
|
3702
|
+
return;
|
|
3703
|
+
}
|
|
5154
3704
|
if (data === "gg" || data === "g" && this.pendingDetailTopKey) {
|
|
5155
3705
|
this.pendingDetailTopKey = false;
|
|
5156
3706
|
this.setScrollOffset(0);
|
|
@@ -5164,7 +3714,10 @@ class WorkflowStatusView {
|
|
|
5164
3714
|
if (data === "G") {
|
|
5165
3715
|
this.setScrollOffset(Number.MAX_SAFE_INTEGER);
|
|
5166
3716
|
} else if (matchesKey(data, Key.left) || data === "h") {
|
|
5167
|
-
this.
|
|
3717
|
+
if (this.state.mode === "live")
|
|
3718
|
+
this.showDetail();
|
|
3719
|
+
else
|
|
3720
|
+
this.showBoard();
|
|
5168
3721
|
} else if (matchesKey(data, Key.down) || data === "j") {
|
|
5169
3722
|
this.setScrollOffset(this.state.scrollOffset + 1);
|
|
5170
3723
|
} else if (matchesKey(data, Key.up) || data === "k") {
|
|
@@ -5211,12 +3764,12 @@ class WorkflowStatusView {
|
|
|
5211
3764
|
const contentWidth = viewportWidth - 2;
|
|
5212
3765
|
if (snapshot)
|
|
5213
3766
|
this.normalizeSelection(snapshot);
|
|
5214
|
-
const lines = snapshot ? this.state.mode === "detail" ? renderStepDetail(this.theme, snapshot, this.state.selectedIndex, this.transcriptCache, contentWidth) : renderBoard(this.theme, snapshot, contentWidth, false, this.statusShortcutLabel, this.state.selectedIndex) : renderEmptyBoard(this.theme, contentWidth);
|
|
3767
|
+
const lines = snapshot ? this.state.mode === "detail" ? renderStepDetail(this.theme, snapshot, this.state.selectedIndex, this.transcriptCache, contentWidth) : this.state.mode === "live" ? renderLiveWorkerActivity(this.theme, snapshot, this.state.selectedIndex, contentWidth) : renderBoard(this.theme, snapshot, contentWidth, false, this.statusShortcutLabel, this.state.selectedIndex) : renderEmptyBoard(this.theme, contentWidth);
|
|
5215
3768
|
if (snapshot && this.state.mode === "detail") {
|
|
5216
3769
|
this.ensureSelectedTranscripts(snapshot);
|
|
5217
3770
|
}
|
|
5218
3771
|
const rendered = lines.map((line) => padAnsi(truncateToWidth4(line, contentWidth, "…"), viewportWidth));
|
|
5219
|
-
const page = paginateBoard(this.state, rendered, viewportWidth, this.tui.terminal?.rows, this.statusShortcutLabel, this.theme, this.state.mode === "detail" ? "↑↓/jk · Ctrl+D/U half-page · gg/G top/bottom · PgUp/PgDn · ←/h/Esc" : "↑/↓ or j/k select · Enter/→/l inspect · PgUp/PgDn");
|
|
3772
|
+
const page = paginateBoard(this.state, rendered, viewportWidth, this.tui.terminal?.rows, this.statusShortcutLabel, this.theme, this.state.mode === "detail" ? "↑↓/jk · Ctrl+D/U half-page · gg/G top/bottom · PgUp/PgDn · Tab live · ←/h/Esc" : this.state.mode === "live" ? "↑↓/jk · Ctrl+D/U half-page · gg/G top/bottom · PgUp/PgDn · Tab/←/h/Esc overview" : "↑/↓ or j/k select · Enter/→/l inspect · PgUp/PgDn");
|
|
5220
3773
|
this.state = page.state;
|
|
5221
3774
|
return page.lines;
|
|
5222
3775
|
}
|
|
@@ -5270,6 +3823,24 @@ class WorkflowStatusView {
|
|
|
5270
3823
|
this.state = { ...this.state, mode: "board", scrollOffset: 0 };
|
|
5271
3824
|
this.tui.requestRender(true);
|
|
5272
3825
|
}
|
|
3826
|
+
showDetail() {
|
|
3827
|
+
if (this.state.mode === "detail")
|
|
3828
|
+
return;
|
|
3829
|
+
this.pendingDetailTopKey = false;
|
|
3830
|
+
this.state = { ...this.state, mode: "detail", scrollOffset: 0 };
|
|
3831
|
+
this.tui.requestRender(true);
|
|
3832
|
+
}
|
|
3833
|
+
showLive() {
|
|
3834
|
+
const snapshot = this.getSnapshot();
|
|
3835
|
+
if (!snapshot)
|
|
3836
|
+
return;
|
|
3837
|
+
const entry = buildPathEntries(snapshot)[this.state.selectedIndex];
|
|
3838
|
+
if (!entry?.isCurrent || snapshot.execution?.kind !== "subagent")
|
|
3839
|
+
return;
|
|
3840
|
+
this.pendingDetailTopKey = false;
|
|
3841
|
+
this.state = { ...this.state, mode: "live", scrollOffset: 0 };
|
|
3842
|
+
this.tui.requestRender(true);
|
|
3843
|
+
}
|
|
5273
3844
|
ensureSelectedTranscripts(snapshot) {
|
|
5274
3845
|
const detail = selectedStepDetail(snapshot, this.state.selectedIndex);
|
|
5275
3846
|
if (!detail)
|
|
@@ -5324,21 +3895,21 @@ async function showWorkflowStatus(ctx, getSnapshot, statusShortcut = DEFAULT_STA
|
|
|
5324
3895
|
// src/harness/workspace-directory.ts
|
|
5325
3896
|
import { realpathSync, statSync } from "node:fs";
|
|
5326
3897
|
import { homedir as homedir2 } from "node:os";
|
|
5327
|
-
import { isAbsolute as
|
|
3898
|
+
import { isAbsolute as isAbsolute6, relative as relative3, resolve as resolve5, sep as sep3, win32 as win322 } from "node:path";
|
|
5328
3899
|
var isWithin2 = (root, candidate) => {
|
|
5329
|
-
const pathFromRoot =
|
|
5330
|
-
return pathFromRoot === "" || pathFromRoot !== ".." && !pathFromRoot.startsWith(`..${
|
|
3900
|
+
const pathFromRoot = relative3(root, candidate);
|
|
3901
|
+
return pathFromRoot === "" || pathFromRoot !== ".." && !pathFromRoot.startsWith(`..${sep3}`) && !isAbsolute6(pathFromRoot);
|
|
5331
3902
|
};
|
|
5332
|
-
var resolveAllowedRoot = (startCwd, allowedRoot) => allowedRoot === "~" ? homedir2() : allowedRoot.startsWith("~/") ?
|
|
3903
|
+
var resolveAllowedRoot = (startCwd, allowedRoot) => allowedRoot === "~" ? homedir2() : allowedRoot.startsWith("~/") ? resolve5(homedir2(), allowedRoot.slice(2)) : isAbsolute6(allowedRoot) ? allowedRoot : resolve5(startCwd, allowedRoot);
|
|
5333
3904
|
function resolveWorkspaceDirectory({
|
|
5334
3905
|
candidateCwd,
|
|
5335
3906
|
startCwd,
|
|
5336
3907
|
allowedRoots
|
|
5337
3908
|
}) {
|
|
5338
|
-
if (!candidateCwd || !
|
|
3909
|
+
if (!candidateCwd || !isAbsolute6(candidateCwd) || candidateCwd.includes("\x00")) {
|
|
5339
3910
|
throw new Error("workspace cwd must be a non-empty absolute path");
|
|
5340
3911
|
}
|
|
5341
|
-
if (!startCwd || !
|
|
3912
|
+
if (!startCwd || !isAbsolute6(startCwd) || startCwd.includes("\x00")) {
|
|
5342
3913
|
throw new Error("workflow start cwd must be a non-empty absolute path");
|
|
5343
3914
|
}
|
|
5344
3915
|
if (allowedRoots.length === 0) {
|
|
@@ -5346,7 +3917,7 @@ function resolveWorkspaceDirectory({
|
|
|
5346
3917
|
}
|
|
5347
3918
|
const canonicalStart = realpathSync(startCwd);
|
|
5348
3919
|
const canonicalRoots = allowedRoots.map((allowedRoot) => {
|
|
5349
|
-
if (!allowedRoot ||
|
|
3920
|
+
if (!allowedRoot || win322.parse(allowedRoot).root !== "" && !isAbsolute6(allowedRoot) || allowedRoot.includes("\x00")) {
|
|
5350
3921
|
throw new Error("workspace allowed roots must be non-empty relative, absolute, or home-relative paths");
|
|
5351
3922
|
}
|
|
5352
3923
|
return realpathSync(resolveAllowedRoot(canonicalStart, allowedRoot));
|
|
@@ -5400,10 +3971,10 @@ function flushUnwrittenSession(session) {
|
|
|
5400
3971
|
// src/harness/dependencies.ts
|
|
5401
3972
|
var MAX_DELEGATED_RESULT_BYTES = 1024 * 1024;
|
|
5402
3973
|
function createDelegationWorkspace() {
|
|
5403
|
-
const resultDirectory = mkdtempSync(
|
|
5404
|
-
const capabilityPath =
|
|
3974
|
+
const resultDirectory = mkdtempSync(join4(tmpdir(), "pi-workflows-step-"));
|
|
3975
|
+
const capabilityPath = join4(resultDirectory, "capability");
|
|
5405
3976
|
const capabilityToken = randomBytes(32).toString("hex");
|
|
5406
|
-
const resultPath =
|
|
3977
|
+
const resultPath = join4(resultDirectory, "result.json");
|
|
5407
3978
|
writeFileSync2(capabilityPath, capabilityToken, {
|
|
5408
3979
|
encoding: "utf8",
|
|
5409
3980
|
flag: "wx",
|
|
@@ -5417,15 +3988,15 @@ function createDelegationWorkspace() {
|
|
|
5417
3988
|
};
|
|
5418
3989
|
}
|
|
5419
3990
|
async function readDelegatedResult(active) {
|
|
5420
|
-
const expectedPath =
|
|
3991
|
+
const expectedPath = join4(active.resultDirectory, "result.json");
|
|
5421
3992
|
if (active.policy.resultPath !== expectedPath) {
|
|
5422
3993
|
throw new Error("delegated result path does not match its private directory");
|
|
5423
3994
|
}
|
|
5424
|
-
const inspected = await
|
|
3995
|
+
const inspected = await lstat2(expectedPath);
|
|
5425
3996
|
if (inspected.isSymbolicLink() || !inspected.isFile()) {
|
|
5426
3997
|
throw new Error("delegated result is not a regular file");
|
|
5427
3998
|
}
|
|
5428
|
-
const handle = await
|
|
3999
|
+
const handle = await open2(expectedPath, constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
5429
4000
|
try {
|
|
5430
4001
|
const beforeRead = await handle.stat();
|
|
5431
4002
|
if (!beforeRead.isFile() || beforeRead.size > MAX_DELEGATED_RESULT_BYTES) {
|
|
@@ -5448,17 +4019,14 @@ var DEFAULT_DEPENDENCIES3 = {
|
|
|
5448
4019
|
createDelegationWorkspace,
|
|
5449
4020
|
readDelegatedResult,
|
|
5450
4021
|
removeDelegationWorkspace: async (resultDirectory) => rm(resultDirectory, { recursive: true, force: true }),
|
|
5451
|
-
waitForDelay: async (delayMs) => new Promise((
|
|
4022
|
+
waitForDelay: async (delayMs) => new Promise((resolve6) => setTimeout(resolve6, delayMs)),
|
|
5452
4023
|
resolveWorkspaceDirectory,
|
|
5453
4024
|
loadCatalog: loadCatalog2,
|
|
5454
4025
|
requestPlannotatorReview,
|
|
5455
4026
|
requestPlannotatorReviewStatus,
|
|
5456
4027
|
requestPromptGateReview,
|
|
5457
|
-
readDelegationReplayAudit,
|
|
5458
|
-
readToolFailureDiagnostic,
|
|
5459
|
-
auditCompletedDelegationTranscript,
|
|
5460
4028
|
showWorkflowStatus,
|
|
5461
|
-
createSubagentClient: (
|
|
4029
|
+
createSubagentClient: () => createSubagentDelegationClient(),
|
|
5462
4030
|
createMainStepRuntime: (pi) => createMainStepRuntime({ pi }),
|
|
5463
4031
|
createMutationQueue: createSerialTaskQueue,
|
|
5464
4032
|
flushUnwrittenSession,
|
|
@@ -5485,7 +4053,8 @@ function workflowStatusSnapshot() {
|
|
|
5485
4053
|
kind: "subagent",
|
|
5486
4054
|
agent: this.activeDelegation.agent,
|
|
5487
4055
|
requestId: this.activeDelegation.requestId,
|
|
5488
|
-
progress: this.activeDelegation.progress ?? "starting"
|
|
4056
|
+
progress: this.activeDelegation.progress ?? "starting",
|
|
4057
|
+
activityLog: this.activeDelegation.activityLog ?? []
|
|
5489
4058
|
};
|
|
5490
4059
|
} else if (this.mainSteps.activeStepId) {
|
|
5491
4060
|
execution = { kind: "main" };
|
|
@@ -5499,6 +4068,13 @@ function workflowStatusSnapshot() {
|
|
|
5499
4068
|
}
|
|
5500
4069
|
function updateStatus() {
|
|
5501
4070
|
refreshStatusWhileRunning.call(this);
|
|
4071
|
+
const run = this.run;
|
|
4072
|
+
this.pi.events.emit("pi-workflows:state", {
|
|
4073
|
+
state: run?.status === "running" ? "working" : run?.status === "awaiting-gate" || run?.status === "paused" ? "blocked" : run?.status === "completed" ? "completed" : "interrupted",
|
|
4074
|
+
workflowId: run?.workflowId,
|
|
4075
|
+
stepId: run?.currentStepId,
|
|
4076
|
+
message: run?.status === "completed" ? `Workflow "${run.workflowId}" completed` : run?.status === "paused" ? run.pauseReason : undefined
|
|
4077
|
+
});
|
|
5502
4078
|
if (!this.latestContext)
|
|
5503
4079
|
return;
|
|
5504
4080
|
if (this.legacyProgressWidgetContext !== this.latestContext) {
|
|
@@ -6063,9 +4639,6 @@ function validateRunWorkflowSemantics(run, workflow) {
|
|
|
6063
4639
|
if (sameWorkflowDigest && currentStepChanged) {
|
|
6064
4640
|
return `current step "${run.currentStepId}" does not match the active workflow digest`;
|
|
6065
4641
|
}
|
|
6066
|
-
if (boundWorkspaceCwd && !currentStep2.subagent) {
|
|
6067
|
-
return `bound workflow current step "${run.currentStepId}" must use a subagent`;
|
|
6068
|
-
}
|
|
6069
4642
|
if (currentStepChanged)
|
|
6070
4643
|
return;
|
|
6071
4644
|
return validatePendingGate(run, currentStep2);
|
|
@@ -6875,69 +5448,83 @@ function buildMainWorkflowNotice(workflow, run, statusShortcutLabel = "Ctrl+Alt+
|
|
|
6875
5448
|
if (!step) {
|
|
6876
5449
|
throw new Error(`unknown workflow step "${run.currentStepId}"`);
|
|
6877
5450
|
}
|
|
6878
|
-
if (!step.subagent) {
|
|
6879
|
-
return [
|
|
6880
|
-
"# Active main-agent workflow",
|
|
6881
|
-
"",
|
|
6882
|
-
`Workflow "${workflow.definition.id}" is running step "${run.currentStepId}" (${step.title}) in this session.`,
|
|
6883
|
-
"Perform only the active workflow step with its allowed resources.",
|
|
6884
|
-
"Call `workflow_complete_step` exactly once when finished.",
|
|
6885
|
-
"Use `/workflow-pause` to halt and repair the workflow before resuming."
|
|
6886
|
-
].join(`
|
|
6887
|
-
`);
|
|
6888
|
-
}
|
|
6889
5451
|
return [
|
|
6890
|
-
"# Active
|
|
5452
|
+
"# Active workflow",
|
|
6891
5453
|
"",
|
|
6892
|
-
`Workflow "${workflow.definition.id}" is running step "${run.currentStepId}" (${step.title}) in
|
|
6893
|
-
|
|
6894
|
-
|
|
5454
|
+
`Workflow "${workflow.definition.id}" is running step "${run.currentStepId}" (${step.title}) in this session.`,
|
|
5455
|
+
...step.agent ? [`Apply the "${step.agent.name}" workflow role prompt.`] : [],
|
|
5456
|
+
"Perform only the active workflow step with its allowed resources.",
|
|
5457
|
+
"Call `workflow_complete_step` exactly once when finished.",
|
|
5458
|
+
`Use \`${statusShortcutLabel}\` or \`/workflow-status\` to inspect status, or \`/workflow-pause\` to halt and repair the workflow before resuming.`
|
|
6895
5459
|
].join(`
|
|
6896
5460
|
`);
|
|
6897
5461
|
}
|
|
6898
|
-
// src/
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
var
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
""
|
|
6930
|
-
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
5462
|
+
// src/agents/profile.ts
|
|
5463
|
+
import { existsSync as existsSync2, readFileSync } from "node:fs";
|
|
5464
|
+
import { join as join5 } from "node:path";
|
|
5465
|
+
import { fileURLToPath } from "node:url";
|
|
5466
|
+
import { parse } from "yaml";
|
|
5467
|
+
var THINKING_LEVELS = [
|
|
5468
|
+
"off",
|
|
5469
|
+
"minimal",
|
|
5470
|
+
"low",
|
|
5471
|
+
"medium",
|
|
5472
|
+
"high",
|
|
5473
|
+
"xhigh",
|
|
5474
|
+
"max"
|
|
5475
|
+
];
|
|
5476
|
+
var isRecord7 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5477
|
+
function parseAgentProfile(source, name) {
|
|
5478
|
+
if (!source.startsWith(`---
|
|
5479
|
+
`))
|
|
5480
|
+
return { prompt: source.trim() };
|
|
5481
|
+
const end = source.indexOf(`
|
|
5482
|
+
---
|
|
5483
|
+
`, 4);
|
|
5484
|
+
if (end === -1) {
|
|
5485
|
+
throw new Error(`workflow agent profile is invalid: ${name}`);
|
|
5486
|
+
}
|
|
5487
|
+
const metadata = parse(source.slice(4, end));
|
|
5488
|
+
if (!isRecord7(metadata)) {
|
|
5489
|
+
throw new Error(`workflow agent profile metadata must be an object: ${name}`);
|
|
5490
|
+
}
|
|
5491
|
+
const unknownKey = Object.keys(metadata).find((key) => key !== "model" && key !== "thinking");
|
|
5492
|
+
if (unknownKey) {
|
|
5493
|
+
throw new Error(`workflow agent profile has unknown metadata "${unknownKey}": ${name}`);
|
|
5494
|
+
}
|
|
5495
|
+
const model = metadata.model;
|
|
5496
|
+
if (model !== undefined && (typeof model !== "string" || !model.trim())) {
|
|
5497
|
+
throw new Error(`workflow agent profile model must be a non-empty string: ${name}`);
|
|
5498
|
+
}
|
|
5499
|
+
const thinking = metadata.thinking;
|
|
5500
|
+
if (thinking !== undefined && (typeof thinking !== "string" || !THINKING_LEVELS.includes(thinking))) {
|
|
5501
|
+
throw new Error(`workflow agent profile thinking must be one of ${THINKING_LEVELS.join(", ")}: ${name}`);
|
|
5502
|
+
}
|
|
5503
|
+
const prompt = source.slice(end + 5).trim();
|
|
5504
|
+
if (!prompt)
|
|
5505
|
+
throw new Error(`workflow agent profile prompt is empty: ${name}`);
|
|
5506
|
+
return {
|
|
5507
|
+
prompt,
|
|
5508
|
+
...typeof model === "string" ? { model: model.trim() } : {},
|
|
5509
|
+
...typeof thinking === "string" ? { thinking } : {}
|
|
5510
|
+
};
|
|
5511
|
+
}
|
|
5512
|
+
function loadAgentProfile(name, userDirectory = defaultUserWorkflowDirectory2()) {
|
|
5513
|
+
const userPath = join5(userDirectory, "agents", `${name}.md`);
|
|
5514
|
+
const bundledUrl = new URL(`../../examples/starter-kit/agents/${name}.md`, import.meta.url);
|
|
5515
|
+
const path = existsSync2(userPath) ? userPath : fileURLToPath(bundledUrl);
|
|
5516
|
+
try {
|
|
5517
|
+
return parseAgentProfile(readFileSync(path, "utf8"), name);
|
|
5518
|
+
} catch (error) {
|
|
5519
|
+
if (error instanceof Error && error.message.startsWith("workflow agent")) {
|
|
5520
|
+
throw error;
|
|
5521
|
+
}
|
|
5522
|
+
throw new Error(`workflow agent profile is unavailable: ${name}`, {
|
|
5523
|
+
cause: error
|
|
5524
|
+
});
|
|
5525
|
+
}
|
|
6940
5526
|
}
|
|
5527
|
+
|
|
6941
5528
|
// src/prompt/step-contract.ts
|
|
6942
5529
|
function createStepContract({
|
|
6943
5530
|
workflow,
|
|
@@ -7076,6 +5663,7 @@ function createTemplateValues({
|
|
|
7076
5663
|
}
|
|
7077
5664
|
|
|
7078
5665
|
// src/prompt/step-task.ts
|
|
5666
|
+
var rolePrompt = (name) => loadAgentProfile(name).prompt;
|
|
7079
5667
|
var resolveStep = (workflow, run) => {
|
|
7080
5668
|
const step = workflow.definition.steps[run.currentStepId];
|
|
7081
5669
|
if (!step) {
|
|
@@ -7137,13 +5725,11 @@ function buildStepTask(options) {
|
|
|
7137
5725
|
`Run: ${run.runId}`,
|
|
7138
5726
|
`Iteration: ${run.iteration ?? 1}`,
|
|
7139
5727
|
`Step: ${run.currentStepId} (${step.title})`,
|
|
7140
|
-
...
|
|
7141
|
-
`Agent profile: ${step.subagent?.agent ?? "generalist"}`,
|
|
7142
|
-
"Context: fresh workflow-step context; no parent or sibling transcript is inherited."
|
|
7143
|
-
] : [],
|
|
5728
|
+
...step.agent ? [`Agent profile: ${step.agent.name}`] : [],
|
|
7144
5729
|
"",
|
|
7145
5730
|
"## Step instructions",
|
|
7146
5731
|
"",
|
|
5732
|
+
...step.agent ? ["## Role prompt", "", rolePrompt(step.agent.name), ""] : [],
|
|
7147
5733
|
prompt,
|
|
7148
5734
|
"",
|
|
7149
5735
|
...isDelegated ? buildDelegatedHandoffSection(handoff) : [],
|
|
@@ -7293,23 +5879,324 @@ ${buildMainWorkflowNotice(workflow, this.run, this.statusShortcutLabel)}`
|
|
|
7293
5879
|
}
|
|
7294
5880
|
function createLifecycleActions() {
|
|
7295
5881
|
return {
|
|
7296
|
-
registerMultilineCommandInput,
|
|
7297
|
-
registerLifecycle,
|
|
7298
|
-
registerPolicy
|
|
5882
|
+
registerMultilineCommandInput,
|
|
5883
|
+
registerLifecycle,
|
|
5884
|
+
registerPolicy
|
|
5885
|
+
};
|
|
5886
|
+
}
|
|
5887
|
+
|
|
5888
|
+
// src/integrations/subagents/child-policy-envelope.ts
|
|
5889
|
+
import { basename as basename3, dirname as dirname4, resolve as resolve8 } from "node:path";
|
|
5890
|
+
|
|
5891
|
+
// src/integrations/subagents/child-policy-paths.ts
|
|
5892
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
5893
|
+
import { basename as basename2, dirname as dirname2, relative as relative4, resolve as resolve6 } from "node:path";
|
|
5894
|
+
var RESULT_FILE_NAME = "result.json";
|
|
5895
|
+
var CAPABILITY_FILE_NAME = "capability";
|
|
5896
|
+
var RESULT_DIRECTORY_PREFIX = "pi-workflows-step-";
|
|
5897
|
+
var DEFAULT_CHILD_POLICY_ENVIRONMENT = {
|
|
5898
|
+
temporaryDirectory: tmpdir2
|
|
5899
|
+
};
|
|
5900
|
+
var isSafeStepFilePath = ({
|
|
5901
|
+
path,
|
|
5902
|
+
expectedName,
|
|
5903
|
+
environment
|
|
5904
|
+
}) => {
|
|
5905
|
+
const temporaryRoot = resolve6(environment.temporaryDirectory());
|
|
5906
|
+
const candidate = resolve6(path);
|
|
5907
|
+
const relativePath = relative4(temporaryRoot, candidate);
|
|
5908
|
+
return relativePath !== "" && !relativePath.startsWith("..") && !relativePath.includes("\x00") && basename2(candidate) === expectedName && basename2(dirname2(candidate)).startsWith(RESULT_DIRECTORY_PREFIX);
|
|
5909
|
+
};
|
|
5910
|
+
var isSafeStepResultPath = (path, environment = DEFAULT_CHILD_POLICY_ENVIRONMENT) => isSafeStepFilePath({
|
|
5911
|
+
path,
|
|
5912
|
+
expectedName: RESULT_FILE_NAME,
|
|
5913
|
+
environment
|
|
5914
|
+
});
|
|
5915
|
+
var isSafeStepCapabilityPath = (path, environment = DEFAULT_CHILD_POLICY_ENVIRONMENT) => isSafeStepFilePath({
|
|
5916
|
+
path,
|
|
5917
|
+
expectedName: CAPABILITY_FILE_NAME,
|
|
5918
|
+
environment
|
|
5919
|
+
});
|
|
5920
|
+
|
|
5921
|
+
// src/integrations/subagents/child-policy-validation.ts
|
|
5922
|
+
import { dirname as dirname3, isAbsolute as isAbsolute8, resolve as resolve7 } from "node:path";
|
|
5923
|
+
|
|
5924
|
+
// src/integrations/subagents/child-policy-sections.ts
|
|
5925
|
+
import { isAbsolute as isAbsolute7, win32 as win323 } from "node:path";
|
|
5926
|
+
var isRecord8 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5927
|
+
var isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
5928
|
+
var hasOnlyKeys = (value, allowed) => Object.keys(value).every((key) => allowed.has(key));
|
|
5929
|
+
var isStepPermissions = (value) => {
|
|
5930
|
+
if (!isRecord8(value) || !isRecord8(value.bash))
|
|
5931
|
+
return false;
|
|
5932
|
+
const bash = value.bash;
|
|
5933
|
+
const bashRules = Array.isArray(bash.allow) ? bash.allow : undefined;
|
|
5934
|
+
const isValidMode = bash.mode === "deny" || bash.mode === "allow-list" || bash.mode === "unrestricted";
|
|
5935
|
+
const hasValidRules = bashRules !== undefined && bashRules.every((rule) => isRecord8(rule) && hasOnlyKeys(rule, new Set(["executable", "argsPrefix"])) && typeof rule.executable === "string" && isStringArray(rule.argsPrefix));
|
|
5936
|
+
return hasOnlyKeys(value, new Set(["tools", "mcp", "extensions", "skills", "bash"])) && hasOnlyKeys(bash, new Set(["mode", "allow"])) && isStringArray(value.tools) && isStringArray(value.mcp) && isStringArray(value.extensions) && isStringArray(value.skills) && isValidMode && hasValidRules && (bash.mode !== "allow-list" || bashRules.length > 0);
|
|
5937
|
+
};
|
|
5938
|
+
var parsePermissions2 = (value) => {
|
|
5939
|
+
if (!isStepPermissions(value.permissions)) {
|
|
5940
|
+
throw new Error("child policy permissions are invalid");
|
|
5941
|
+
}
|
|
5942
|
+
return { permissions: value.permissions };
|
|
5943
|
+
};
|
|
5944
|
+
var parseOutcomes = (value) => {
|
|
5945
|
+
const outcomes = value.outcomes;
|
|
5946
|
+
if (!isStringArray(outcomes) || outcomes.length === 0 || new Set(outcomes).size !== outcomes.length) {
|
|
5947
|
+
throw new Error("child policy outcomes are invalid");
|
|
5948
|
+
}
|
|
5949
|
+
const pauseOutcomes = value.pauseOutcomes;
|
|
5950
|
+
if (!isStringArray(pauseOutcomes) || new Set(pauseOutcomes).size !== pauseOutcomes.length || pauseOutcomes.some((outcome) => !outcomes.includes(outcome))) {
|
|
5951
|
+
throw new Error("child policy pause outcomes are invalid");
|
|
5952
|
+
}
|
|
5953
|
+
const summaryMaxChars = value.summaryMaxChars;
|
|
5954
|
+
if (typeof summaryMaxChars !== "number" || !Number.isInteger(summaryMaxChars) || summaryMaxChars < 100 || summaryMaxChars > 50000) {
|
|
5955
|
+
throw new Error("child policy summaryMaxChars is invalid");
|
|
5956
|
+
}
|
|
5957
|
+
const gateSubmitOutcome = value.gateSubmitOutcome;
|
|
5958
|
+
if (gateSubmitOutcome !== undefined && (typeof gateSubmitOutcome !== "string" || !outcomes.includes(gateSubmitOutcome))) {
|
|
5959
|
+
throw new Error("child policy gate outcome is invalid");
|
|
5960
|
+
}
|
|
5961
|
+
return {
|
|
5962
|
+
outcomes,
|
|
5963
|
+
pauseOutcomes,
|
|
5964
|
+
summaryMaxChars,
|
|
5965
|
+
...gateSubmitOutcome === undefined ? {} : { gateSubmitOutcome }
|
|
5966
|
+
};
|
|
5967
|
+
};
|
|
5968
|
+
var parseWorkspace2 = (value, outcomes) => {
|
|
5969
|
+
if (value.workspace === undefined)
|
|
5970
|
+
return {};
|
|
5971
|
+
if (!isRecord8(value.workspace) || !hasOnlyKeys(value.workspace, new Set(["bindOn", "allowedRoots"]))) {
|
|
5972
|
+
throw new Error("child policy workspace is invalid");
|
|
5973
|
+
}
|
|
5974
|
+
const bindOn = value.workspace.bindOn;
|
|
5975
|
+
const allowedRoots = value.workspace.allowedRoots;
|
|
5976
|
+
if (!isStringArray(bindOn) || bindOn.length === 0 || new Set(bindOn).size !== bindOn.length || bindOn.some((outcome) => !outcomes.includes(outcome))) {
|
|
5977
|
+
throw new Error("child policy workspace bindOn outcomes are invalid");
|
|
5978
|
+
}
|
|
5979
|
+
if (!isStringArray(allowedRoots) || allowedRoots.length === 0 || allowedRoots.length > MAX_WORKSPACE_ALLOWED_ROOTS || new Set(allowedRoots).size !== allowedRoots.length || allowedRoots.some((root) => !root.trim() || root !== root.trim() || root.length > MAX_WORKSPACE_PATH_CHARS || root.includes("\x00") || win323.parse(root).root !== "" && !isAbsolute7(root))) {
|
|
5980
|
+
throw new Error("child policy workspace allowed roots are invalid");
|
|
5981
|
+
}
|
|
5982
|
+
return { workspace: { bindOn, allowedRoots } };
|
|
5983
|
+
};
|
|
5984
|
+
var parseChildPolicySections = (value) => {
|
|
5985
|
+
const outcomeSections = parseOutcomes(value);
|
|
5986
|
+
return {
|
|
5987
|
+
...parsePermissions2(value),
|
|
5988
|
+
...outcomeSections,
|
|
5989
|
+
...parseWorkspace2(value, outcomeSections.outcomes)
|
|
5990
|
+
};
|
|
5991
|
+
};
|
|
5992
|
+
|
|
5993
|
+
// src/integrations/subagents/child-policy-validation.ts
|
|
5994
|
+
var POLICY_DIGEST_PATTERN = /^[a-f0-9]{64}$/;
|
|
5995
|
+
var CAPABILITY_TOKEN_PATTERN = /^[a-f0-9]{64}$/;
|
|
5996
|
+
var POLICY_KEYS = new Set([
|
|
5997
|
+
"version",
|
|
5998
|
+
"requestId",
|
|
5999
|
+
"agent",
|
|
6000
|
+
"workflowId",
|
|
6001
|
+
"runId",
|
|
6002
|
+
"stepId",
|
|
6003
|
+
"stepTitle",
|
|
6004
|
+
"cwd",
|
|
6005
|
+
"policyDigest",
|
|
6006
|
+
"capabilityPath",
|
|
6007
|
+
"capabilityToken",
|
|
6008
|
+
"resultPath",
|
|
6009
|
+
"permissions",
|
|
6010
|
+
"outcomes",
|
|
6011
|
+
"pauseOutcomes",
|
|
6012
|
+
"summaryMaxChars",
|
|
6013
|
+
"gateSubmitOutcome",
|
|
6014
|
+
"workspace"
|
|
6015
|
+
]);
|
|
6016
|
+
var isRecord9 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6017
|
+
var requiredString = (value, field) => {
|
|
6018
|
+
const candidate = value[field];
|
|
6019
|
+
if (typeof candidate !== "string" || !candidate) {
|
|
6020
|
+
throw new Error(`child policy ${field} must be a non-empty string`);
|
|
6021
|
+
}
|
|
6022
|
+
return candidate;
|
|
6023
|
+
};
|
|
6024
|
+
var isAgentProfileName = (name) => Boolean(name && AGENT_PROFILE_NAME_PATTERN.test(name));
|
|
6025
|
+
var rejectUnknownProperties = (value) => {
|
|
6026
|
+
const unknownKey = Object.keys(value).find((key) => !POLICY_KEYS.has(key));
|
|
6027
|
+
if (unknownKey) {
|
|
6028
|
+
throw new Error(`child policy has unknown property "${unknownKey}"`);
|
|
6029
|
+
}
|
|
6030
|
+
};
|
|
6031
|
+
var parseIdentityAndPaths = (value, environment) => {
|
|
6032
|
+
const requestId = requiredString(value, "requestId");
|
|
6033
|
+
const agent = requiredString(value, "agent");
|
|
6034
|
+
const workflowId = requiredString(value, "workflowId");
|
|
6035
|
+
const runId = requiredString(value, "runId");
|
|
6036
|
+
const stepId = requiredString(value, "stepId");
|
|
6037
|
+
const stepTitle3 = requiredString(value, "stepTitle");
|
|
6038
|
+
const cwd = requiredString(value, "cwd");
|
|
6039
|
+
const policyDigest = requiredString(value, "policyDigest");
|
|
6040
|
+
const capabilityPath = requiredString(value, "capabilityPath");
|
|
6041
|
+
const capabilityToken = requiredString(value, "capabilityToken");
|
|
6042
|
+
const resultPath = requiredString(value, "resultPath");
|
|
6043
|
+
if (value.version !== 1)
|
|
6044
|
+
throw new Error("unsupported child policy version");
|
|
6045
|
+
if (!isAbsolute8(cwd)) {
|
|
6046
|
+
throw new Error("child policy cwd must be an absolute path");
|
|
6047
|
+
}
|
|
6048
|
+
if (!POLICY_DIGEST_PATTERN.test(policyDigest)) {
|
|
6049
|
+
throw new Error("child policy digest is invalid");
|
|
6050
|
+
}
|
|
6051
|
+
if (!isAgentProfileName(agent)) {
|
|
6052
|
+
throw new Error("child policy agent is not a valid agent profile name");
|
|
6053
|
+
}
|
|
6054
|
+
if (!CAPABILITY_TOKEN_PATTERN.test(capabilityToken)) {
|
|
6055
|
+
throw new Error("child policy capability token is invalid");
|
|
6056
|
+
}
|
|
6057
|
+
if (!isSafeStepCapabilityPath(capabilityPath, environment)) {
|
|
6058
|
+
throw new Error("child policy capability path is outside its temporary directory");
|
|
6059
|
+
}
|
|
6060
|
+
if (!isSafeStepResultPath(resultPath, environment)) {
|
|
6061
|
+
throw new Error("child policy result path is outside its temporary directory");
|
|
6062
|
+
}
|
|
6063
|
+
if (dirname3(resolve7(capabilityPath)) !== dirname3(resolve7(resultPath))) {
|
|
6064
|
+
throw new Error("child policy files must share one temporary directory");
|
|
6065
|
+
}
|
|
6066
|
+
return {
|
|
6067
|
+
version: 1,
|
|
6068
|
+
requestId,
|
|
6069
|
+
agent,
|
|
6070
|
+
workflowId,
|
|
6071
|
+
runId,
|
|
6072
|
+
stepId,
|
|
6073
|
+
stepTitle: stepTitle3,
|
|
6074
|
+
cwd,
|
|
6075
|
+
policyDigest,
|
|
6076
|
+
capabilityPath,
|
|
6077
|
+
capabilityToken,
|
|
6078
|
+
resultPath
|
|
7299
6079
|
};
|
|
7300
|
-
}
|
|
6080
|
+
};
|
|
6081
|
+
var parseChildPolicy = (value, environment = DEFAULT_CHILD_POLICY_ENVIRONMENT) => {
|
|
6082
|
+
if (!isRecord9(value))
|
|
6083
|
+
throw new Error("child policy must be an object");
|
|
6084
|
+
rejectUnknownProperties(value);
|
|
6085
|
+
return {
|
|
6086
|
+
...parseIdentityAndPaths(value, environment),
|
|
6087
|
+
...parseChildPolicySections(value)
|
|
6088
|
+
};
|
|
6089
|
+
};
|
|
7301
6090
|
|
|
6091
|
+
// src/integrations/subagents/child-policy-envelope.ts
|
|
6092
|
+
var CHILD_POLICY_OPEN = "<pi-workflows-policy-v1>";
|
|
6093
|
+
var CHILD_POLICY_CLOSE = "</pi-workflows-policy-v1>";
|
|
6094
|
+
var UPSTREAM_TASK_PREFIX = "Task: ";
|
|
6095
|
+
var UPSTREAM_TASK_FILE_OPEN = '<file name="';
|
|
6096
|
+
var UPSTREAM_TASK_FILE_HEADER_CLOSE = `">
|
|
6097
|
+
`;
|
|
6098
|
+
var UPSTREAM_TASK_FILE_CLOSE = `
|
|
6099
|
+
</file>
|
|
6100
|
+
`;
|
|
6101
|
+
var UPSTREAM_TASK_DIRECTORY_PREFIX = "pi-subagent-";
|
|
6102
|
+
var encodeChildPolicy = (policy) => {
|
|
6103
|
+
const encoded = Buffer.from(JSON.stringify(policy), "utf8").toString("base64url");
|
|
6104
|
+
return `${CHILD_POLICY_OPEN}${encoded}${CHILD_POLICY_CLOSE}`;
|
|
6105
|
+
};
|
|
6106
|
+
var unwrapTaskFile = ({
|
|
6107
|
+
text,
|
|
6108
|
+
environment
|
|
6109
|
+
}) => {
|
|
6110
|
+
if (!text.startsWith(UPSTREAM_TASK_FILE_OPEN) || !text.endsWith(UPSTREAM_TASK_FILE_CLOSE)) {
|
|
6111
|
+
return;
|
|
6112
|
+
}
|
|
6113
|
+
const pathStart = UPSTREAM_TASK_FILE_OPEN.length;
|
|
6114
|
+
const headerEnd = text.indexOf(UPSTREAM_TASK_FILE_HEADER_CLOSE, pathStart);
|
|
6115
|
+
if (headerEnd === -1)
|
|
6116
|
+
return;
|
|
6117
|
+
const taskFilePath = text.slice(pathStart, headerEnd);
|
|
6118
|
+
const taskDirectory = dirname4(resolve8(taskFilePath));
|
|
6119
|
+
const isExpectedTaskFile = basename3(taskFilePath) === "task.md" && basename3(taskDirectory).startsWith(UPSTREAM_TASK_DIRECTORY_PREFIX) && dirname4(taskDirectory) === resolve8(environment.temporaryDirectory());
|
|
6120
|
+
if (!isExpectedTaskFile)
|
|
6121
|
+
return;
|
|
6122
|
+
const bodyStart = headerEnd + UPSTREAM_TASK_FILE_HEADER_CLOSE.length;
|
|
6123
|
+
const body = text.slice(bodyStart, -UPSTREAM_TASK_FILE_CLOSE.length);
|
|
6124
|
+
if (!body.startsWith(`${UPSTREAM_TASK_PREFIX}${CHILD_POLICY_OPEN}`)) {
|
|
6125
|
+
return;
|
|
6126
|
+
}
|
|
6127
|
+
return body.slice(UPSTREAM_TASK_PREFIX.length);
|
|
6128
|
+
};
|
|
6129
|
+
var unwrapUpstreamTask = ({
|
|
6130
|
+
text,
|
|
6131
|
+
environment
|
|
6132
|
+
}) => {
|
|
6133
|
+
if (text.startsWith(CHILD_POLICY_OPEN))
|
|
6134
|
+
return text;
|
|
6135
|
+
if (text.startsWith(`${UPSTREAM_TASK_PREFIX}${CHILD_POLICY_OPEN}`)) {
|
|
6136
|
+
return text.slice(UPSTREAM_TASK_PREFIX.length);
|
|
6137
|
+
}
|
|
6138
|
+
return unwrapTaskFile({ text, environment });
|
|
6139
|
+
};
|
|
6140
|
+
var decodePolicy = (encoded) => {
|
|
6141
|
+
try {
|
|
6142
|
+
return JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
6143
|
+
} catch {
|
|
6144
|
+
throw new Error("delegated task child policy cannot be decoded");
|
|
6145
|
+
}
|
|
6146
|
+
};
|
|
6147
|
+
var extractChildPolicy = (text, environment = DEFAULT_CHILD_POLICY_ENVIRONMENT) => {
|
|
6148
|
+
const taskWithPolicy = unwrapUpstreamTask({ text, environment });
|
|
6149
|
+
if (taskWithPolicy === undefined)
|
|
6150
|
+
return;
|
|
6151
|
+
const payloadStart = CHILD_POLICY_OPEN.length;
|
|
6152
|
+
const payloadEnd = taskWithPolicy.indexOf(CHILD_POLICY_CLOSE, payloadStart);
|
|
6153
|
+
const hasNestedEnvelope = taskWithPolicy.indexOf(CHILD_POLICY_OPEN, payloadStart) !== -1;
|
|
6154
|
+
if (payloadEnd === -1 || hasNestedEnvelope) {
|
|
6155
|
+
throw new Error("delegated task contains an invalid child policy envelope");
|
|
6156
|
+
}
|
|
6157
|
+
const encoded = taskWithPolicy.slice(payloadStart, payloadEnd);
|
|
6158
|
+
const task = taskWithPolicy.slice(payloadEnd + CHILD_POLICY_CLOSE.length).trim();
|
|
6159
|
+
if (!task)
|
|
6160
|
+
throw new Error("delegated task is empty after policy extraction");
|
|
6161
|
+
return {
|
|
6162
|
+
policy: parseChildPolicy(decodePolicy(encoded), environment),
|
|
6163
|
+
task
|
|
6164
|
+
};
|
|
6165
|
+
};
|
|
6166
|
+
// src/integrations/subagents/delegated-result.ts
|
|
6167
|
+
var parseDelegatedStepResult = (value, policy) => {
|
|
6168
|
+
try {
|
|
6169
|
+
return parseWorkflowStepResult(value, {
|
|
6170
|
+
policyDigest: policy.policyDigest,
|
|
6171
|
+
outcomes: [...policy.outcomes],
|
|
6172
|
+
summaryMaxChars: policy.summaryMaxChars,
|
|
6173
|
+
...policy.gateSubmitOutcome ? { gateSubmitOutcome: policy.gateSubmitOutcome } : {},
|
|
6174
|
+
...policy.workspace ? { workspace: policy.workspace } : {}
|
|
6175
|
+
});
|
|
6176
|
+
} catch (error) {
|
|
6177
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6178
|
+
throw new Error(message.replaceAll("workflow step", "delegated step"), {
|
|
6179
|
+
cause: error
|
|
6180
|
+
});
|
|
6181
|
+
}
|
|
6182
|
+
};
|
|
7302
6183
|
// src/harness/delegation-plan.ts
|
|
7303
|
-
function delegationTranscriptBinding(requestId, policyDigest) {
|
|
7304
|
-
return `<pi-workflows-delegation-binding-v1>${requestId}:${policyDigest}</pi-workflows-delegation-binding-v1>`;
|
|
7305
|
-
}
|
|
7306
6184
|
function createDelegationPlan(input, dependencies) {
|
|
7307
|
-
const { workflow, run, step, latestContext
|
|
7308
|
-
const
|
|
7309
|
-
if (!
|
|
6185
|
+
const { workflow, run, step, latestContext } = input;
|
|
6186
|
+
const agent = step.agent?.name;
|
|
6187
|
+
if (!agent) {
|
|
6188
|
+
return {
|
|
6189
|
+
kind: "invalid",
|
|
6190
|
+
reason: `Step "${run.currentStepId}" has no agent profile`
|
|
6191
|
+
};
|
|
6192
|
+
}
|
|
6193
|
+
let agentProfile;
|
|
6194
|
+
try {
|
|
6195
|
+
agentProfile = loadAgentProfile(agent);
|
|
6196
|
+
} catch (error) {
|
|
7310
6197
|
return {
|
|
7311
6198
|
kind: "invalid",
|
|
7312
|
-
reason:
|
|
6199
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
7313
6200
|
};
|
|
7314
6201
|
}
|
|
7315
6202
|
if (!run.cwd) {
|
|
@@ -7391,7 +6278,7 @@ function createDelegationPlan(input, dependencies) {
|
|
|
7391
6278
|
const policyDigest = digest({
|
|
7392
6279
|
version: 1,
|
|
7393
6280
|
requestId,
|
|
7394
|
-
agent
|
|
6281
|
+
agent,
|
|
7395
6282
|
runId: run.runId,
|
|
7396
6283
|
stepId: run.currentStepId,
|
|
7397
6284
|
stepDigest: run.currentStepDigest,
|
|
@@ -7405,7 +6292,7 @@ function createDelegationPlan(input, dependencies) {
|
|
|
7405
6292
|
const policy = {
|
|
7406
6293
|
version: 1,
|
|
7407
6294
|
requestId,
|
|
7408
|
-
agent
|
|
6295
|
+
agent,
|
|
7409
6296
|
workflowId: workflow.definition.id,
|
|
7410
6297
|
runId: run.runId,
|
|
7411
6298
|
stepId: run.currentStepId,
|
|
@@ -7422,16 +6309,7 @@ function createDelegationPlan(input, dependencies) {
|
|
|
7422
6309
|
...step.gate ? { gateSubmitOutcome: step.gate.submitOutcome } : {},
|
|
7423
6310
|
...step.workspace ? { workspace: structuredClone(step.workspace) } : {}
|
|
7424
6311
|
};
|
|
7425
|
-
const
|
|
7426
|
-
const transcriptTask = [
|
|
7427
|
-
buildDelegatedStepTask(workflow, run, ""),
|
|
7428
|
-
delegationTranscriptBinding(requestId, policyDigest),
|
|
7429
|
-
...recovery ? [
|
|
7430
|
-
automaticRecoveryTask(recovery.failures.map(({ reason }) => reason), recovery.attempt, MAX_DELEGATION_RECOVERY_ATTEMPTS)
|
|
7431
|
-
] : []
|
|
7432
|
-
].join(`
|
|
7433
|
-
|
|
7434
|
-
`);
|
|
6312
|
+
const task = buildDelegatedStepTask(workflow, run, "");
|
|
7435
6313
|
const active = {
|
|
7436
6314
|
requestId,
|
|
7437
6315
|
runId: run.runId,
|
|
@@ -7440,39 +6318,20 @@ function createDelegationPlan(input, dependencies) {
|
|
|
7440
6318
|
sessionEpoch: input.sessionEpoch,
|
|
7441
6319
|
resultDirectory: workspace.resultDirectory,
|
|
7442
6320
|
policy,
|
|
7443
|
-
transcriptTask,
|
|
7444
|
-
agent
|
|
7445
|
-
...trustedSessionRoot ? { trustedSessionRoot } : {},
|
|
7446
|
-
broadRecoveryAuthorized: subagent.retryToolFailures,
|
|
7447
|
-
recoveryAttemptCount: recovery?.attempt ?? 0,
|
|
7448
|
-
recoveryFailures: recovery?.failures ?? []
|
|
6321
|
+
transcriptTask: task,
|
|
6322
|
+
agent
|
|
7449
6323
|
};
|
|
7450
6324
|
const request = {
|
|
7451
6325
|
version: 1,
|
|
7452
6326
|
requestId,
|
|
7453
|
-
agent
|
|
6327
|
+
agent,
|
|
7454
6328
|
task: `${encodeChildPolicy(policy)}
|
|
7455
6329
|
|
|
7456
|
-
${
|
|
7457
|
-
context: "fresh",
|
|
6330
|
+
${task}`,
|
|
7458
6331
|
cwd: delegationCwd,
|
|
7459
|
-
timeoutMs:
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
outputSchema: WORKFLOW_COMPLETION_PARAMETERS,
|
|
7463
|
-
agentContract: { version: 1 },
|
|
7464
|
-
artifacts: subagent.artifacts,
|
|
7465
|
-
...subagent.model ? { model: subagent.model } : {},
|
|
7466
|
-
...subagent.turnBudget ? { turnBudget: structuredClone(subagent.turnBudget) } : {},
|
|
7467
|
-
...subagent.toolBudget ? {
|
|
7468
|
-
toolBudget: {
|
|
7469
|
-
hard: subagent.toolBudget.hard,
|
|
7470
|
-
...subagent.toolBudget.soft === undefined ? {} : { soft: subagent.toolBudget.soft },
|
|
7471
|
-
...subagent.toolBudget.block === undefined ? {} : {
|
|
7472
|
-
block: subagent.toolBudget.block === "*" ? "*" : [...subagent.toolBudget.block]
|
|
7473
|
-
}
|
|
7474
|
-
}
|
|
7475
|
-
} : {}
|
|
6332
|
+
timeoutMs: 900000,
|
|
6333
|
+
...agentProfile.model ? { model: agentProfile.model } : {},
|
|
6334
|
+
...agentProfile.thinking ? { thinking: agentProfile.thinking } : {}
|
|
7476
6335
|
};
|
|
7477
6336
|
return { kind: "ready", active, request };
|
|
7478
6337
|
}
|
|
@@ -7513,7 +6372,7 @@ function resolveStepEffects(run, step, result, dependencies) {
|
|
|
7513
6372
|
}
|
|
7514
6373
|
|
|
7515
6374
|
// src/harness/step-execution-actions.ts
|
|
7516
|
-
function launchCurrentStep(workflow
|
|
6375
|
+
function launchCurrentStep(workflow) {
|
|
7517
6376
|
const run = this.run;
|
|
7518
6377
|
if (!run || run.status !== "running" || this.activeDelegation || this.mainSteps.activeStepId) {
|
|
7519
6378
|
return;
|
|
@@ -7523,17 +6382,12 @@ function launchCurrentStep(workflow, recovery) {
|
|
|
7523
6382
|
this.pauseForExecutionFailure("Workflow", `Step "${run.currentStepId}" is missing from the workflow`);
|
|
7524
6383
|
return;
|
|
7525
6384
|
}
|
|
7526
|
-
if (!step.subagent) {
|
|
7527
|
-
this.launchMainStep(workflow, run, step);
|
|
7528
|
-
return;
|
|
7529
|
-
}
|
|
7530
6385
|
const plan = createDelegationPlan({
|
|
7531
6386
|
workflow,
|
|
7532
6387
|
run,
|
|
7533
6388
|
step,
|
|
7534
6389
|
sessionEpoch: this.sessionEpoch,
|
|
7535
|
-
latestContext: this.latestContext
|
|
7536
|
-
recovery
|
|
6390
|
+
latestContext: this.latestContext
|
|
7537
6391
|
}, this.dependencies);
|
|
7538
6392
|
if (plan.kind === "invalid") {
|
|
7539
6393
|
this.pauseForExecutionFailure("Subagent step", plan.reason);
|
|
@@ -7620,6 +6474,7 @@ function launchMainStep(workflow, run, step) {
|
|
|
7620
6474
|
outcomes: allowedOutcomes(workflow, run),
|
|
7621
6475
|
summaryMaxChars: workflow.definition.summaryMaxChars,
|
|
7622
6476
|
...step.gate ? { gateSubmitOutcome: step.gate.submitOutcome } : {},
|
|
6477
|
+
...step.workspace ? { workspace: structuredClone(step.workspace) } : {},
|
|
7623
6478
|
onTrace: (lines, context) => this.queueMainStepLog(identity, lines, context),
|
|
7624
6479
|
onSettled: (result, context) => this.queueMainStepResult(identity, result, context)
|
|
7625
6480
|
};
|
|
@@ -7716,11 +6571,20 @@ function handleDelegationUpdate(active, update) {
|
|
|
7716
6571
|
if (this.activeDelegation !== active)
|
|
7717
6572
|
return;
|
|
7718
6573
|
const progress = [
|
|
6574
|
+
update.activity,
|
|
7719
6575
|
update.currentTool ? `tool ${update.currentTool}` : undefined,
|
|
7720
6576
|
update.toolCount !== undefined ? `${update.toolCount} calls` : undefined,
|
|
7721
6577
|
update.tokens !== undefined ? `${update.tokens} tokens` : undefined
|
|
7722
6578
|
].filter((part) => part !== undefined);
|
|
7723
6579
|
active.progress = progress.join(", ") || "running";
|
|
6580
|
+
if (update.detail) {
|
|
6581
|
+
const previous = active.activityLog ?? [];
|
|
6582
|
+
const replacesPreviousResponse = update.detail.startsWith("response: ") && previous.at(-1)?.startsWith("response: ");
|
|
6583
|
+
active.activityLog = [
|
|
6584
|
+
...replacesPreviousResponse ? previous.slice(0, -1) : previous,
|
|
6585
|
+
update.detail
|
|
6586
|
+
].slice(-8);
|
|
6587
|
+
}
|
|
7724
6588
|
this.updateStatus();
|
|
7725
6589
|
}
|
|
7726
6590
|
function queueDelegationResponse(active, response) {
|
|
@@ -7751,49 +6615,19 @@ async function finishDelegation(active, response) {
|
|
|
7751
6615
|
return;
|
|
7752
6616
|
}
|
|
7753
6617
|
this.activeDelegation = undefined;
|
|
7754
|
-
let terminalFailure;
|
|
7755
6618
|
let cleanupAttempted = false;
|
|
7756
6619
|
try {
|
|
7757
6620
|
if (!this.isSessionActive || this.sessionEpoch !== active.sessionEpoch || !this.run || this.run.status !== "running" || this.run.runId !== active.runId || this.run.currentStepId !== active.stepId || this.run.currentStepDigest !== active.stepDigest) {
|
|
7758
6621
|
return;
|
|
7759
6622
|
}
|
|
7760
6623
|
const terminalAt = this.dependencies.now();
|
|
7761
|
-
if (response.requestId === active.requestId && (response.agent === undefined || response.agent === active.agent) && response.sessionFile && active.trustedSessionRoot && typeof response.runId === "string" && response.runId && response.childIndex === 0) {
|
|
7762
|
-
try {
|
|
7763
|
-
this.run = attachSubagentTranscript(this.run, active.requestId, {
|
|
7764
|
-
trustedRoot: active.trustedSessionRoot,
|
|
7765
|
-
sessionFile: response.sessionFile,
|
|
7766
|
-
runId: response.runId,
|
|
7767
|
-
childIndex: response.childIndex
|
|
7768
|
-
}, terminalAt);
|
|
7769
|
-
} catch {}
|
|
7770
|
-
}
|
|
7771
6624
|
const workflow = this.catalog.workflows.get(this.run.workflowId);
|
|
7772
6625
|
const step = workflow?.definition.steps[this.run.currentStepId];
|
|
7773
6626
|
if (!workflow || !step) {
|
|
7774
6627
|
throw new Error("Active workflow configuration is unavailable");
|
|
7775
6628
|
}
|
|
7776
|
-
if (response.status
|
|
7777
|
-
|
|
7778
|
-
if (!transcriptAudit.verified) {
|
|
7779
|
-
throw new Error(`Subagent "${active.agent}" completed without a verifiable terminal transcript: ${transcriptAudit.reason}`);
|
|
7780
|
-
}
|
|
7781
|
-
if (transcriptAudit.warning) {
|
|
7782
|
-
throw new Error(`Subagent "${active.agent}" completed with an unresolved post-completion watchdog warning: ${transcriptAudit.warning}`);
|
|
7783
|
-
}
|
|
7784
|
-
}
|
|
7785
|
-
let recoveredTerminalFailure;
|
|
7786
|
-
if (response.status !== "completed" || this.delegationFailures.hasContradictoryCompletion(response)) {
|
|
7787
|
-
const failure = await this.delegationFailures.describeDelegationFailure(active, response);
|
|
7788
|
-
terminalFailure = failure;
|
|
7789
|
-
if (response.status !== "failed" || failure.diagnostic?.completionAfterFailure !== true) {
|
|
7790
|
-
throw new Error(failure.reason);
|
|
7791
|
-
}
|
|
7792
|
-
const projectionError = this.delegationFailures.recoveredProjectionError(active, response, failure.diagnostic);
|
|
7793
|
-
if (projectionError) {
|
|
7794
|
-
throw new Error(this.delegationFailures.rejectedRecoveryReason(failure, projectionError));
|
|
7795
|
-
}
|
|
7796
|
-
recoveredTerminalFailure = failure;
|
|
6629
|
+
if (response.status !== "completed") {
|
|
6630
|
+
throw new Error(`Workflow worker "${active.agent}" ${response.status.replaceAll("_", " ")}${response.error ? `: ${response.error}` : ""}`);
|
|
7797
6631
|
}
|
|
7798
6632
|
const requiredSkillWarning = step.requires.skills.length > 0 ? response.warnings?.find((warning) => /skill/i.test(warning)) : undefined;
|
|
7799
6633
|
if (requiredSkillWarning) {
|
|
@@ -7803,32 +6637,14 @@ async function finishDelegation(active, response) {
|
|
|
7803
6637
|
try {
|
|
7804
6638
|
serializedResult = await this.dependencies.readDelegatedResult(active);
|
|
7805
6639
|
} catch (error) {
|
|
7806
|
-
if (recoveredTerminalFailure) {
|
|
7807
|
-
throw new Error(this.delegationFailures.rejectedRecoveryReason(recoveredTerminalFailure, error), { cause: error });
|
|
7808
|
-
}
|
|
7809
6640
|
if (hasErrorCode(error, "ENOENT")) {
|
|
7810
6641
|
throw new Error(`Subagent "${active.agent}" completed without producing the required correlated structured_output result`, { cause: error });
|
|
7811
6642
|
}
|
|
7812
6643
|
throw error;
|
|
7813
6644
|
}
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
const rawResult = JSON.parse(serializedResult);
|
|
7817
|
-
result = parseDelegatedStepResult(rawResult, active.policy);
|
|
7818
|
-
} catch (error) {
|
|
7819
|
-
if (recoveredTerminalFailure) {
|
|
7820
|
-
throw new Error(this.delegationFailures.rejectedRecoveryReason(recoveredTerminalFailure, error), { cause: error });
|
|
7821
|
-
}
|
|
7822
|
-
throw error;
|
|
7823
|
-
}
|
|
7824
|
-
if (recoveredTerminalFailure?.diagnostic && !this.delegationFailures.completionMatchesResult(recoveredTerminalFailure.diagnostic, result, active.policy)) {
|
|
7825
|
-
throw new Error(this.delegationFailures.rejectedRecoveryReason(recoveredTerminalFailure, "structured_output transcript value does not match the correlated result"));
|
|
7826
|
-
}
|
|
6645
|
+
const rawResult = JSON.parse(serializedResult);
|
|
6646
|
+
const result = parseDelegatedStepResult(rawResult, active.policy);
|
|
7827
6647
|
const acceptedAt = terminalAt;
|
|
7828
|
-
if (recoveredTerminalFailure) {
|
|
7829
|
-
const isFalsePositive = recoveredTerminalFailure.diagnostic?.correlation === "successful-output-before-completion";
|
|
7830
|
-
this.latestContext?.ui.notify(isFalsePositive ? `Accepted "${active.stepId}" because the trusted child transcript proved the terminal tool error was a false positive and produced a matching structured result` : `Accepted "${active.stepId}" because the child resolved an earlier tool failure and produced a valid structured result`, "warning");
|
|
7831
|
-
}
|
|
7832
6648
|
if (step.gate?.submitOutcome === result.outcome) {
|
|
7833
6649
|
this.run = recordCurrentStepResult(this.run, result, acceptedAt);
|
|
7834
6650
|
await this.submitGate(workflow, this.run, result.outcome, result.summary, result.artifact ?? "");
|
|
@@ -7846,10 +6662,7 @@ async function finishDelegation(active, response) {
|
|
|
7846
6662
|
const reason = error instanceof Error ? error.message : String(error);
|
|
7847
6663
|
cleanupAttempted = true;
|
|
7848
6664
|
await this.cleanupDelegation(active);
|
|
7849
|
-
|
|
7850
|
-
const failureSummary = terminalFailure?.error ? `Subagent ${terminalFailure.status.replaceAll("_", " ")}: ${terminalFailure.error}` : terminalFailure ? `Subagent ${terminalFailure.status.replaceAll("_", " ")}` : reason;
|
|
7851
|
-
this.pauseForDelegationFailure(reason, failureSummary);
|
|
7852
|
-
}
|
|
6665
|
+
this.pauseForDelegationFailure(reason);
|
|
7853
6666
|
} finally {
|
|
7854
6667
|
try {
|
|
7855
6668
|
if (!cleanupAttempted)
|
|
@@ -7870,6 +6683,7 @@ function createDelegationResponseActions() {
|
|
|
7870
6683
|
}
|
|
7871
6684
|
|
|
7872
6685
|
// src/harness/delegation-control-actions.ts
|
|
6686
|
+
var boundedFailureField = (value) => value.length <= 500 ? value : `${value.slice(0, 499)}…`;
|
|
7873
6687
|
async function cancelActiveDelegation(reason) {
|
|
7874
6688
|
const active = this.activeDelegation;
|
|
7875
6689
|
if (!active)
|
|
@@ -7902,25 +6716,6 @@ async function cleanupDelegation(active) {
|
|
|
7902
6716
|
} catch {}
|
|
7903
6717
|
}
|
|
7904
6718
|
}
|
|
7905
|
-
function retryDelegationAfterFailure(active, failure, reason) {
|
|
7906
|
-
if (!failure)
|
|
7907
|
-
return false;
|
|
7908
|
-
const fingerprint = this.delegationFailures.delegationFailureFingerprint(failure);
|
|
7909
|
-
const isRepeatedFailure = active.recoveryFailures.some((previousFailure) => previousFailure.fingerprint === fingerprint);
|
|
7910
|
-
if (active.recoveryAttemptCount >= MAX_DELEGATION_RECOVERY_ATTEMPTS || isRepeatedFailure || !this.delegationFailures.isRetryableTerminalFailure(failure) || !this.delegationFailures.isSafeToRetryDelegation(active.policy, active.broadRecoveryAuthorized, failure.replayAudit) || !this.isSessionActive || this.sessionEpoch !== active.sessionEpoch || !this.run || this.run.status !== "running" || this.run.runId !== active.runId || this.run.currentStepId !== active.stepId || this.run.currentStepDigest !== active.stepDigest || this.activeDelegation) {
|
|
7911
|
-
return false;
|
|
7912
|
-
}
|
|
7913
|
-
const workflow = this.catalog.workflows.get(this.run.workflowId);
|
|
7914
|
-
if (!workflow)
|
|
7915
|
-
return false;
|
|
7916
|
-
const attempt = active.recoveryAttemptCount + 1;
|
|
7917
|
-
this.latestContext?.ui.notify(`Automatic recovery for "${active.stepId}" after a subagent failure (${attempt}/${MAX_DELEGATION_RECOVERY_ATTEMPTS})`, "warning");
|
|
7918
|
-
this.launchCurrentStep(workflow, {
|
|
7919
|
-
attempt,
|
|
7920
|
-
failures: [...active.recoveryFailures, { fingerprint, reason }]
|
|
7921
|
-
});
|
|
7922
|
-
return true;
|
|
7923
|
-
}
|
|
7924
6719
|
function pauseForDelegationFailure(reason, failureSummary = reason) {
|
|
7925
6720
|
this.pauseForExecutionFailure("Subagent step", reason, failureSummary);
|
|
7926
6721
|
}
|
|
@@ -7968,7 +6763,6 @@ function createDelegationControlActions() {
|
|
|
7968
6763
|
return {
|
|
7969
6764
|
cancelActiveDelegation,
|
|
7970
6765
|
cleanupDelegation,
|
|
7971
|
-
retryDelegationAfterFailure,
|
|
7972
6766
|
pauseForDelegationFailure,
|
|
7973
6767
|
pauseForExecutionFailure,
|
|
7974
6768
|
retainUnconfirmedDelegation,
|
|
@@ -7976,6 +6770,44 @@ function createDelegationControlActions() {
|
|
|
7976
6770
|
};
|
|
7977
6771
|
}
|
|
7978
6772
|
|
|
6773
|
+
// src/harness/artifact-contract.ts
|
|
6774
|
+
function countOccurrences(value, substring) {
|
|
6775
|
+
let count = 0;
|
|
6776
|
+
let offset = 0;
|
|
6777
|
+
while (true) {
|
|
6778
|
+
const match = value.indexOf(substring, offset);
|
|
6779
|
+
if (match === -1)
|
|
6780
|
+
return count;
|
|
6781
|
+
count += 1;
|
|
6782
|
+
offset = match + substring.length;
|
|
6783
|
+
}
|
|
6784
|
+
}
|
|
6785
|
+
function validateArtifactContract(artifact, contract) {
|
|
6786
|
+
if (!contract)
|
|
6787
|
+
return;
|
|
6788
|
+
if (artifact.length > contract.maxChars) {
|
|
6789
|
+
return `gate artifact exceeds ${contract.maxChars} characters`;
|
|
6790
|
+
}
|
|
6791
|
+
const required = contract.requiredSubstrings.find((substring) => !artifact.includes(substring));
|
|
6792
|
+
if (required) {
|
|
6793
|
+
return `gate artifact is missing required text: ${JSON.stringify(required)}`;
|
|
6794
|
+
}
|
|
6795
|
+
const forbidden = contract.forbiddenSubstrings.find((substring) => artifact.includes(substring));
|
|
6796
|
+
if (forbidden) {
|
|
6797
|
+
return `gate artifact contains forbidden text: ${JSON.stringify(forbidden)}`;
|
|
6798
|
+
}
|
|
6799
|
+
for (const group of contract.equalOccurrenceGroups) {
|
|
6800
|
+
const counts = group.map((substring) => countOccurrences(artifact, substring));
|
|
6801
|
+
if (counts.some((count) => count === 0)) {
|
|
6802
|
+
return `gate artifact is missing required repeated text: ${JSON.stringify(group)}`;
|
|
6803
|
+
}
|
|
6804
|
+
if (!counts.every((count) => count === counts[0])) {
|
|
6805
|
+
return `gate artifact has unequal repeated text counts: ${JSON.stringify(group)}`;
|
|
6806
|
+
}
|
|
6807
|
+
}
|
|
6808
|
+
return;
|
|
6809
|
+
}
|
|
6810
|
+
|
|
7979
6811
|
// src/harness/gate-submission-action.ts
|
|
7980
6812
|
function isCurrentGateRequest(run, originalRun, requestId) {
|
|
7981
6813
|
return run !== undefined && run.runId === originalRun.runId && run.currentStepId === originalRun.currentStepId && run.pendingGate?.requestId === requestId && run.pendingGate.reviewId === undefined && (run.status === "awaiting-gate" || run.status === "paused");
|
|
@@ -7986,6 +6818,22 @@ async function submitGate(workflow, originalRun, outcome, summary, artifact) {
|
|
|
7986
6818
|
const step = workflow.definition.steps[originalRun.currentStepId];
|
|
7987
6819
|
if (!step?.gate)
|
|
7988
6820
|
throw new Error("Current step has no gate");
|
|
6821
|
+
const contractError = validateArtifactContract(artifact, step.gate.artifactContract);
|
|
6822
|
+
if (contractError) {
|
|
6823
|
+
if (step.gate.artifactContract?.onValidationFailure !== "retry") {
|
|
6824
|
+
throw new Error(contractError);
|
|
6825
|
+
}
|
|
6826
|
+
const retrySummary = `Artifact contract failed: ${contractError}`;
|
|
6827
|
+
this.run = advanceRun(workflow, originalRun, "retry", retrySummary, this.dependencies.now());
|
|
6828
|
+
this.persist();
|
|
6829
|
+
this.updateStatus();
|
|
6830
|
+
this.settleAfterTransition(workflow, {
|
|
6831
|
+
stepId: originalRun.currentStepId,
|
|
6832
|
+
outcome: "retry",
|
|
6833
|
+
summary: retrySummary
|
|
6834
|
+
});
|
|
6835
|
+
return;
|
|
6836
|
+
}
|
|
7989
6837
|
this.run = beginGate(workflow, originalRun, outcome, artifact, requestId, this.dependencies.now(), summary);
|
|
7990
6838
|
this.persist();
|
|
7991
6839
|
this.restoreBaselineTools();
|
|
@@ -8253,12 +7101,8 @@ function preflightStep(step, inventory) {
|
|
|
8253
7101
|
const toolNames = new Set(inventory.tools.map((tool) => tool.name));
|
|
8254
7102
|
const extensionResources = [...inventory.tools, ...inventory.commands];
|
|
8255
7103
|
const hasExtension = (extension) => extensionResources.some((resource) => sourceMatches(resource, extension));
|
|
8256
|
-
const hasSubagentTool = inventory.tools.some((tool) => tool.name === "subagent" && sourceMatches(tool, "pi-subagents"));
|
|
8257
7104
|
const isPlannotatorRequired = step.gate?.provider === "plannotator" && !step.requires.extensions.includes("plannotator");
|
|
8258
7105
|
return [
|
|
8259
|
-
...step.subagent && !hasSubagentTool ? [
|
|
8260
|
-
'pi-subagents is required, but its "subagent" tool is not installed or detectable'
|
|
8261
|
-
] : [],
|
|
8262
7106
|
...missingRequiredResources({
|
|
8263
7107
|
requiredNames: step.requires.tools,
|
|
8264
7108
|
hasResource: (toolName) => toolNames.has(toolName),
|
|
@@ -8463,7 +7307,6 @@ var CORE_ACTIONS = createCoreActions();
|
|
|
8463
7307
|
class WorkflowHarness {
|
|
8464
7308
|
pi;
|
|
8465
7309
|
dependencies;
|
|
8466
|
-
delegationFailures;
|
|
8467
7310
|
subagents;
|
|
8468
7311
|
mainSteps;
|
|
8469
7312
|
catalog = createEmptyCatalog();
|
|
@@ -8511,7 +7354,6 @@ class WorkflowHarness {
|
|
|
8511
7354
|
finishDelegation = DELEGATION_RESPONSE_ACTIONS.finishDelegation;
|
|
8512
7355
|
cancelActiveDelegation = DELEGATION_CONTROL_ACTIONS.cancelActiveDelegation;
|
|
8513
7356
|
cleanupDelegation = DELEGATION_CONTROL_ACTIONS.cleanupDelegation;
|
|
8514
|
-
retryDelegationAfterFailure = DELEGATION_CONTROL_ACTIONS.retryDelegationAfterFailure;
|
|
8515
7357
|
pauseForDelegationFailure = DELEGATION_CONTROL_ACTIONS.pauseForDelegationFailure;
|
|
8516
7358
|
pauseForExecutionFailure = DELEGATION_CONTROL_ACTIONS.pauseForExecutionFailure;
|
|
8517
7359
|
retainUnconfirmedDelegation = DELEGATION_CONTROL_ACTIONS.retainUnconfirmedDelegation;
|
|
@@ -8537,7 +7379,6 @@ class WorkflowHarness {
|
|
|
8537
7379
|
constructor(pi, statusShortcut = DEFAULT_STATUS_SHORTCUT, dependencyOverrides = {}) {
|
|
8538
7380
|
this.pi = pi;
|
|
8539
7381
|
this.dependencies = createWorkflowHarnessDependencies(dependencyOverrides);
|
|
8540
|
-
this.delegationFailures = createDelegationFailureActions(this.dependencies);
|
|
8541
7382
|
this.statusShortcut = statusShortcut;
|
|
8542
7383
|
this.statusShortcutLabel = formatShortcutLabel(statusShortcut);
|
|
8543
7384
|
this.subagents = this.dependencies.createSubagentClient(pi);
|
|
@@ -8597,6 +7438,9 @@ class WorkflowHarness {
|
|
|
8597
7438
|
}
|
|
8598
7439
|
}
|
|
8599
7440
|
|
|
7441
|
+
// src/integrations/subagents/child-runtime.ts
|
|
7442
|
+
import { Type as Type2 } from "typebox";
|
|
7443
|
+
|
|
8600
7444
|
// src/integrations/subagents/child-runtime-completion.ts
|
|
8601
7445
|
var CHILD_COMPLETION_TOOL = "structured_output";
|
|
8602
7446
|
var CHILD_COORDINATION_TOOLS = new Set([
|
|
@@ -8638,9 +7482,9 @@ var parseChildStructuredResult = ({
|
|
|
8638
7482
|
// src/integrations/subagents/child-runtime-dependencies.ts
|
|
8639
7483
|
import { randomUUID as randomUUID2, timingSafeEqual } from "node:crypto";
|
|
8640
7484
|
import {
|
|
8641
|
-
existsSync as
|
|
7485
|
+
existsSync as existsSync3,
|
|
8642
7486
|
lstatSync,
|
|
8643
|
-
readFileSync,
|
|
7487
|
+
readFileSync as readFileSync2,
|
|
8644
7488
|
realpathSync as realpathSync2,
|
|
8645
7489
|
renameSync,
|
|
8646
7490
|
statSync as statSync2,
|
|
@@ -8655,9 +7499,9 @@ var tokensAreEqual = (actual, expected) => {
|
|
|
8655
7499
|
};
|
|
8656
7500
|
var DEFAULT_CHILD_RUNTIME_DEPENDENCIES = {
|
|
8657
7501
|
fileSystem: {
|
|
8658
|
-
exists:
|
|
7502
|
+
exists: existsSync3,
|
|
8659
7503
|
inspect: lstatSync,
|
|
8660
|
-
readText: (path) =>
|
|
7504
|
+
readText: (path) => readFileSync2(path, "utf8"),
|
|
8661
7505
|
realPath: realpathSync2,
|
|
8662
7506
|
rename: renameSync,
|
|
8663
7507
|
stat: statSync2,
|
|
@@ -8672,7 +7516,7 @@ var DEFAULT_CHILD_RUNTIME_DEPENDENCIES = {
|
|
|
8672
7516
|
},
|
|
8673
7517
|
createUniqueId: randomUUID2,
|
|
8674
7518
|
currentWorkingDirectory: () => process.cwd(),
|
|
8675
|
-
environmentChildAgent: () => process.env.
|
|
7519
|
+
environmentChildAgent: () => process.env.PI_WORKFLOWS_CHILD_AGENT?.trim() || undefined,
|
|
8676
7520
|
temporaryDirectory: tmpdir3,
|
|
8677
7521
|
tokensAreEqual
|
|
8678
7522
|
};
|
|
@@ -8734,13 +7578,7 @@ var writeChildResult = ({
|
|
|
8734
7578
|
var childPolicyStep = (policy) => ({
|
|
8735
7579
|
title: policy.stepTitle,
|
|
8736
7580
|
prompt: { inline: "Delegated workflow step" },
|
|
8737
|
-
|
|
8738
|
-
agent: policy.agent,
|
|
8739
|
-
context: "fresh",
|
|
8740
|
-
timeoutMs: 900000,
|
|
8741
|
-
artifacts: false,
|
|
8742
|
-
retryToolFailures: false
|
|
8743
|
-
},
|
|
7581
|
+
agent: { name: policy.agent },
|
|
8744
7582
|
permissions: policy.permissions,
|
|
8745
7583
|
requires: { tools: [], extensions: [], skills: [] },
|
|
8746
7584
|
transitions: {},
|
|
@@ -8796,6 +7634,20 @@ var registerSubagentChildRuntime = (pi, options = {}) => {
|
|
|
8796
7634
|
const dependencies = options.dependencies ?? DEFAULT_CHILD_RUNTIME_DEPENDENCIES;
|
|
8797
7635
|
const childAgent = resolveChildAgent(options, dependencies);
|
|
8798
7636
|
let state = INITIAL_STATE;
|
|
7637
|
+
pi.registerTool({
|
|
7638
|
+
name: CHILD_COMPLETION_TOOL,
|
|
7639
|
+
label: "Complete Workflow Step",
|
|
7640
|
+
description: "Return the one structured result for this workflow step",
|
|
7641
|
+
parameters: Type2.Object({ value: Type2.Any() }),
|
|
7642
|
+
executionMode: "sequential",
|
|
7643
|
+
execute: async () => ({
|
|
7644
|
+
content: [
|
|
7645
|
+
{ type: "text", text: "Captured workflow step result." }
|
|
7646
|
+
],
|
|
7647
|
+
details: {},
|
|
7648
|
+
terminate: true
|
|
7649
|
+
})
|
|
7650
|
+
});
|
|
8799
7651
|
pi.on("input", (event) => {
|
|
8800
7652
|
let extracted;
|
|
8801
7653
|
try {
|
|
@@ -8815,9 +7667,8 @@ var registerSubagentChildRuntime = (pi, options = {}) => {
|
|
|
8815
7667
|
return invalidPolicyInput(pi, policyError, event.images);
|
|
8816
7668
|
}
|
|
8817
7669
|
try {
|
|
8818
|
-
if (!
|
|
8819
|
-
throw new Error("
|
|
8820
|
-
}
|
|
7670
|
+
if (!childAgent)
|
|
7671
|
+
throw new Error("workflow worker agent is unavailable");
|
|
8821
7672
|
verifyChildWorkingDirectory(extracted.policy, dependencies);
|
|
8822
7673
|
verifyChildCapability({
|
|
8823
7674
|
policy: extracted.policy,
|
|
@@ -8826,7 +7677,7 @@ var registerSubagentChildRuntime = (pi, options = {}) => {
|
|
|
8826
7677
|
});
|
|
8827
7678
|
const profileTools = new Set(pi.getActiveTools());
|
|
8828
7679
|
if (!profileTools.has(CHILD_COMPLETION_TOOL)) {
|
|
8829
|
-
throw new Error("
|
|
7680
|
+
throw new Error("workflow worker completion tool is unavailable");
|
|
8830
7681
|
}
|
|
8831
7682
|
const effectiveTools = new Set(resolveActiveTools(pi.getAllTools(), childPolicyStep(extracted.policy), CHILD_COMPLETION_TOOL).filter((toolName) => !CHILD_COORDINATION_TOOLS.has(toolName)));
|
|
8832
7683
|
state = {
|
|
@@ -8945,10 +7796,9 @@ var DEFAULT_DEPENDENCIES4 = {
|
|
|
8945
7796
|
loadSettings: loadSettings2,
|
|
8946
7797
|
userWorkflowDirectory: defaultUserWorkflowDirectory2,
|
|
8947
7798
|
runtimeEnvironment: () => ({
|
|
8948
|
-
isSubagentChild: process.env.
|
|
8949
|
-
childAgent: process.env.
|
|
7799
|
+
isSubagentChild: process.env.PI_WORKFLOWS_CHILD === "1",
|
|
7800
|
+
childAgent: process.env.PI_WORKFLOWS_CHILD_AGENT?.trim()
|
|
8950
7801
|
}),
|
|
8951
|
-
isSubagentRuntimeName,
|
|
8952
7802
|
registerChildRuntime: (pi, childAgent) => {
|
|
8953
7803
|
registerSubagentChildRuntime(pi, { childAgent });
|
|
8954
7804
|
},
|
|
@@ -8959,10 +7809,8 @@ var DEFAULT_DEPENDENCIES4 = {
|
|
|
8959
7809
|
function createPiWorkflowsExtension(dependencies) {
|
|
8960
7810
|
return async (pi) => {
|
|
8961
7811
|
const environment = dependencies.runtimeEnvironment();
|
|
8962
|
-
if (environment.isSubagentChild) {
|
|
8963
|
-
|
|
8964
|
-
dependencies.registerChildRuntime(pi, environment.childAgent);
|
|
8965
|
-
}
|
|
7812
|
+
if (environment.isSubagentChild && environment.childAgent) {
|
|
7813
|
+
dependencies.registerChildRuntime(pi, environment.childAgent);
|
|
8966
7814
|
return;
|
|
8967
7815
|
}
|
|
8968
7816
|
const { settings } = await dependencies.loadSettings(dependencies.userWorkflowDirectory());
|
|
@@ -8972,6 +7820,6 @@ function createPiWorkflowsExtension(dependencies) {
|
|
|
8972
7820
|
var piWorkflowsExtension = createPiWorkflowsExtension(DEFAULT_DEPENDENCIES4);
|
|
8973
7821
|
var src_default = piWorkflowsExtension;
|
|
8974
7822
|
export {
|
|
8975
|
-
|
|
8976
|
-
|
|
7823
|
+
createPiWorkflowsExtension,
|
|
7824
|
+
src_default as default
|
|
8977
7825
|
};
|