stageflow 0.42.0 → 0.44.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 +12 -3
- package/dist/agent/activity.d.ts +18 -0
- package/dist/agent/claudeAdapter.js +7 -0
- package/dist/agent/claudeTools.d.ts +27 -18
- package/dist/agent/claudeTools.js +54 -15
- package/dist/agent/compactionEvents.d.ts +34 -0
- package/dist/agent/compactionEvents.js +37 -0
- package/dist/agent/openRouterRouting.d.ts +7 -0
- package/dist/agent/openRouterRouting.js +23 -0
- package/dist/agent/piAdapter.d.ts +6 -0
- package/dist/agent/piAdapter.js +128 -27
- package/dist/agent/piSettings.d.ts +7 -0
- package/dist/agent/piSettings.js +9 -0
- package/dist/agent/sandboxPiTools.d.ts +3 -1
- package/dist/agent/sandboxPiTools.js +8 -2
- package/dist/agent/stageTurnDriver.d.ts +11 -2
- package/dist/agent/stageTurnDriver.js +45 -4
- package/dist/agent/toolOutputBudget.d.ts +15 -1
- package/dist/agent/toolOutputBudget.js +18 -0
- package/dist/agent/toolOutputBudgetExtension.d.ts +2 -0
- package/dist/agent/toolOutputBudgetExtension.js +40 -3
- package/dist/agents/authorChat.d.ts +1 -0
- package/dist/agents/authorChat.js +7 -0
- package/dist/agents/authorSessionStore.d.ts +3 -0
- package/dist/agents/authorSessionStore.js +17 -2
- package/dist/agents/hostContext.d.ts +1 -1
- package/dist/agents/runtime/piRuntime.d.ts +5 -1
- package/dist/agents/runtime/piRuntime.js +46 -12
- package/dist/agents/service.js +3 -2
- package/dist/agents/sessions.js +3 -1
- package/dist/compaction/directives.d.ts +18 -0
- package/dist/compaction/directives.js +119 -0
- package/dist/compaction/extension.d.ts +20 -0
- package/dist/compaction/extension.js +78 -0
- package/dist/compaction/mask.d.ts +18 -0
- package/dist/compaction/mask.js +155 -0
- package/dist/compaction/pin.d.ts +21 -0
- package/dist/compaction/pin.js +270 -0
- package/dist/compaction/scrub.d.ts +7 -0
- package/dist/compaction/scrub.js +26 -0
- package/dist/compaction/sessionWiring.d.ts +22 -0
- package/dist/compaction/sessionWiring.js +26 -0
- package/dist/compaction/summarize.d.ts +79 -0
- package/dist/compaction/summarize.js +171 -0
- package/dist/compaction/toolPolicy.d.ts +3 -0
- package/dist/compaction/toolPolicy.js +33 -0
- package/dist/compaction/types.d.ts +45 -0
- package/dist/compaction/types.js +7 -0
- package/dist/compaction/workingState.d.ts +56 -0
- package/dist/compaction/workingState.js +198 -0
- package/dist/config/createPipeline.js +2 -0
- package/dist/config/createTrigger.js +7 -1
- package/dist/config/ensureCatalogScanRoot.d.ts +2 -0
- package/dist/config/ensureCatalogScanRoot.js +6 -0
- package/dist/config/evalCatalog.js +30 -2
- package/dist/config/githubTriggerAction.d.ts +8 -0
- package/dist/config/githubTriggerAction.js +29 -0
- package/dist/config/hostConfig.js +2 -0
- package/dist/config/loadEval.js +21 -3
- package/dist/config/pipelineNeeds.d.ts +2 -0
- package/dist/config/pipelineNeeds.js +16 -3
- package/dist/config/pipelineRoute.js +5 -4
- package/dist/config/validateCatalog.d.ts +1 -1
- package/dist/config/validateCatalog.js +12 -0
- package/dist/config/validateEval.js +43 -2
- package/dist/config/writeEval.js +2 -2
- package/dist/envelope/assertEmittedArtifacts.d.ts +12 -0
- package/dist/envelope/assertEmittedArtifacts.js +43 -0
- package/dist/envelope/preEmitChecks.d.ts +3 -0
- package/dist/evals/constants.d.ts +3 -1
- package/dist/evals/constants.js +3 -1
- package/dist/evals/engine/FakeEngine.d.ts +1 -1
- package/dist/evals/engine/FakeEngine.js +2 -1
- package/dist/evals/engine/promptfoo/PromptfooEngine.d.ts +1 -1
- package/dist/evals/engine/promptfoo/PromptfooEngine.js +2 -1
- package/dist/evals/engine/promptfoo/generate.d.ts +1 -1
- package/dist/evals/engine/promptfoo/generate.js +11 -8
- package/dist/evals/engine/promptfoo/rubricPrompt.d.ts +1 -1
- package/dist/evals/engine/promptfoo/rubricPrompt.js +5 -3
- package/dist/evals/manager.d.ts +13 -0
- package/dist/evals/manager.js +10 -2
- package/dist/evals/plan.d.ts +8 -1
- package/dist/evals/plan.js +28 -9
- package/dist/evals/planInternalAgent.d.ts +19 -0
- package/dist/evals/planInternalAgent.js +205 -0
- package/dist/evals/read/api.d.ts +12 -0
- package/dist/evals/read/api.js +5 -0
- package/dist/evals/read/list.js +10 -1
- package/dist/evals/read/runs.d.ts +5 -0
- package/dist/evals/read/runs.js +7 -1
- package/dist/evals/runner/caseRunner.d.ts +2 -1
- package/dist/evals/runner/caseRunner.js +16 -5
- package/dist/evals/runner/internalAgentCaseRunner.d.ts +6 -0
- package/dist/evals/runner/internalAgentCaseRunner.js +250 -0
- package/dist/evals/snapshot.d.ts +10 -2
- package/dist/evals/snapshot.js +8 -3
- package/dist/evals/types.d.ts +12 -2
- package/dist/evals/types.js +4 -1
- package/dist/guide/cardContract.d.ts +133 -0
- package/dist/guide/cardContract.js +156 -0
- package/dist/guide/chat.d.ts +45 -0
- package/dist/guide/chat.js +378 -0
- package/dist/guide/knowledge/index.d.ts +14 -0
- package/dist/guide/knowledge/index.js +109 -0
- package/dist/guide/knowledge/topics/agents.d.ts +2 -0
- package/dist/guide/knowledge/topics/agents.js +58 -0
- package/dist/guide/knowledge/topics/bestPractices.d.ts +2 -0
- package/dist/guide/knowledge/topics/bestPractices.js +67 -0
- package/dist/guide/knowledge/topics/connections.d.ts +2 -0
- package/dist/guide/knowledge/topics/connections.js +66 -0
- package/dist/guide/knowledge/topics/consoleMap.d.ts +2 -0
- package/dist/guide/knowledge/topics/consoleMap.js +55 -0
- package/dist/guide/knowledge/topics/evals.d.ts +2 -0
- package/dist/guide/knowledge/topics/evals.js +72 -0
- package/dist/guide/knowledge/topics/limitations.d.ts +2 -0
- package/dist/guide/knowledge/topics/limitations.js +84 -0
- package/dist/guide/knowledge/topics/mcpSkillsBrowser.d.ts +2 -0
- package/dist/guide/knowledge/topics/mcpSkillsBrowser.js +57 -0
- package/dist/guide/knowledge/topics/memories.d.ts +2 -0
- package/dist/guide/knowledge/topics/memories.js +63 -0
- package/dist/guide/knowledge/topics/modelsAndProviders.d.ts +2 -0
- package/dist/guide/knowledge/topics/modelsAndProviders.js +56 -0
- package/dist/guide/knowledge/topics/overview.d.ts +2 -0
- package/dist/guide/knowledge/topics/overview.js +56 -0
- package/dist/guide/knowledge/topics/pipelines.d.ts +2 -0
- package/dist/guide/knowledge/topics/pipelines.js +93 -0
- package/dist/guide/knowledge/topics/stages.d.ts +2 -0
- package/dist/guide/knowledge/topics/stages.js +54 -0
- package/dist/guide/knowledge/topics/tasks.d.ts +2 -0
- package/dist/guide/knowledge/topics/tasks.js +52 -0
- package/dist/guide/knowledge/topics/triggers.d.ts +2 -0
- package/dist/guide/knowledge/topics/triggers.js +68 -0
- package/dist/guide/knowledge/topics/troubleshooting.d.ts +2 -0
- package/dist/guide/knowledge/topics/troubleshooting.js +85 -0
- package/dist/guide/knowledgeTools.d.ts +2 -0
- package/dist/guide/knowledgeTools.js +56 -0
- package/dist/guide/showUi.d.ts +6 -0
- package/dist/guide/showUi.js +546 -0
- package/dist/guide/threadStore.d.ts +48 -0
- package/dist/guide/threadStore.js +189 -0
- package/dist/guide/writeRoot.d.ts +11 -0
- package/dist/guide/writeRoot.js +70 -0
- package/dist/hosted/translate.js +2 -0
- package/dist/mcp/agentTools.js +12 -0
- package/dist/mcp/catalogTools.js +18 -20
- package/dist/mcp/catalogWriteTools.d.ts +8 -0
- package/dist/mcp/catalogWriteTools.js +225 -0
- package/dist/mcp/connectionTools.d.ts +3 -0
- package/dist/mcp/connectionTools.js +85 -0
- package/dist/mcp/controlTools.js +34 -1
- package/dist/mcp/deps.d.ts +9 -0
- package/dist/mcp/evalTools.js +684 -21
- package/dist/mcp/memoryHostTools.d.ts +3 -0
- package/dist/mcp/memoryHostTools.js +72 -0
- package/dist/mcp/pipelineAuthorTools.js +14 -1
- package/dist/mcp/server.js +5 -0
- package/dist/mcp/tools.js +6 -0
- package/dist/mcp/triggerTools.js +77 -31
- package/dist/operatorAgent/evalGate.d.ts +22 -0
- package/dist/operatorAgent/evalGate.js +77 -0
- package/dist/operatorAgent/fakeHost.js +10 -2
- package/dist/operatorAgent/host.js +18 -2
- package/dist/operatorAgent/index.d.ts +1 -0
- package/dist/operatorAgent/index.js +1 -0
- package/dist/operatorAgent/internalAgents.d.ts +11 -0
- package/dist/operatorAgent/internalAgents.js +13 -0
- package/dist/operatorAgent/piModel.d.ts +5 -1
- package/dist/operatorAgent/piModel.js +15 -10
- package/dist/operatorAgent/profiles/agentAuthor.js +66 -22
- package/dist/operatorAgent/profiles/agentAuthorPlaybook.d.ts +1 -1
- package/dist/operatorAgent/profiles/agentAuthorPlaybook.js +1 -1
- package/dist/operatorAgent/profiles/guideConfirm.d.ts +21 -0
- package/dist/operatorAgent/profiles/guideConfirm.js +100 -0
- package/dist/operatorAgent/profiles/guideTools.d.ts +7 -0
- package/dist/operatorAgent/profiles/guideTools.js +123 -0
- package/dist/operatorAgent/profiles/stageflowGuide.d.ts +7 -0
- package/dist/operatorAgent/profiles/stageflowGuide.js +132 -0
- package/dist/operatorAgent/profiles/workshopAuthor.js +23 -0
- package/dist/operatorAgent/profiles/workshopAuthorPlaybook.d.ts +1 -1
- package/dist/operatorAgent/profiles/workshopAuthorPlaybook.js +9 -1
- package/dist/operatorAgent/profiles/workshopAuthorTools.d.ts +2 -0
- package/dist/operatorAgent/profiles/workshopAuthorTools.js +197 -9
- package/dist/operatorAgent/types.d.ts +23 -0
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/runstore/retention.d.ts +2 -1
- package/dist/runstore/retention.js +3 -7
- package/dist/runstore/runStatusClock.d.ts +17 -0
- package/dist/runstore/runStatusClock.js +19 -0
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +1 -0
- package/dist/runstore/sqlite/SqliteRunStore.js +45 -45
- package/dist/runstore/workspaceLayout.js +6 -1
- package/dist/runtime/artifactDiskInspect.d.ts +14 -0
- package/dist/runtime/artifactDiskInspect.js +52 -0
- package/dist/runtime/artifactPath.d.ts +24 -0
- package/dist/runtime/artifactPath.js +84 -0
- package/dist/runtime/completionCheckRunner.js +15 -43
- package/dist/runtime/githubPollSource.d.ts +22 -4
- package/dist/runtime/githubPollSource.js +93 -8
- package/dist/runtime/priorArtifactGrants.d.ts +8 -0
- package/dist/runtime/priorArtifactGrants.js +105 -0
- package/dist/runtime/runActionEligibility.d.ts +46 -0
- package/dist/runtime/runActionEligibility.js +123 -0
- package/dist/runtime/runManager.d.ts +4 -1
- package/dist/runtime/runManager.js +52 -10
- package/dist/runtime/runRetryCoordinator.d.ts +2 -14
- package/dist/runtime/runRetryCoordinator.js +3 -67
- package/dist/runtime/sandboxContainer.d.ts +7 -0
- package/dist/runtime/sandboxContainer.js +48 -6
- package/dist/runtime/sandboxDesk.d.ts +2 -1
- package/dist/runtime/sandboxDesk.js +11 -12
- package/dist/runtime/stageEnvironment.js +1 -0
- package/dist/runtime/stageRoots.d.ts +12 -0
- package/dist/runtime/stageSandbox.d.ts +2 -1
- package/dist/runtime/stageSandbox.js +18 -2
- package/dist/runtime/stageWorker.js +20 -0
- package/dist/server/agentAuthorRoutes.js +12 -3
- package/dist/server/bootstrap.d.ts +5 -0
- package/dist/server/bootstrap.js +14 -1
- package/dist/server/evalRoutes.js +38 -9
- package/dist/server/guideRoutes.d.ts +5 -0
- package/dist/server/guideRoutes.js +166 -0
- package/dist/server/http.d.ts +10 -0
- package/dist/server/http.js +35 -1
- package/dist/server/internalAgentRoutes.d.ts +6 -0
- package/dist/server/internalAgentRoutes.js +12 -0
- package/dist/server/mcpHost.js +2 -0
- package/dist/toolkit/meta.d.ts +1 -1
- package/dist/toolkit/meta.js +39 -1
- package/dist/toolkit/profiles.d.ts +5 -0
- package/dist/toolkit/profiles.js +12 -0
- package/dist/tools/askOperator.js +2 -1
- package/dist/tools/emitStageEnvelope.d.ts +17 -16
- package/dist/tools/emitStageEnvelope.js +46 -32
- package/dist/tools/stageToolNames.d.ts +4 -0
- package/dist/tools/stageToolNames.js +4 -0
- package/dist/tools/workingState.d.ts +40 -0
- package/dist/tools/workingState.js +108 -0
- package/dist/tools/writeStageArtifact.d.ts +0 -9
- package/dist/tools/writeStageArtifact.js +2 -1
- package/dist/ui/assets/{MarkdownText-BTrOqu5J.js → MarkdownText-S5C_vemc.js} +2 -2
- package/dist/ui/assets/adapters-C0l4ruiR.js +17 -0
- package/dist/ui/assets/chat-CjnsmvqR.js +2 -0
- package/dist/ui/assets/chat-DFDd4UjS.css +2 -0
- package/dist/ui/assets/{main-CHlmTNJe.css → main-Del1GeLf.css} +1 -1
- package/dist/ui/assets/main-_yXVqcW9.js +277 -0
- package/dist/ui/chat.html +3 -4
- package/dist/ui/index.html +3 -4
- package/dist/workshop/chatTurn.js +1 -1
- package/dist/workshop/workshopPin.d.ts +3 -3
- package/dist/workshop/workshopPin.js +16 -15
- package/examples/compaction-stress/README.md +30 -0
- package/examples/compaction-stress/check-run.mjs +53 -0
- package/examples/compaction-stress/collect-evidence.yaml +60 -0
- package/examples/compaction-stress/compaction-stress.pipeline.yaml +141 -0
- package/examples/compaction-stress/compaction-stress.task.yaml +15 -0
- package/examples/compaction-stress/generate-evidence.mjs +110 -0
- package/examples/compaction-stress/root-cause.yaml +26 -0
- package/examples/compaction-stress/stageflow.yaml +9 -0
- package/examples/compaction-stress/write-postmortem.yaml +40 -0
- package/examples/evals/README.md +18 -0
- package/examples/evals/guide-behavior.dataset.jsonl +15 -0
- package/examples/evals/guide-behavior.eval.yaml +35 -0
- package/examples/evals/guide-depth.dataset.jsonl +35 -0
- package/examples/evals/guide-depth.eval.yaml +169 -0
- package/examples/memory/evals/guide-behavior.dataset.jsonl +15 -0
- package/examples/memory/evals/guide-behavior.eval.yaml +35 -0
- package/examples/memory/evals/guide-depth.dataset.jsonl +35 -0
- package/examples/memory/evals/guide-depth.eval.yaml +169 -0
- package/package.json +1 -1
- package/dist/ui/assets/AssistantRuntimeProvider-qIX1xNoH.js +0 -9
- package/dist/ui/assets/ReadonlyStore-BfXqHykm.js +0 -6
- package/dist/ui/assets/chat-Df_wCpak.js +0 -5
- package/dist/ui/assets/chat-P7KQznHm.css +0 -2
- package/dist/ui/assets/main-CvVfxmMd.js +0 -271
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isInsideDir } from "../runstore/workspaceLayout.js";
|
|
4
|
+
import { acceptArtifactListedPath, isGrantableArtifactPath, } from "./artifactPath.js";
|
|
5
|
+
import { inspectArtifactUnderRoot } from "./artifactDiskInspect.js";
|
|
6
|
+
import { SANDBOX_MOUNT_PATH } from "./sandboxContainer.js";
|
|
7
|
+
export { isGrantableArtifactPath } from "./artifactPath.js";
|
|
8
|
+
function successArtifactsFromPrior(prior) {
|
|
9
|
+
const envelopes = [];
|
|
10
|
+
if (prior.prior !== null && prior.prior !== undefined) {
|
|
11
|
+
envelopes.push(prior.prior);
|
|
12
|
+
}
|
|
13
|
+
if (prior.joinPriors !== undefined) {
|
|
14
|
+
envelopes.push(...prior.joinPriors);
|
|
15
|
+
}
|
|
16
|
+
if (prior.priorEnvelopesByStage !== undefined) {
|
|
17
|
+
for (const value of Object.values(prior.priorEnvelopesByStage)) {
|
|
18
|
+
if (Array.isArray(value))
|
|
19
|
+
envelopes.push(...value);
|
|
20
|
+
else
|
|
21
|
+
envelopes.push(value);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const listed = [];
|
|
25
|
+
for (const envelope of envelopes) {
|
|
26
|
+
if (envelope.status !== "success")
|
|
27
|
+
continue;
|
|
28
|
+
listed.push(...envelope.artifacts);
|
|
29
|
+
}
|
|
30
|
+
return listed;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Soft-skip grant host resolution: missing / symlink / non-file → null.
|
|
34
|
+
* Closed-posture walk via inspect; realpath must stay inside the workspace.
|
|
35
|
+
*/
|
|
36
|
+
async function resolveGrantableHostFile(workspaceRoot, listedPath) {
|
|
37
|
+
try {
|
|
38
|
+
const presence = await inspectArtifactUnderRoot(workspaceRoot, listedPath);
|
|
39
|
+
if (presence.file_type !== "file" || presence.absolutePath === undefined) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const current = presence.absolutePath;
|
|
43
|
+
if (!isInsideDir(current, workspaceRoot) || current === workspaceRoot) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const workspaceReal = await realpath(workspaceRoot);
|
|
47
|
+
const fileReal = await realpath(current);
|
|
48
|
+
if (!isInsideDir(fileReal, workspaceReal) || fileReal === workspaceReal) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return current;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Sandbox path for one artifact. Colliding names from different attempts include the attempt. */
|
|
58
|
+
export function assignGrantContainerPaths(listedPaths) {
|
|
59
|
+
const parsed = listedPaths.map((listedPath) => {
|
|
60
|
+
const parts = acceptArtifactListedPath(listedPath, { kind: "grantable" });
|
|
61
|
+
const base = parts === undefined
|
|
62
|
+
? null
|
|
63
|
+
: `${SANDBOX_MOUNT_PATH}/grants/${parts.stageId}/${parts.relative}`;
|
|
64
|
+
return { listedPath, parts, base };
|
|
65
|
+
});
|
|
66
|
+
const counts = new Map();
|
|
67
|
+
for (const item of parsed) {
|
|
68
|
+
if (item.base === null)
|
|
69
|
+
continue;
|
|
70
|
+
counts.set(item.base, (counts.get(item.base) ?? 0) + 1);
|
|
71
|
+
}
|
|
72
|
+
const assigned = new Map();
|
|
73
|
+
for (const item of parsed) {
|
|
74
|
+
if (item.parts === undefined || item.base === null)
|
|
75
|
+
continue;
|
|
76
|
+
const containerPath = (counts.get(item.base) ?? 0) > 1
|
|
77
|
+
? `${SANDBOX_MOUNT_PATH}/grants/${item.parts.stageId}/attempt-${item.parts.attempt}/${item.parts.relative}`
|
|
78
|
+
: item.base;
|
|
79
|
+
assigned.set(item.listedPath, containerPath);
|
|
80
|
+
}
|
|
81
|
+
return assigned;
|
|
82
|
+
}
|
|
83
|
+
export async function artifactGrantsFromPriors(workspaceDir, prior) {
|
|
84
|
+
const workspaceRoot = path.resolve(workspaceDir);
|
|
85
|
+
const listed = successArtifactsFromPrior(prior).filter(isGrantableArtifactPath);
|
|
86
|
+
const containerPaths = assignGrantContainerPaths(listed);
|
|
87
|
+
const grants = [];
|
|
88
|
+
const seen = new Set();
|
|
89
|
+
for (const listedPath of listed) {
|
|
90
|
+
const containerPath = containerPaths.get(listedPath);
|
|
91
|
+
if (containerPath === undefined)
|
|
92
|
+
continue;
|
|
93
|
+
const hostPath = await resolveGrantableHostFile(workspaceRoot, listedPath);
|
|
94
|
+
if (hostPath === null || seen.has(hostPath))
|
|
95
|
+
continue;
|
|
96
|
+
seen.add(hostPath);
|
|
97
|
+
grants.push({
|
|
98
|
+
listedPath,
|
|
99
|
+
hostPath,
|
|
100
|
+
containerPath,
|
|
101
|
+
kind: "file",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return grants;
|
|
105
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type RunDetail, type RunPipelineDagSnapshot, type RunStatus, type StageSnapshot } from "../runstore/port.js";
|
|
2
|
+
export type StageRetrySubject = Pick<StageSnapshot, "stage_id" | "status" | "definition_id" | "envelope">;
|
|
3
|
+
export type RunActionEligibilityInput = {
|
|
4
|
+
runStatus: RunStatus;
|
|
5
|
+
stage?: StageRetrySubject;
|
|
6
|
+
stages?: ReadonlyArray<Pick<StageSnapshot, "status" | "stage_id">>;
|
|
7
|
+
recoveryActive?: boolean;
|
|
8
|
+
dag?: Pick<RunPipelineDagSnapshot, "nodes"> | null;
|
|
9
|
+
};
|
|
10
|
+
export type RunActionEligibility = {
|
|
11
|
+
canRetry: boolean;
|
|
12
|
+
canCancel: boolean;
|
|
13
|
+
canAbandon: boolean;
|
|
14
|
+
blockers: string[];
|
|
15
|
+
};
|
|
16
|
+
export declare function resumesRunOnStageRetry(status: RunStatus): boolean;
|
|
17
|
+
export declare function canCancelRun(status: RunStatus): boolean;
|
|
18
|
+
export declare function canAbandonStage(stageStatus: string): boolean;
|
|
19
|
+
export declare function succeededStageRetryBlocker(stageSnap: StageRetrySubject): string | undefined;
|
|
20
|
+
export declare function stageRetryBlocker(input: {
|
|
21
|
+
runStatus: RunStatus;
|
|
22
|
+
stage: StageRetrySubject;
|
|
23
|
+
stages?: ReadonlyArray<Pick<StageSnapshot, "status" | "stage_id">>;
|
|
24
|
+
recoveryActive?: boolean;
|
|
25
|
+
dag?: Pick<RunPipelineDagSnapshot, "nodes"> | null;
|
|
26
|
+
}): string | undefined;
|
|
27
|
+
export declare function canRetryStage(input: {
|
|
28
|
+
runStatus: RunStatus;
|
|
29
|
+
stage: StageRetrySubject;
|
|
30
|
+
stages?: ReadonlyArray<Pick<StageSnapshot, "status" | "stage_id">>;
|
|
31
|
+
recoveryActive?: boolean;
|
|
32
|
+
dag?: Pick<RunPipelineDagSnapshot, "nodes"> | null;
|
|
33
|
+
}): boolean;
|
|
34
|
+
export declare function runActionEligibility(input: RunActionEligibilityInput): RunActionEligibility;
|
|
35
|
+
export declare function assertStageRetryEligible(detail: RunDetail, stageId: string, opts?: {
|
|
36
|
+
recoveryActive?: boolean;
|
|
37
|
+
persistedStatus?: RunStatus;
|
|
38
|
+
dag?: Pick<RunPipelineDagSnapshot, "nodes"> | null;
|
|
39
|
+
}): {
|
|
40
|
+
ok: true;
|
|
41
|
+
} | {
|
|
42
|
+
ok: false;
|
|
43
|
+
reason: string;
|
|
44
|
+
status: number;
|
|
45
|
+
code: string;
|
|
46
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { findUnhandledFailedStage, } from "../runstore/port.js";
|
|
2
|
+
import { isRunClockTerminal } from "../runstore/runStatusClock.js";
|
|
3
|
+
export function resumesRunOnStageRetry(status) {
|
|
4
|
+
return isRunClockTerminal(status);
|
|
5
|
+
}
|
|
6
|
+
export function canCancelRun(status) {
|
|
7
|
+
return status !== "succeeded" && status !== "failed";
|
|
8
|
+
}
|
|
9
|
+
export function canAbandonStage(stageStatus) {
|
|
10
|
+
return stageStatus === "running";
|
|
11
|
+
}
|
|
12
|
+
export function succeededStageRetryBlocker(stageSnap) {
|
|
13
|
+
if (stageSnap.definition_id !== undefined &&
|
|
14
|
+
stageSnap.definition_id !== stageSnap.stage_id) {
|
|
15
|
+
return `Cannot re-run a succeeded clone instance stage (stage=${stageSnap.stage_id}, definition=${stageSnap.definition_id}): clone instances are created by upstream fan-out and cannot be rebuilt in place. Use rerun to start a new run.`;
|
|
16
|
+
}
|
|
17
|
+
if (stageSnap.envelope?.fork_choice !== undefined) {
|
|
18
|
+
return `Cannot re-run a succeeded fork stage (stage=${stageSnap.stage_id}): its envelope recorded a fork_choice and skipped branches cannot be restored. Use rerun to start a new run.`;
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
export function stageRetryBlocker(input) {
|
|
23
|
+
const { stage, runStatus } = input;
|
|
24
|
+
if (stage.status === "waiting_for_input") {
|
|
25
|
+
return `Stage is waiting for input and cannot be retried`;
|
|
26
|
+
}
|
|
27
|
+
const recoveryActive = input.recoveryActive === true;
|
|
28
|
+
if (!resumesRunOnStageRetry(runStatus)) {
|
|
29
|
+
const stages = input.stages ?? [];
|
|
30
|
+
const acceptedPendingRetry = runStatus === "running" &&
|
|
31
|
+
stage.status === "failed" &&
|
|
32
|
+
findUnhandledFailedStage(stages, input.dag) === undefined &&
|
|
33
|
+
stages.some((s) => s.status === "pending");
|
|
34
|
+
if (!(runStatus === "running" && recoveryActive) && !acceptedPendingRetry) {
|
|
35
|
+
return `Run is not failed or succeeded (status=${runStatus})`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (stage.status !== "failed" && stage.status !== "succeeded") {
|
|
39
|
+
return `Stage is not failed or succeeded (status=${stage.status})`;
|
|
40
|
+
}
|
|
41
|
+
if (stage.status === "succeeded") {
|
|
42
|
+
return succeededStageRetryBlocker(stage);
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
export function canRetryStage(input) {
|
|
47
|
+
return stageRetryBlocker(input) === undefined;
|
|
48
|
+
}
|
|
49
|
+
export function runActionEligibility(input) {
|
|
50
|
+
const blockers = [];
|
|
51
|
+
const canCancel = canCancelRun(input.runStatus);
|
|
52
|
+
if (!canCancel) {
|
|
53
|
+
blockers.push(`Run is ${input.runStatus} and cannot be cancelled`);
|
|
54
|
+
}
|
|
55
|
+
const canAbandon = input.stage !== undefined && canAbandonStage(input.stage.status);
|
|
56
|
+
if (input.stage !== undefined && !canAbandon) {
|
|
57
|
+
blockers.push(`Stage is not running (status=${input.stage.status})`);
|
|
58
|
+
}
|
|
59
|
+
let canRetry = false;
|
|
60
|
+
if (input.stage === undefined) {
|
|
61
|
+
blockers.push("Stage is required to retry");
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
const retryBlocker = stageRetryBlocker({
|
|
65
|
+
runStatus: input.runStatus,
|
|
66
|
+
stage: input.stage,
|
|
67
|
+
stages: input.stages,
|
|
68
|
+
recoveryActive: input.recoveryActive,
|
|
69
|
+
dag: input.dag,
|
|
70
|
+
});
|
|
71
|
+
if (retryBlocker === undefined) {
|
|
72
|
+
canRetry = true;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
blockers.push(retryBlocker);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { canRetry, canCancel, canAbandon, blockers };
|
|
79
|
+
}
|
|
80
|
+
export function assertStageRetryEligible(detail, stageId, opts) {
|
|
81
|
+
const stageSnap = detail.stages.find((s) => s.stage_id === stageId);
|
|
82
|
+
if (!stageSnap) {
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
reason: `Stage not found: ${stageId}`,
|
|
86
|
+
status: 404,
|
|
87
|
+
code: "stage_not_found",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const runStatus = opts?.persistedStatus ?? detail.status;
|
|
91
|
+
const blocker = stageRetryBlocker({
|
|
92
|
+
runStatus,
|
|
93
|
+
stage: stageSnap,
|
|
94
|
+
stages: detail.stages,
|
|
95
|
+
recoveryActive: opts?.recoveryActive,
|
|
96
|
+
dag: opts?.dag,
|
|
97
|
+
});
|
|
98
|
+
if (blocker === undefined) {
|
|
99
|
+
return { ok: true };
|
|
100
|
+
}
|
|
101
|
+
if (blocker.includes("waiting for input")) {
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
reason: blocker,
|
|
105
|
+
status: 409,
|
|
106
|
+
code: "hitl_not_retriable",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (blocker.startsWith("Stage is not failed or succeeded")) {
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
reason: blocker,
|
|
113
|
+
status: 409,
|
|
114
|
+
code: "stage_not_failed",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
ok: false,
|
|
119
|
+
reason: blocker,
|
|
120
|
+
status: 409,
|
|
121
|
+
code: "run_not_retryable",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -285,6 +285,7 @@ export declare class RunManager {
|
|
|
285
285
|
private notifyGateClosed;
|
|
286
286
|
abandonStage(runId: string, stageId: string): Promise<AbandonStageResult>;
|
|
287
287
|
cancelRun(runId: string, reason: string): Promise<CancelRunResult>;
|
|
288
|
+
private interruptLiveStages;
|
|
288
289
|
setA2aStore(store: A2aStore): void;
|
|
289
290
|
deleteRun(runId: string, options?: {
|
|
290
291
|
force?: boolean;
|
|
@@ -321,7 +322,9 @@ export declare class RunManager {
|
|
|
321
322
|
pinned?: boolean;
|
|
322
323
|
callerId?: string | null;
|
|
323
324
|
}): Promise<StartRunResult>;
|
|
324
|
-
retryStage(runId: string, stageId: string
|
|
325
|
+
retryStage(runId: string, stageId: string, options?: {
|
|
326
|
+
awaitRoot?: boolean;
|
|
327
|
+
}): Promise<RetryStageResult>;
|
|
325
328
|
resumeTimedOutStage(runId: string, stageId: string, options?: {
|
|
326
329
|
source?: "explicit" | "auto";
|
|
327
330
|
}): Promise<RetryStageResult>;
|
|
@@ -988,7 +988,16 @@ export class RunManager {
|
|
|
988
988
|
catch {
|
|
989
989
|
return { ok: false, reason: `Run not found: ${runId}`, status: 404 };
|
|
990
990
|
}
|
|
991
|
+
const trimmedReason = reason.trim();
|
|
992
|
+
if (trimmedReason.length === 0) {
|
|
993
|
+
return {
|
|
994
|
+
ok: false,
|
|
995
|
+
reason: "Cancel reason is required",
|
|
996
|
+
status: 400,
|
|
997
|
+
};
|
|
998
|
+
}
|
|
991
999
|
if (meta.status === "cancelled") {
|
|
1000
|
+
await this.interruptLiveStages(runId);
|
|
992
1001
|
return { ok: true, runId };
|
|
993
1002
|
}
|
|
994
1003
|
if (meta.status === "succeeded" || meta.status === "failed") {
|
|
@@ -998,14 +1007,6 @@ export class RunManager {
|
|
|
998
1007
|
status: 409,
|
|
999
1008
|
};
|
|
1000
1009
|
}
|
|
1001
|
-
const trimmedReason = reason.trim();
|
|
1002
|
-
if (trimmedReason.length === 0) {
|
|
1003
|
-
return {
|
|
1004
|
-
ok: false,
|
|
1005
|
-
reason: "Cancel reason is required",
|
|
1006
|
-
status: 400,
|
|
1007
|
-
};
|
|
1008
|
-
}
|
|
1009
1010
|
await this.options.store.updateRunStatus(runId, "cancelled");
|
|
1010
1011
|
this.notifyGateClosed(runId);
|
|
1011
1012
|
await this.options.store.setCancelReason(runId, trimmedReason);
|
|
@@ -1056,6 +1057,39 @@ export class RunManager {
|
|
|
1056
1057
|
}
|
|
1057
1058
|
return { ok: true, runId };
|
|
1058
1059
|
}
|
|
1060
|
+
async interruptLiveStages(runId) {
|
|
1061
|
+
const halt = this.schedulingHalts.get(runId);
|
|
1062
|
+
if (halt !== undefined) {
|
|
1063
|
+
halt.halted = true;
|
|
1064
|
+
}
|
|
1065
|
+
if (this.stageProcessLauncher !== undefined) {
|
|
1066
|
+
await this.stageProcessLauncher.cancelRun(runId);
|
|
1067
|
+
}
|
|
1068
|
+
const detail = await this.options.store.readRun(runId);
|
|
1069
|
+
for (const stage of detail.stages) {
|
|
1070
|
+
if (stage.status !== "running" && stage.status !== "waiting_for_input") {
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
1073
|
+
const wasWaiting = stage.status === "waiting_for_input";
|
|
1074
|
+
await markStageInterrupted({
|
|
1075
|
+
store: this.options.store,
|
|
1076
|
+
runId,
|
|
1077
|
+
stageId: stage.stage_id,
|
|
1078
|
+
reason: OPERATOR_CANCEL_REASON,
|
|
1079
|
+
status: "failed",
|
|
1080
|
+
});
|
|
1081
|
+
if (this.retryCoordinator.isActive(runId) &&
|
|
1082
|
+
this.retryCoordinator.getActiveRoots(runId)?.has(stage.stage_id)) {
|
|
1083
|
+
this.retryCoordinator.signalExternalRetryRootTerminal(runId, stage.stage_id);
|
|
1084
|
+
}
|
|
1085
|
+
if (wasWaiting) {
|
|
1086
|
+
this.hitl.clearLiveWait(runId, stage.stage_id);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
if (this.active.has(runId)) {
|
|
1090
|
+
this.removeActiveEntry(runId, false);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1059
1093
|
setA2aStore(store) {
|
|
1060
1094
|
this.a2aStore = store;
|
|
1061
1095
|
}
|
|
@@ -1357,8 +1391,8 @@ export class RunManager {
|
|
|
1357
1391
|
}
|
|
1358
1392
|
return this.reserveAndStartPipeline(taskYaml, pipeline, `run ${runId} task`, rerunCwd, undefined, skipGates, ciIdentity, rerunProjectRoot, undefined, undefined, pinned, options?.callerId);
|
|
1359
1393
|
}
|
|
1360
|
-
async retryStage(runId, stageId) {
|
|
1361
|
-
return this.retryStageInternal(runId, stageId,
|
|
1394
|
+
async retryStage(runId, stageId, options) {
|
|
1395
|
+
return this.retryStageInternal(runId, stageId, options?.awaitRoot !== false);
|
|
1362
1396
|
}
|
|
1363
1397
|
async resumeTimedOutStage(runId, stageId, options) {
|
|
1364
1398
|
if (!this.acceptingWork) {
|
|
@@ -1622,6 +1656,14 @@ export class RunManager {
|
|
|
1622
1656
|
if (!result.ok || awaitRoot !== false) {
|
|
1623
1657
|
this.retryInFlight.delete(retryKey);
|
|
1624
1658
|
}
|
|
1659
|
+
else if (result.done !== undefined) {
|
|
1660
|
+
void result.done.finally(() => {
|
|
1661
|
+
this.retryInFlight.delete(retryKey);
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
else {
|
|
1665
|
+
this.retryInFlight.delete(retryKey);
|
|
1666
|
+
}
|
|
1625
1667
|
return result;
|
|
1626
1668
|
}
|
|
1627
1669
|
catch (err) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { StagePort } from "../agent/port.js";
|
|
2
|
-
import
|
|
2
|
+
import type { RunStatus, RunStore } from "../runstore/port.js";
|
|
3
3
|
import type { PipelineRunResult } from "./pipelineRunner.js";
|
|
4
4
|
import { type SchedulerPreparedPipeline } from "./pipelineScheduler.js";
|
|
5
5
|
import type { StageExecutionMode } from "./stageConcurrency.js";
|
|
@@ -7,6 +7,7 @@ import type { StageBrowserSupport } from "../browser/browserHost.js";
|
|
|
7
7
|
import type { StageProcessLauncher } from "./stageProcessLauncher.js";
|
|
8
8
|
import type { StageHitlController } from "./stageHitl.js";
|
|
9
9
|
import type { OperatorCatalog } from "./stageAttemptBootstrap.js";
|
|
10
|
+
export { assertStageRetryEligible, resumesRunOnStageRetry, succeededStageRetryBlocker, } from "./runActionEligibility.js";
|
|
10
11
|
export declare class DuplicateRetryRootError extends Error {
|
|
11
12
|
constructor(runId: string, stageId: string);
|
|
12
13
|
}
|
|
@@ -69,19 +70,6 @@ export type RetryStageRequest = {
|
|
|
69
70
|
awaitRoot?: boolean;
|
|
70
71
|
beforeAttemptStart?: (attempt: number) => Promise<void>;
|
|
71
72
|
};
|
|
72
|
-
export declare function succeededStageRetryBlocker(stageSnap: StageSnapshot): string | undefined;
|
|
73
|
-
export declare function assertStageRetryEligible(detail: RunDetail, stageId: string, opts?: {
|
|
74
|
-
recoveryActive?: boolean;
|
|
75
|
-
persistedStatus?: RunStatus;
|
|
76
|
-
dag?: Pick<RunPipelineDagSnapshot, "nodes"> | null;
|
|
77
|
-
}): {
|
|
78
|
-
ok: true;
|
|
79
|
-
} | {
|
|
80
|
-
ok: false;
|
|
81
|
-
reason: string;
|
|
82
|
-
status: number;
|
|
83
|
-
code: string;
|
|
84
|
-
};
|
|
85
73
|
export declare function readRetryRootWaitTimeoutMs(): number;
|
|
86
74
|
export declare class RunRetryCoordinator {
|
|
87
75
|
private readonly sessions;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { findUnhandledFailedStage, } from "../runstore/port.js";
|
|
2
1
|
import { retryRun, } from "./pipelineScheduler.js";
|
|
3
2
|
import { loadRunContext } from "./resumeReconstruct.js";
|
|
4
3
|
import { syncRunStatusFromStages } from "./stageRecovery.js";
|
|
4
|
+
import { assertStageRetryEligible, resumesRunOnStageRetry, } from "./runActionEligibility.js";
|
|
5
|
+
export { assertStageRetryEligible, resumesRunOnStageRetry, succeededStageRetryBlocker, } from "./runActionEligibility.js";
|
|
5
6
|
export class DuplicateRetryRootError extends Error {
|
|
6
7
|
constructor(runId, stageId) {
|
|
7
8
|
super(`Retry already in progress for run ${runId} stage ${stageId}`);
|
|
@@ -20,71 +21,6 @@ export class RetryRootWaitTimeoutError extends Error {
|
|
|
20
21
|
this.name = "RetryRootWaitTimeoutError";
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
|
-
export function succeededStageRetryBlocker(stageSnap) {
|
|
24
|
-
if (stageSnap.definition_id !== undefined &&
|
|
25
|
-
stageSnap.definition_id !== stageSnap.stage_id) {
|
|
26
|
-
return `Cannot re-run a succeeded clone instance stage (stage=${stageSnap.stage_id}, definition=${stageSnap.definition_id}): clone instances are created by upstream fan-out and cannot be rebuilt in place. Use rerun to start a new run.`;
|
|
27
|
-
}
|
|
28
|
-
if (stageSnap.envelope?.fork_choice !== undefined) {
|
|
29
|
-
return `Cannot re-run a succeeded fork stage (stage=${stageSnap.stage_id}): its envelope recorded a fork_choice and skipped branches cannot be restored. Use rerun to start a new run.`;
|
|
30
|
-
}
|
|
31
|
-
return undefined;
|
|
32
|
-
}
|
|
33
|
-
export function assertStageRetryEligible(detail, stageId, opts) {
|
|
34
|
-
const stageSnap = detail.stages.find((s) => s.stage_id === stageId);
|
|
35
|
-
if (!stageSnap) {
|
|
36
|
-
return {
|
|
37
|
-
ok: false,
|
|
38
|
-
reason: `Stage not found: ${stageId}`,
|
|
39
|
-
status: 404,
|
|
40
|
-
code: "stage_not_found",
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
if (stageSnap.status === "waiting_for_input") {
|
|
44
|
-
return {
|
|
45
|
-
ok: false,
|
|
46
|
-
reason: `Stage is waiting for input and cannot be retried`,
|
|
47
|
-
status: 409,
|
|
48
|
-
code: "hitl_not_retriable",
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
const runStatus = opts?.persistedStatus ?? detail.status;
|
|
52
|
-
const recoveryActive = opts?.recoveryActive === true;
|
|
53
|
-
if (runStatus !== "failed" && runStatus !== "succeeded") {
|
|
54
|
-
const acceptedPendingRetry = runStatus === "running" &&
|
|
55
|
-
stageSnap.status === "failed" &&
|
|
56
|
-
findUnhandledFailedStage(detail.stages, opts?.dag) === undefined &&
|
|
57
|
-
detail.stages.some((stage) => stage.status === "pending");
|
|
58
|
-
if (!(runStatus === "running" && recoveryActive) && !acceptedPendingRetry) {
|
|
59
|
-
return {
|
|
60
|
-
ok: false,
|
|
61
|
-
reason: `Run is not failed or succeeded (status=${runStatus})`,
|
|
62
|
-
status: 409,
|
|
63
|
-
code: "run_not_retryable",
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
if (stageSnap.status !== "failed" && stageSnap.status !== "succeeded") {
|
|
68
|
-
return {
|
|
69
|
-
ok: false,
|
|
70
|
-
reason: `Stage is not failed or succeeded (status=${stageSnap.status})`,
|
|
71
|
-
status: 409,
|
|
72
|
-
code: "stage_not_failed",
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
if (stageSnap.status === "succeeded") {
|
|
76
|
-
const blocker = succeededStageRetryBlocker(stageSnap);
|
|
77
|
-
if (blocker !== undefined) {
|
|
78
|
-
return {
|
|
79
|
-
ok: false,
|
|
80
|
-
reason: blocker,
|
|
81
|
-
status: 409,
|
|
82
|
-
code: "run_not_retryable",
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return { ok: true };
|
|
87
|
-
}
|
|
88
24
|
function mutationKey(stageId, attempt) {
|
|
89
25
|
return `${stageId}\0${attempt}`;
|
|
90
26
|
}
|
|
@@ -318,7 +254,7 @@ export class RunRetryCoordinator {
|
|
|
318
254
|
};
|
|
319
255
|
}
|
|
320
256
|
insertedForResume = tracked.insertedForResume === true;
|
|
321
|
-
if (priorRunStatus
|
|
257
|
+
if (resumesRunOnStageRetry(priorRunStatus)) {
|
|
322
258
|
await store.updateRunStatus(runId, "running");
|
|
323
259
|
bumpedRunning = true;
|
|
324
260
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export declare const SANDBOX_CONTAINER_IMAGE_ENV = "STAGEFLOW_STAGE_CONTAINER_IMAGE";
|
|
2
2
|
export declare const SANDBOX_CONTAINER_DOCKER_BIN_ENV = "STAGEFLOW_STAGE_CONTAINER_DOCKER_BIN";
|
|
3
|
+
/** One host directory bind-mounted read-only into every stage sandbox, at the same path. */
|
|
4
|
+
export declare const STAGE_READONLY_DIR_ENV = "STAGEFLOW_STAGE_READONLY_DIR";
|
|
3
5
|
/** Docker container names allow only `[a-zA-Z0-9_.-]`. */
|
|
4
6
|
export declare function sanitizeContainerNameSegment(value: string): string;
|
|
5
7
|
export declare function buildContainerName(input: {
|
|
@@ -18,9 +20,13 @@ export declare function resolveSandboxContainerOptions(env: Record<string, strin
|
|
|
18
20
|
/** Fixed path inside the sandbox. The daemon owns the volume; we never pass it a path from this process. */
|
|
19
21
|
export declare const SANDBOX_MOUNT_PATH = "/workspace";
|
|
20
22
|
export declare function sandboxVolumeName(containerName: string): string;
|
|
23
|
+
export declare function stageReadOnlyDir(env: Record<string, string | undefined>): string | undefined;
|
|
24
|
+
export declare function assertStageReadOnlyDir(raw: string): string;
|
|
25
|
+
export declare function assertStageReadOnlyDirExists(dir: string): Promise<void>;
|
|
21
26
|
export declare function buildStartContainerArgs(params: {
|
|
22
27
|
image: string;
|
|
23
28
|
containerName: string;
|
|
29
|
+
readOnlyDir?: string;
|
|
24
30
|
}): string[];
|
|
25
31
|
export declare function startSandboxContainer(params: {
|
|
26
32
|
image: string;
|
|
@@ -28,6 +34,7 @@ export declare function startSandboxContainer(params: {
|
|
|
28
34
|
stageId: string;
|
|
29
35
|
attempt?: number;
|
|
30
36
|
dockerBin?: string;
|
|
37
|
+
readOnlyDir?: string;
|
|
31
38
|
}): Promise<{
|
|
32
39
|
containerName: string;
|
|
33
40
|
}>;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { stat } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
3
5
|
import { promisify } from "node:util";
|
|
4
6
|
export const SANDBOX_CONTAINER_IMAGE_ENV = "STAGEFLOW_STAGE_CONTAINER_IMAGE";
|
|
5
7
|
export const SANDBOX_CONTAINER_DOCKER_BIN_ENV = "STAGEFLOW_STAGE_CONTAINER_DOCKER_BIN";
|
|
8
|
+
/** One host directory bind-mounted read-only into every stage sandbox, at the same path. */
|
|
9
|
+
export const STAGE_READONLY_DIR_ENV = "STAGEFLOW_STAGE_READONLY_DIR";
|
|
6
10
|
const execFileAsync = promisify(execFile);
|
|
7
11
|
/** Docker container names allow only `[a-zA-Z0-9_.-]`. */
|
|
8
12
|
export function sanitizeContainerNameSegment(value) {
|
|
@@ -35,21 +39,55 @@ export function sandboxVolumeName(containerName) {
|
|
|
35
39
|
const safe = containerName.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
36
40
|
return `sf-bash-${safe}`;
|
|
37
41
|
}
|
|
42
|
+
export function stageReadOnlyDir(env) {
|
|
43
|
+
const raw = env[STAGE_READONLY_DIR_ENV];
|
|
44
|
+
if (raw === undefined || raw.trim() === "")
|
|
45
|
+
return undefined;
|
|
46
|
+
return assertStageReadOnlyDir(raw);
|
|
47
|
+
}
|
|
48
|
+
export function assertStageReadOnlyDir(raw) {
|
|
49
|
+
const trimmed = raw.trim();
|
|
50
|
+
if (!path.isAbsolute(trimmed)) {
|
|
51
|
+
throw new Error(`${STAGE_READONLY_DIR_ENV} must be an absolute path`);
|
|
52
|
+
}
|
|
53
|
+
if (trimmed.includes("\0") || trimmed.includes(":")) {
|
|
54
|
+
throw new Error(`${STAGE_READONLY_DIR_ENV} contains a character docker cannot mount`);
|
|
55
|
+
}
|
|
56
|
+
const resolved = path.resolve(trimmed);
|
|
57
|
+
const parts = resolved.split(path.sep).filter((part) => part.length > 0);
|
|
58
|
+
if (parts.length < 3) {
|
|
59
|
+
throw new Error(`${STAGE_READONLY_DIR_ENV} is too broad`);
|
|
60
|
+
}
|
|
61
|
+
return resolved;
|
|
62
|
+
}
|
|
63
|
+
export async function assertStageReadOnlyDirExists(dir) {
|
|
64
|
+
let info;
|
|
65
|
+
try {
|
|
66
|
+
info = await stat(dir);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
throw new Error(`read-only folder is not available: ${dir}`);
|
|
70
|
+
}
|
|
71
|
+
if (!info.isDirectory()) {
|
|
72
|
+
throw new Error(`read-only folder is not a directory: ${dir}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
38
75
|
export function buildStartContainerArgs(params) {
|
|
39
76
|
const { image, containerName } = params;
|
|
40
|
-
|
|
77
|
+
const args = [
|
|
41
78
|
"run",
|
|
42
79
|
"-d",
|
|
43
80
|
"--name",
|
|
44
81
|
containerName,
|
|
45
82
|
"-v",
|
|
46
83
|
`${sandboxVolumeName(containerName)}:${SANDBOX_MOUNT_PATH}`,
|
|
47
|
-
"-w",
|
|
48
|
-
SANDBOX_MOUNT_PATH,
|
|
49
|
-
image,
|
|
50
|
-
"sleep",
|
|
51
|
-
"infinity",
|
|
52
84
|
];
|
|
85
|
+
if (params.readOnlyDir !== undefined) {
|
|
86
|
+
const dir = assertStageReadOnlyDir(params.readOnlyDir);
|
|
87
|
+
args.push("-v", `${dir}:${dir}:ro`);
|
|
88
|
+
}
|
|
89
|
+
args.push("-w", SANDBOX_MOUNT_PATH, image, "sleep", "infinity");
|
|
90
|
+
return args;
|
|
53
91
|
}
|
|
54
92
|
function errorMessage(err) {
|
|
55
93
|
if (err && typeof err === "object" && "stderr" in err) {
|
|
@@ -61,9 +99,13 @@ function errorMessage(err) {
|
|
|
61
99
|
}
|
|
62
100
|
export async function startSandboxContainer(params) {
|
|
63
101
|
const containerName = buildContainerName({ runId: params.runId, stageId: params.stageId, attempt: params.attempt }, randomUUID().slice(0, 8));
|
|
102
|
+
if (params.readOnlyDir !== undefined) {
|
|
103
|
+
await assertStageReadOnlyDirExists(params.readOnlyDir);
|
|
104
|
+
}
|
|
64
105
|
const args = buildStartContainerArgs({
|
|
65
106
|
image: params.image,
|
|
66
107
|
containerName,
|
|
108
|
+
...(params.readOnlyDir !== undefined ? { readOnlyDir: params.readOnlyDir } : {}),
|
|
67
109
|
});
|
|
68
110
|
try {
|
|
69
111
|
await execFileAsync(params.dockerBin ?? "docker", args);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { StageArtifactGrant } from "./stageRoots.js";
|
|
1
2
|
export type SandboxDeskRole = "artifacts" | "envelope" | "checkout" | "grant" | "work";
|
|
2
3
|
export type SandboxDeskBinding = {
|
|
3
4
|
role: SandboxDeskRole;
|
|
@@ -14,7 +15,7 @@ export type SandboxDesk = {
|
|
|
14
15
|
export declare function buildSandboxDesk(input: {
|
|
15
16
|
attemptDir: string;
|
|
16
17
|
checkoutRoot?: string;
|
|
17
|
-
|
|
18
|
+
grants?: readonly StageArtifactGrant[];
|
|
18
19
|
}): SandboxDesk;
|
|
19
20
|
export declare function populateSandboxDesk(params: {
|
|
20
21
|
containerName: string;
|