stageflow 0.36.0 → 0.40.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 +61 -26
- package/dist/agent/claudeAdapter.js +21 -1
- package/dist/agent/claudeTools.d.ts +13 -0
- package/dist/agent/claudeTools.js +98 -0
- package/dist/agent/piAdapter.d.ts +9 -1
- package/dist/agent/piAdapter.js +59 -18
- package/dist/agent/piIsolatedMcp.js +14 -2
- package/dist/agent/piIsolatedMcpProbe.d.ts +5 -1
- package/dist/agent/piIsolatedMcpProbe.js +16 -30
- package/dist/agent/port.d.ts +5 -0
- package/dist/agent/sandboxPiTools.d.ts +99 -0
- package/dist/agent/sandboxPiTools.js +109 -0
- package/dist/agents/agentId.d.ts +8 -0
- package/dist/agents/agentId.js +27 -0
- package/dist/agents/approvalBroker.d.ts +16 -0
- package/dist/agents/approvalBroker.js +45 -0
- package/dist/agents/authorChat.d.ts +19 -0
- package/dist/agents/authorChat.js +63 -0
- package/dist/agents/chatTurn.d.ts +23 -0
- package/dist/agents/chatTurn.js +77 -0
- package/dist/agents/communications/agentmail.d.ts +19 -0
- package/dist/agents/communications/agentmail.js +46 -0
- package/dist/agents/communications/agentmailListener.d.ts +59 -0
- package/dist/agents/communications/agentmailListener.js +183 -0
- package/dist/agents/communications/allowlist.d.ts +10 -0
- package/dist/agents/communications/allowlist.js +36 -0
- package/dist/agents/communications/channelConnections.d.ts +3 -0
- package/dist/agents/communications/channelConnections.js +51 -0
- package/dist/agents/communications/delivery.d.ts +53 -0
- package/dist/agents/communications/delivery.js +142 -0
- package/dist/agents/communications/email.d.ts +26 -0
- package/dist/agents/communications/email.js +20 -0
- package/dist/agents/communications/identity.d.ts +13 -0
- package/dist/agents/communications/identity.js +18 -0
- package/dist/agents/communications/imessageListener.d.ts +41 -0
- package/dist/agents/communications/imessageListener.js +92 -0
- package/dist/agents/communications/inbound.d.ts +63 -0
- package/dist/agents/communications/inbound.js +143 -0
- package/dist/agents/communications/listener.d.ts +36 -0
- package/dist/agents/communications/listener.js +38 -0
- package/dist/agents/communications/photon.d.ts +19 -0
- package/dist/agents/communications/photon.js +38 -0
- package/dist/agents/communications/port.d.ts +27 -0
- package/dist/agents/communications/port.js +6 -0
- package/dist/agents/communications/slack.d.ts +28 -0
- package/dist/agents/communications/slack.js +65 -0
- package/dist/agents/communications/supervisor.d.ts +36 -0
- package/dist/agents/communications/supervisor.js +96 -0
- package/dist/agents/communications/telegram.d.ts +19 -0
- package/dist/agents/communications/telegram.js +52 -0
- package/dist/agents/definitionSummary.d.ts +13 -0
- package/dist/agents/definitionSummary.js +91 -0
- package/dist/agents/delegate.d.ts +3 -0
- package/dist/agents/delegate.js +8 -0
- package/dist/agents/errors.d.ts +10 -0
- package/dist/agents/errors.js +32 -0
- package/dist/agents/guardrails.d.ts +24 -0
- package/dist/agents/guardrails.js +39 -0
- package/dist/agents/historyFit.d.ts +10 -0
- package/dist/agents/historyFit.js +26 -0
- package/dist/agents/hostContext.d.ts +54 -0
- package/dist/agents/hostContext.js +156 -0
- package/dist/agents/ledgers.d.ts +29 -0
- package/dist/agents/ledgers.js +31 -0
- package/dist/agents/lineDiff.d.ts +12 -0
- package/dist/agents/lineDiff.js +46 -0
- package/dist/agents/ndjson.d.ts +9 -0
- package/dist/agents/ndjson.js +47 -0
- package/dist/agents/otlpExport.d.ts +9 -0
- package/dist/agents/otlpExport.js +58 -0
- package/dist/agents/otlpSink.d.ts +7 -0
- package/dist/agents/otlpSink.js +23 -0
- package/dist/agents/policy.d.ts +31 -0
- package/dist/agents/policy.js +84 -0
- package/dist/agents/projectRoot.d.ts +5 -0
- package/dist/agents/projectRoot.js +15 -0
- package/dist/agents/runtime/piRuntime.d.ts +52 -0
- package/dist/agents/runtime/piRuntime.js +474 -0
- package/dist/agents/runtime/types.d.ts +77 -0
- package/dist/agents/runtime/types.js +26 -0
- package/dist/agents/sandbox.d.ts +17 -0
- package/dist/agents/sandbox.js +10 -0
- package/dist/agents/scheduleSource.d.ts +32 -0
- package/dist/agents/scheduleSource.js +100 -0
- package/dist/agents/service.d.ts +298 -0
- package/dist/agents/service.js +1105 -0
- package/dist/agents/sessions.d.ts +38 -0
- package/dist/agents/sessions.js +321 -0
- package/dist/agents/store.d.ts +277 -0
- package/dist/agents/store.js +643 -0
- package/dist/agents/trace.d.ts +27 -0
- package/dist/agents/trace.js +29 -0
- package/dist/agents/turnLifecycle.d.ts +72 -0
- package/dist/agents/turnLifecycle.js +157 -0
- package/dist/agents/webChat/publicChat.d.ts +36 -0
- package/dist/agents/webChat/publicChat.js +290 -0
- package/dist/agents/webChat/rateLimit.d.ts +15 -0
- package/dist/agents/webChat/rateLimit.js +33 -0
- package/dist/agents/webChat/slug.d.ts +2 -0
- package/dist/agents/webChat/slug.js +9 -0
- package/dist/channels/providers/agentmail.js +14 -1
- package/dist/channels/providers/photonImessage.js +36 -0
- package/dist/channels/providers/slack.js +2 -1
- package/dist/channels/providers/telegram.js +2 -1
- package/dist/channels/types.d.ts +27 -0
- package/dist/cli/agentsCommand.d.ts +14 -0
- package/dist/cli/agentsCommand.js +291 -0
- package/dist/cli.js +9 -0
- package/dist/config/browseCatalog.js +4 -11
- package/dist/config/draftPackage.d.ts +1 -0
- package/dist/config/draftPackage.js +4 -0
- package/dist/config/hostConfig.js +9 -0
- package/dist/config/loadAgent.d.ts +266 -0
- package/dist/config/loadAgent.js +374 -0
- package/dist/config/loadPipeline.js +5 -2
- package/dist/config/loadTask.js +34 -1
- package/dist/config/mcpHostCatalog.d.ts +50 -0
- package/dist/config/mcpHostCatalog.js +405 -0
- package/dist/config/mcpOAuth.d.ts +37 -0
- package/dist/config/mcpOAuth.js +263 -0
- package/dist/config/mcpRedaction.d.ts +42 -0
- package/dist/config/mcpRedaction.js +314 -0
- package/dist/config/mcpToolCache.d.ts +20 -0
- package/dist/config/mcpToolCache.js +43 -0
- package/dist/config/mergePipelineIncludes.d.ts +2 -0
- package/dist/config/mergePipelineIncludes.js +25 -0
- package/dist/config/resolveStageMcpServers.d.ts +35 -3
- package/dist/config/resolveStageMcpServers.js +124 -9
- package/dist/config/validateCatalog.d.ts +1 -1
- package/dist/config/validateCatalog.js +22 -1
- package/dist/mcp/agentTools.d.ts +8 -0
- package/dist/mcp/agentTools.js +132 -0
- package/dist/mcp/catalogTools.js +17 -10
- package/dist/mcp/deps.d.ts +6 -0
- package/dist/mcp/pipelineAuthorSupport.d.ts +19 -0
- package/dist/mcp/pipelineAuthorSupport.js +10 -0
- package/dist/mcp/pipelineAuthorTools.d.ts +8 -0
- package/dist/mcp/pipelineAuthorTools.js +109 -0
- package/dist/mcp/server.js +5 -0
- package/dist/mcp/tools.js +4 -0
- package/dist/mcp/triggerTools.js +32 -0
- package/dist/memory/access.d.ts +36 -0
- package/dist/memory/access.js +116 -0
- package/dist/memory/agentIssues.d.ts +3 -0
- package/dist/memory/agentIssues.js +23 -0
- package/dist/memory/bashDeny.d.ts +6 -0
- package/dist/memory/bashDeny.js +35 -0
- package/dist/memory/host.d.ts +51 -0
- package/dist/memory/host.js +394 -0
- package/dist/memory/ids.d.ts +15 -0
- package/dist/memory/ids.js +31 -0
- package/dist/memory/worker.d.ts +1 -0
- package/dist/memory/worker.js +84 -0
- package/dist/models/catalog.d.ts +113 -0
- package/dist/models/catalog.js +238 -0
- package/dist/models/response.d.ts +29 -0
- package/dist/models/response.js +25 -0
- package/dist/operatorAgent/agentAuthorContext.d.ts +17 -0
- package/dist/operatorAgent/agentAuthorContext.js +32 -0
- package/dist/operatorAgent/fakeHost.js +2 -0
- package/dist/operatorAgent/host.js +6 -3
- package/dist/operatorAgent/index.d.ts +4 -0
- package/dist/operatorAgent/index.js +4 -0
- package/dist/operatorAgent/piModel.js +2 -0
- package/dist/operatorAgent/profiles/agentAuthor.d.ts +11 -0
- package/dist/operatorAgent/profiles/agentAuthor.js +192 -0
- package/dist/operatorAgent/profiles/agentAuthorPlaybook.d.ts +1 -0
- package/dist/operatorAgent/profiles/agentAuthorPlaybook.js +29 -0
- package/dist/operatorAgent/profiles/workshopAuthor.js +2 -2
- package/dist/operatorAgent/profiles/workshopAuthorTools.js +3 -3
- package/dist/operatorAgent/proposals.d.ts +1 -0
- package/dist/operatorAgent/proposals.js +23 -2
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/preflight/pipelinePreflight.js +1 -0
- package/dist/runstore/createStore.d.ts +2 -0
- package/dist/runstore/createStore.js +8 -0
- package/dist/runstore/overviewSummary.d.ts +56 -0
- package/dist/runstore/overviewSummary.js +134 -0
- package/dist/runstore/port.d.ts +3 -0
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +2 -0
- package/dist/runstore/sqlite/SqliteRunStore.js +18 -0
- package/dist/runstore/sqlite/migrations/013-agents.d.ts +12 -0
- package/dist/runstore/sqlite/migrations/013-agents.js +156 -0
- package/dist/runstore/sqlite/migrations/014-agent-deliveries.d.ts +11 -0
- package/dist/runstore/sqlite/migrations/014-agent-deliveries.js +32 -0
- package/dist/runstore/sqlite/migrations/index.js +4 -0
- package/dist/runtime/gitCheckoutCapability.d.ts +1 -0
- package/dist/runtime/gitCheckoutCapability.js +1 -1
- package/dist/runtime/gitWorktreeCheckout.d.ts +22 -0
- package/dist/runtime/gitWorktreeCheckout.js +120 -0
- package/dist/runtime/sandboxContainer.d.ts +62 -0
- package/dist/runtime/sandboxContainer.js +151 -0
- package/dist/runtime/sandboxDesk.d.ts +28 -0
- package/dist/runtime/sandboxDesk.js +242 -0
- package/dist/runtime/sandboxFiles.d.ts +20 -0
- package/dist/runtime/sandboxFiles.js +150 -0
- package/dist/runtime/stageAttemptBootstrap.js +15 -1
- package/dist/runtime/stageEnvironment.d.ts +2 -0
- package/dist/runtime/stageEnvironment.js +8 -1
- package/dist/runtime/stageProcessLauncher.js +23 -0
- package/dist/runtime/stageRoots.d.ts +9 -0
- package/dist/runtime/stageRoots.js +14 -0
- package/dist/runtime/stageRunner.d.ts +5 -0
- package/dist/runtime/stageRunner.js +38 -4
- package/dist/runtime/stageSandbox.d.ts +16 -0
- package/dist/runtime/stageSandbox.js +54 -0
- package/dist/runtime/stageSecrets.d.ts +6 -1
- package/dist/runtime/stageSecrets.js +34 -1
- package/dist/runtime/stageWorker.js +138 -105
- package/dist/runtime/workspaceBinding.d.ts +2 -1
- package/dist/runtime/workspaceBinding.js +20 -4
- package/dist/server/agentAuthorRoutes.d.ts +5 -0
- package/dist/server/agentAuthorRoutes.js +64 -0
- package/dist/server/agentRoutes.d.ts +5 -0
- package/dist/server/agentRoutes.js +323 -0
- package/dist/server/bootstrap.d.ts +8 -0
- package/dist/server/bootstrap.js +36 -10
- package/dist/server/controlToken.js +8 -0
- package/dist/server/http.d.ts +8 -1
- package/dist/server/http.js +172 -20
- package/dist/server/mcpHost.js +1 -0
- package/dist/server/memoryRoutes.d.ts +5 -0
- package/dist/server/memoryRoutes.js +146 -0
- package/dist/server/projectMcpRoutes.d.ts +1 -0
- package/dist/server/projectMcpRoutes.js +241 -3
- package/dist/toolkit/guard.d.ts +27 -0
- package/dist/toolkit/guard.js +98 -0
- package/dist/toolkit/index.d.ts +48 -0
- package/dist/toolkit/index.js +82 -0
- package/dist/toolkit/meta.d.ts +36 -0
- package/dist/toolkit/meta.js +117 -0
- package/dist/toolkit/profiles.d.ts +13 -0
- package/dist/toolkit/profiles.js +25 -0
- package/dist/toolkit/record.d.ts +15 -0
- package/dist/toolkit/record.js +17 -0
- package/dist/tools/memoryTools.d.ts +95 -0
- package/dist/tools/memoryTools.js +179 -0
- package/dist/types/pipeline.d.ts +3 -0
- package/dist/types/stage.d.ts +2 -0
- package/dist/types/task.d.ts +11 -1
- package/dist/ui/assets/AssistantRuntimeProvider-CsiPFhCT.js +9 -0
- package/dist/ui/assets/MarkdownText-D7tYaIFO.js +29 -0
- package/dist/ui/assets/ReadonlyStore-BfXqHykm.js +6 -0
- package/dist/ui/assets/Tooltip-DrpoRRNi.js +1 -0
- package/dist/ui/assets/chat-BbLukq9W.js +5 -0
- package/dist/ui/assets/chat-zIZU_bP1.css +2 -0
- package/dist/ui/assets/jsx-runtime-iVoFAvQ_.js +1 -0
- package/dist/ui/assets/{index-Clv4sJpU.css → main-BFo79krC.css} +1 -1
- package/dist/ui/assets/main-CijYMeM0.js +259 -0
- package/dist/ui/chat.html +25 -0
- package/dist/ui/index.html +6 -2
- package/dist/workshop/modelSettings.d.ts +2 -2
- package/dist/workshop/modelSettings.js +5 -11
- package/examples/README.md +1 -0
- package/examples/channels-smoke/README.md +12 -1
- package/examples/github-release/README.md +1 -1
- package/examples/memory/README.md +101 -0
- package/examples/memory/desk-outage.task.yaml +15 -0
- package/examples/memory/desk-refund.task.yaml +15 -0
- package/examples/memory/desk-revisit-outage.task.yaml +11 -0
- package/examples/memory/desk-revisit.pipeline.yaml +123 -0
- package/examples/memory/desk.pipeline.yaml +192 -0
- package/examples/memory/file-card.pipeline.yaml +39 -0
- package/examples/memory/file-ledger.task.yaml +22 -0
- package/examples/memory/file-release.task.yaml +24 -0
- package/examples/memory/file-rich.pipeline.yaml +40 -0
- package/examples/memory/file-ticket.task.yaml +7 -0
- package/examples/memory/file-timeline.task.yaml +28 -0
- package/examples/memory/find-card.pipeline.yaml +46 -0
- package/examples/memory/find-ticket.task.yaml +7 -0
- package/examples/memory/missing.pipeline.yaml +16 -0
- package/examples/memory/probe-bash.task.yaml +3 -0
- package/examples/memory/recall.pipeline.yaml +24 -0
- package/examples/memory/recall.task.yaml +5 -0
- package/examples/memory/remember-ada.task.yaml +6 -0
- package/examples/memory/remember-grace.task.yaml +6 -0
- package/examples/memory/remember.pipeline.yaml +58 -0
- package/examples/memory/sandbox-memory-bash.pipeline.yaml +28 -0
- package/examples/memory/stageflow.yaml +12 -0
- package/examples/memory/update-card.pipeline.yaml +43 -0
- package/examples/memory/update-ledger.task.yaml +23 -0
- package/examples/memory/update-rich.pipeline.yaml +44 -0
- package/examples/memory/update-ticket.task.yaml +7 -0
- package/examples/ship-feature/README.md +15 -0
- package/examples/stage-mcp/README.md +1 -1
- package/package.json +2 -1
- package/dist/ui/assets/index-BOhqnPNg.js +0 -260
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { copyToSandboxContainer, execInSandboxContainer, SANDBOX_MOUNT_PATH, } from "./sandboxContainer.js";
|
|
5
|
+
export class SandboxPathError extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "SandboxPathError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function shellQuote(value) {
|
|
12
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
13
|
+
}
|
|
14
|
+
/** Resolve a tool path onto the sandbox mount. Host absolutes and `..` escapes are rejected. */
|
|
15
|
+
export function confineSandboxPath(inputPath) {
|
|
16
|
+
if (typeof inputPath !== "string" || inputPath.trim() === "") {
|
|
17
|
+
throw new SandboxPathError("path must be a non-empty string");
|
|
18
|
+
}
|
|
19
|
+
if (inputPath.includes("\0") || inputPath.startsWith("~")) {
|
|
20
|
+
throw new SandboxPathError("path escapes the sandbox");
|
|
21
|
+
}
|
|
22
|
+
const slashes = inputPath.replaceAll("\\", "/");
|
|
23
|
+
const joined = slashes.startsWith("/")
|
|
24
|
+
? path.posix.normalize(slashes)
|
|
25
|
+
: path.posix.normalize(path.posix.join(SANDBOX_MOUNT_PATH, slashes));
|
|
26
|
+
if (joined !== SANDBOX_MOUNT_PATH && !joined.startsWith(`${SANDBOX_MOUNT_PATH}/`)) {
|
|
27
|
+
throw new SandboxPathError("path escapes the sandbox");
|
|
28
|
+
}
|
|
29
|
+
return joined;
|
|
30
|
+
}
|
|
31
|
+
function assertFilePath(containerPath) {
|
|
32
|
+
if (containerPath === SANDBOX_MOUNT_PATH) {
|
|
33
|
+
throw new SandboxPathError("path must name a file under /workspace");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function requireExec(result, fallback) {
|
|
37
|
+
if (result.exitCode === 0)
|
|
38
|
+
return;
|
|
39
|
+
const detail = result.stderr.trim() || result.stdout.trim();
|
|
40
|
+
throw new Error(detail || fallback);
|
|
41
|
+
}
|
|
42
|
+
export async function readSandboxFile(target, inputPath) {
|
|
43
|
+
const containerPath = confineSandboxPath(inputPath);
|
|
44
|
+
assertFilePath(containerPath);
|
|
45
|
+
const quoted = shellQuote(containerPath);
|
|
46
|
+
const result = await execInSandboxContainer({
|
|
47
|
+
containerName: target.containerName,
|
|
48
|
+
dockerBin: target.dockerBin,
|
|
49
|
+
command: `if [ ! -f ${quoted} ]; then echo "No such file: ${containerPath}" >&2; exit 1; fi; base64 < ${quoted}`,
|
|
50
|
+
});
|
|
51
|
+
await requireExec(result, `failed to read ${containerPath}`);
|
|
52
|
+
return Buffer.from(result.stdout, "base64").toString("utf8");
|
|
53
|
+
}
|
|
54
|
+
export async function writeSandboxFile(target, inputPath, content) {
|
|
55
|
+
const containerPath = confineSandboxPath(inputPath);
|
|
56
|
+
assertFilePath(containerPath);
|
|
57
|
+
const parent = path.posix.dirname(containerPath);
|
|
58
|
+
const made = await execInSandboxContainer({
|
|
59
|
+
containerName: target.containerName,
|
|
60
|
+
dockerBin: target.dockerBin,
|
|
61
|
+
command: `mkdir -p -- ${shellQuote(parent)}`,
|
|
62
|
+
});
|
|
63
|
+
await requireExec(made, `failed to write ${containerPath}`);
|
|
64
|
+
const scratch = await mkdtemp(path.join(tmpdir(), "sf-sandbox-write-"));
|
|
65
|
+
const payload = path.join(scratch, "payload");
|
|
66
|
+
try {
|
|
67
|
+
await writeFile(payload, content);
|
|
68
|
+
await copyToSandboxContainer({
|
|
69
|
+
containerName: target.containerName,
|
|
70
|
+
dockerBin: target.dockerBin,
|
|
71
|
+
hostPath: payload,
|
|
72
|
+
containerPath,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
await rm(scratch, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export async function editSandboxFile(target, inputPath, edits) {
|
|
80
|
+
if (edits.length === 0) {
|
|
81
|
+
throw new Error("edits must contain at least one replacement");
|
|
82
|
+
}
|
|
83
|
+
const original = await readSandboxFile(target, inputPath);
|
|
84
|
+
const ranges = [];
|
|
85
|
+
for (const edit of edits) {
|
|
86
|
+
if (edit.oldText.length === 0) {
|
|
87
|
+
throw new Error("oldText must not be empty");
|
|
88
|
+
}
|
|
89
|
+
const count = countOccurrences(original, edit.oldText);
|
|
90
|
+
if (count !== 1) {
|
|
91
|
+
throw new Error(`oldText matched ${count} times`);
|
|
92
|
+
}
|
|
93
|
+
const start = original.indexOf(edit.oldText);
|
|
94
|
+
ranges.push({ start, end: start + edit.oldText.length, newText: edit.newText });
|
|
95
|
+
}
|
|
96
|
+
ranges.sort((a, b) => b.start - a.start);
|
|
97
|
+
for (let i = 0; i < ranges.length; i += 1) {
|
|
98
|
+
const later = ranges[i];
|
|
99
|
+
for (let j = i + 1; j < ranges.length; j += 1) {
|
|
100
|
+
const earlier = ranges[j];
|
|
101
|
+
if (earlier.end > later.start) {
|
|
102
|
+
throw new Error("edits overlap");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
let next = original;
|
|
107
|
+
for (const range of ranges) {
|
|
108
|
+
next = next.slice(0, range.start) + range.newText + next.slice(range.end);
|
|
109
|
+
}
|
|
110
|
+
await writeSandboxFile(target, inputPath, next);
|
|
111
|
+
}
|
|
112
|
+
export function sliceSandboxLines(text, offset, limit) {
|
|
113
|
+
if (offset === undefined && limit === undefined)
|
|
114
|
+
return text;
|
|
115
|
+
const lines = text.split("\n");
|
|
116
|
+
const start = offset !== undefined ? Math.max(0, Math.floor(offset) - 1) : 0;
|
|
117
|
+
if (start >= lines.length) {
|
|
118
|
+
throw new Error(`Offset ${offset} is beyond end of file (${lines.length} lines total)`);
|
|
119
|
+
}
|
|
120
|
+
const end = limit !== undefined ? start + Math.max(0, Math.floor(limit)) : lines.length;
|
|
121
|
+
return lines.slice(start, end).join("\n");
|
|
122
|
+
}
|
|
123
|
+
export function formatSandboxBashResult(result) {
|
|
124
|
+
const parts = [result.stdout];
|
|
125
|
+
if (result.stderr.trim().length > 0)
|
|
126
|
+
parts.push(result.stderr);
|
|
127
|
+
parts.push(`Exit code: ${result.exitCode}`);
|
|
128
|
+
return parts.join("\n");
|
|
129
|
+
}
|
|
130
|
+
export async function runSandboxBash(target, command, timeoutMs) {
|
|
131
|
+
const result = await execInSandboxContainer({
|
|
132
|
+
containerName: target.containerName,
|
|
133
|
+
dockerBin: target.dockerBin,
|
|
134
|
+
command,
|
|
135
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
136
|
+
});
|
|
137
|
+
return formatSandboxBashResult(result);
|
|
138
|
+
}
|
|
139
|
+
function countOccurrences(haystack, needle) {
|
|
140
|
+
let count = 0;
|
|
141
|
+
let from = 0;
|
|
142
|
+
while (from <= haystack.length) {
|
|
143
|
+
const at = haystack.indexOf(needle, from);
|
|
144
|
+
if (at < 0)
|
|
145
|
+
return count;
|
|
146
|
+
count += 1;
|
|
147
|
+
from = at + needle.length;
|
|
148
|
+
}
|
|
149
|
+
return count;
|
|
150
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { emailHostFor, stageEmail, workerStageEmail, validateStageEmailAccounts } from "../email/host.js";
|
|
2
2
|
import { ChannelError } from "../channels/errors.js";
|
|
3
3
|
import { channelHostFor, stageChannels, validateStageChannelConnections, workerStageChannels } from "../channels/host.js";
|
|
4
|
+
import { bindStageMemory } from "../memory/access.js";
|
|
5
|
+
import { memoryHostFor } from "../memory/host.js";
|
|
6
|
+
import { workerMemoryForStage } from "../memory/worker.js";
|
|
7
|
+
import { memoryPrompt } from "../tools/memoryTools.js";
|
|
4
8
|
import { EmailError } from "../email/port.js";
|
|
5
9
|
import { bindingKindForOrigin, catalogOrSeededOrigin, decideWorkspaceConfigTrust, } from "../config/configOrigin.js";
|
|
6
10
|
import { loginCheckPromptBlock, readStageLoginCheck, } from "../browser/loginCheck.js";
|
|
@@ -98,12 +102,13 @@ async function resolveAttemptMcpServers(allowlist, factoryCwd, artifactsDir, opt
|
|
|
98
102
|
};
|
|
99
103
|
const stageIdOpt = options.stageId !== undefined ? { stageId: options.stageId } : {};
|
|
100
104
|
const bindingKind = bindingKindForOrigin(options.bindingKind);
|
|
101
|
-
if (factoryCwd !== undefined && (await mcpCatalogExists(factoryCwd))) {
|
|
105
|
+
if (factoryCwd !== undefined && (await mcpCatalogExists(factoryCwd, { scope: { kind: "validate" } }))) {
|
|
102
106
|
await mkdir(artifactsDir, { recursive: true });
|
|
103
107
|
const resolved = await resolveStageMcpServers({
|
|
104
108
|
projectRoot: factoryCwd,
|
|
105
109
|
allowlist: names,
|
|
106
110
|
env,
|
|
111
|
+
scope: { kind: "stage", secrets: options.declaredSecrets ?? [] },
|
|
107
112
|
bindingKind,
|
|
108
113
|
trustWorkspaceConfig: options.trustWorkspaceConfig,
|
|
109
114
|
...stageIdOpt,
|
|
@@ -187,6 +192,7 @@ async function openStageWithOperatorCatalog(agent, input, catalog, factoryCwd, a
|
|
|
187
192
|
trustWorkspaceConfig: resolveOptions.trustWorkspaceConfig,
|
|
188
193
|
stageEnv: resolveOptions.stageEnv,
|
|
189
194
|
stageId: input.stageId,
|
|
195
|
+
declaredSecrets: (input.stage.secrets ?? []).map((d) => d.name),
|
|
190
196
|
});
|
|
191
197
|
resolvedMcpServers = mcp.servers;
|
|
192
198
|
origins.push(...mcp.origins);
|
|
@@ -462,6 +468,11 @@ export async function openStageAttempt(input) {
|
|
|
462
468
|
throw error;
|
|
463
469
|
}
|
|
464
470
|
}
|
|
471
|
+
const stageMemory = input.stage.memories?.length
|
|
472
|
+
? process.env.SF_STAGE_WORKER === "1"
|
|
473
|
+
? await workerMemoryForStage(input.stage.memories)
|
|
474
|
+
: await bindStageMemory(memoryHostFor(), input.stage.memories)
|
|
475
|
+
: undefined;
|
|
465
476
|
const opened = await openStageWithOperatorCatalog(input.agent, {
|
|
466
477
|
roots,
|
|
467
478
|
stage: input.stage,
|
|
@@ -481,6 +492,9 @@ export async function openStageAttempt(input) {
|
|
|
481
492
|
channels: process.env.SF_STAGE_WORKER === "1" ? workerStageChannels()
|
|
482
493
|
: stageChannels(channelHostFor(input.factoryCwd ?? process.cwd()), input.stage, input.runId),
|
|
483
494
|
} : {}),
|
|
495
|
+
...(stageMemory
|
|
496
|
+
? { memory: stageMemory, memoryPrompt: await memoryPrompt(stageMemory) }
|
|
497
|
+
: {}),
|
|
484
498
|
...(input.sessionMode !== undefined ? { sessionMode: input.sessionMode } : {}),
|
|
485
499
|
onActivity: (event) => {
|
|
486
500
|
input.onActivity?.(input.stage.browser !== undefined
|
|
@@ -2,6 +2,7 @@ export declare const STAGE_ENV_PASSTHROUGH = "STAGEFLOW_STAGE_ENV_PASSTHROUGH";
|
|
|
2
2
|
export declare const STAGE_ENV_ALLOW = "STAGEFLOW_STAGE_ENV_ALLOW";
|
|
3
3
|
/** Names copied from Host when present (exact-name allowlist; no prefix wildcards). */
|
|
4
4
|
export declare const STAGE_ENV_ALLOWLIST: readonly string[];
|
|
5
|
+
export declare const PROVIDER_AND_CLOUD_NAMES: readonly ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENAI_API_KEY_FILE", "GOOGLE_API_KEY", "GEMINI_API_KEY", "GROQ_API_KEY", "MISTRAL_API_KEY", "DEEPSEEK_API_KEY", "TOGETHER_API_KEY", "FIREWORKS_API_KEY", "PERPLEXITY_API_KEY", "XAI_API_KEY", "COHERE_API_KEY", "OPENROUTER_API_KEY", "AZURE_OPENAI_API_KEY", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "AWS_SECURITY_TOKEN", "AWS_PROFILE", "AWS_DEFAULT_REGION", "AWS_REGION", "GOOGLE_APPLICATION_CREDENTIALS", "GCLOUD_PROJECT", "AZURE_CLIENT_SECRET", "AZURE_TENANT_ID", "AZURE_CLIENT_ID", "NPM_TOKEN", "NODE_AUTH_TOKEN", "SSH_AUTH_SOCK", "SSH_AGENT_PID", "GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN_FILE", "GH_TOKEN_FILE"];
|
|
5
6
|
/** Never grantable — even via secrets `as: env`. */
|
|
6
7
|
export declare const FOREVER_DENIED_SECRET_NAMES: ReadonlySet<string>;
|
|
7
8
|
/** Blocked from ambient / ALLOW / PASSTHROUGH; `as: env` grants may still inject. */
|
|
@@ -29,6 +30,7 @@ export type BuildStageEnvironmentResult = {
|
|
|
29
30
|
passthroughActive: boolean;
|
|
30
31
|
};
|
|
31
32
|
export declare function isForeverDeniedSecret(name: string): boolean;
|
|
33
|
+
export declare function isProviderOrCloudName(name: string): boolean;
|
|
32
34
|
export declare function isAmbientBlockedEnv(name: string): boolean;
|
|
33
35
|
/**
|
|
34
36
|
* Construct the stage child environment. Exact-name allowlist only — never a
|
|
@@ -26,8 +26,10 @@ export const STAGE_ENV_ALLOWLIST = [
|
|
|
26
26
|
"no_proxy",
|
|
27
27
|
"STAGEFLOW_HOME",
|
|
28
28
|
"STAGEFLOW_CURSOR_EXTENSION",
|
|
29
|
+
"STAGEFLOW_STAGE_CONTAINER_IMAGE",
|
|
30
|
+
"STAGEFLOW_STAGE_CONTAINER_DOCKER_BIN",
|
|
29
31
|
];
|
|
30
|
-
const PROVIDER_AND_CLOUD_NAMES = [
|
|
32
|
+
export const PROVIDER_AND_CLOUD_NAMES = [
|
|
31
33
|
"ANTHROPIC_API_KEY",
|
|
32
34
|
"OPENAI_API_KEY",
|
|
33
35
|
"OPENAI_API_KEY_FILE",
|
|
@@ -70,6 +72,8 @@ export const FOREVER_DENIED_SECRET_NAMES = new Set([
|
|
|
70
72
|
"STAGEFLOW_CONTROL_TOKEN_FILE",
|
|
71
73
|
"STAGEFLOW_READ_TOKEN",
|
|
72
74
|
"STAGEFLOW_READ_TOKEN_FILE",
|
|
75
|
+
// Host-generated key for the MCP OAuth token store; never hand it to a stage or an MCP server.
|
|
76
|
+
"PI_MCP_ADAPTER_OAUTH_FILE_KEY",
|
|
73
77
|
]);
|
|
74
78
|
/** Blocked from ambient / ALLOW / PASSTHROUGH; `as: env` grants may still inject. */
|
|
75
79
|
export const AMBIENT_BLOCKED_ENV_NAMES = new Set([
|
|
@@ -94,6 +98,9 @@ export function isForeverDeniedSecret(name) {
|
|
|
94
98
|
return true;
|
|
95
99
|
return name.startsWith("STAGEFLOW_CONTROL_TOKEN_");
|
|
96
100
|
}
|
|
101
|
+
export function isProviderOrCloudName(name) {
|
|
102
|
+
return PROVIDER_AND_CLOUD_NAMES.includes(name);
|
|
103
|
+
}
|
|
97
104
|
export function isAmbientBlockedEnv(name) {
|
|
98
105
|
return isForeverDeniedSecret(name) || AMBIENT_BLOCKED_ENV_NAMES.has(name);
|
|
99
106
|
}
|
|
@@ -15,6 +15,8 @@ import { SF_STAGE_WORKER, STAGE_WORKER_EXIT, } from "./stageWorkerProtocol.js";
|
|
|
15
15
|
import { emailHostFor, emailWorkerEnvironment, stageEmail, validateStageEmailAccounts } from "../email/host.js";
|
|
16
16
|
import { ChannelError } from "../channels/errors.js";
|
|
17
17
|
import { channelFault, channelHostFor, channelsWorkerEnvironment, stageChannels, validateStageChannelConnections } from "../channels/host.js";
|
|
18
|
+
import { MemoryError, memoryHostFor } from "../memory/host.js";
|
|
19
|
+
import { isMemoryVerb, runMemoryRequest } from "../memory/access.js";
|
|
18
20
|
import { EmailError } from "../email/port.js";
|
|
19
21
|
import { overlayStageBindingEnv, } from "./stageRoots.js";
|
|
20
22
|
function defaultStageCliEntry() {
|
|
@@ -443,6 +445,27 @@ export class StageProcessLauncher {
|
|
|
443
445
|
})();
|
|
444
446
|
return;
|
|
445
447
|
}
|
|
448
|
+
const memoryVerb = request?.type;
|
|
449
|
+
if (!settled && isMemoryVerb(memoryVerb)) {
|
|
450
|
+
void (async () => {
|
|
451
|
+
if (typeof request.requestId !== "string" || request.requestId.length > 100)
|
|
452
|
+
return;
|
|
453
|
+
let response;
|
|
454
|
+
try {
|
|
455
|
+
const stage = input.stage;
|
|
456
|
+
if (!stage || settled)
|
|
457
|
+
throw new MemoryError("MEMORY_NOT_ATTACHED");
|
|
458
|
+
response = await runMemoryRequest(memoryHostFor(), stage.memories ?? [], memoryVerb, request.input);
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
const code = error instanceof MemoryError ? error.code : "MEMORY_UNREADABLE";
|
|
462
|
+
response = { error: { code } };
|
|
463
|
+
}
|
|
464
|
+
if (child.connected && !settled)
|
|
465
|
+
child.send({ type: "memory.response", requestId: request.requestId, ...response });
|
|
466
|
+
})();
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
446
469
|
if (!settled && request?.type === "channel.send") {
|
|
447
470
|
void (async () => {
|
|
448
471
|
if (typeof request.requestId !== "string" || request.requestId.length > 100)
|
|
@@ -11,6 +11,14 @@ export type StageRoots = {
|
|
|
11
11
|
attempt?: number;
|
|
12
12
|
/** Durable Pi auth.json path for ModelRuntime.create({ authPath }). */
|
|
13
13
|
authPath?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Sandbox container `stageWorker.ts` started for this attempt, if any.
|
|
16
|
+
* When set, read/write/edit/bash run inside that container's /workspace
|
|
17
|
+
* desk. Undefined leaves those tools on the host.
|
|
18
|
+
*/
|
|
19
|
+
containerName?: string;
|
|
20
|
+
/** The docker binary the container above was started with (see STAGEFLOW_STAGE_CONTAINER_DOCKER_BIN). */
|
|
21
|
+
containerDockerBin?: string;
|
|
14
22
|
};
|
|
15
23
|
export declare const STAGEFLOW_CHECKOUT = "STAGEFLOW_CHECKOUT";
|
|
16
24
|
export declare const STAGEFLOW_RUN_WORKSPACE = "STAGEFLOW_RUN_WORKSPACE";
|
|
@@ -51,6 +59,7 @@ export declare function stageBindingEnvFromRun(options: {
|
|
|
51
59
|
};
|
|
52
60
|
export declare function withResolvedAuthPath(roots: StageRoots, factoryCwd: string): StageRoots;
|
|
53
61
|
export declare function rootsForStageWorker(runWorkspaceDir: string, stageId: string, model: string, checkoutRoot?: string, attemptCtx?: StageAttemptContext): StageRoots;
|
|
62
|
+
export declare function withContainerName(roots: StageRoots, containerName: string | undefined, containerDockerBin?: string): StageRoots;
|
|
54
63
|
export declare function bindPiAgentDirEnv(agentDirPath: string): () => void;
|
|
55
64
|
export declare function resolveCheckoutPath(declared: string, cwd: string): string;
|
|
56
65
|
export declare function resolveAndValidateCheckout(task: TaskFile, override: string | undefined, factoryCwd: string): Promise<string | undefined>;
|
|
@@ -4,6 +4,7 @@ import { isCursorModelRef } from "../agent/cursorProvider.js";
|
|
|
4
4
|
import { stageDir } from "../runstore/paths.js";
|
|
5
5
|
import { noAttemptContext } from "./stageAttemptContext.js";
|
|
6
6
|
import { resolveCredentialBinding, STAGEFLOW_AGENT_AUTH_PATH_ENV, currentExplicitAuthPath, } from "./credentialBinding.js";
|
|
7
|
+
import { ensureWorktreeCheckout } from "./gitWorktreeCheckout.js";
|
|
7
8
|
import { SF_STAGE_WORKER } from "./stageWorkerProtocol.js";
|
|
8
9
|
export const STAGEFLOW_CHECKOUT = "STAGEFLOW_CHECKOUT";
|
|
9
10
|
export const STAGEFLOW_RUN_WORKSPACE = "STAGEFLOW_RUN_WORKSPACE";
|
|
@@ -151,6 +152,11 @@ export function rootsForStageWorker(runWorkspaceDir, stageId, model, checkoutRoo
|
|
|
151
152
|
cwd: stageDir(runWorkspaceDir, stageId),
|
|
152
153
|
};
|
|
153
154
|
}
|
|
155
|
+
export function withContainerName(roots, containerName, containerDockerBin) {
|
|
156
|
+
return containerName === undefined
|
|
157
|
+
? roots
|
|
158
|
+
: { ...roots, containerName, containerDockerBin };
|
|
159
|
+
}
|
|
154
160
|
export function bindPiAgentDirEnv(agentDirPath) {
|
|
155
161
|
const prev = process.env[PI_CODING_AGENT_DIR_ENV];
|
|
156
162
|
process.env[PI_CODING_AGENT_DIR_ENV] = agentDirPath;
|
|
@@ -170,6 +176,14 @@ export async function resolveAndValidateCheckout(task, override, factoryCwd) {
|
|
|
170
176
|
const raw = override ?? task.checkout;
|
|
171
177
|
if (raw === undefined)
|
|
172
178
|
return undefined;
|
|
179
|
+
if (typeof raw !== "string") {
|
|
180
|
+
return ensureWorktreeCheckout({
|
|
181
|
+
projectRoot: factoryCwd,
|
|
182
|
+
branch: raw.branch,
|
|
183
|
+
base: raw.base,
|
|
184
|
+
taskId: task.id,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
173
187
|
if (raw.trim() === "") {
|
|
174
188
|
throw new Error("Invalid checkout: path is empty or whitespace-only");
|
|
175
189
|
}
|
|
@@ -53,6 +53,11 @@ export type RunStageOptions = {
|
|
|
53
53
|
stageEnv?: Record<string, string>;
|
|
54
54
|
/** Test seam for the Host login check run by a `browser_login` verify item. */
|
|
55
55
|
browser?: Pick<StageBrowserSupport, "runner" | "loginCheck">;
|
|
56
|
+
/**
|
|
57
|
+
* Runs after the agent turn and before completion verification records a
|
|
58
|
+
* verdict. Used to copy a sandbox desk back onto the host tree.
|
|
59
|
+
*/
|
|
60
|
+
beforeVerify?: () => Promise<void>;
|
|
56
61
|
};
|
|
57
62
|
/**
|
|
58
63
|
* Yield loop: next() until completed; on wait, coordinator enterWait (KTD6).
|
|
@@ -47,6 +47,17 @@ async function appendLifecycleEventAndPatchExecution(options) {
|
|
|
47
47
|
// no execution row yet
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
async function runBeforeVerify(beforeVerify) {
|
|
51
|
+
if (beforeVerify === undefined)
|
|
52
|
+
return undefined;
|
|
53
|
+
try {
|
|
54
|
+
await beforeVerify();
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return error instanceof Error ? error.message : String(error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
50
61
|
async function finalizeStageResult(options) {
|
|
51
62
|
const { store, runId, stageId, result, activityChain, attemptCtx, browser } = options;
|
|
52
63
|
const attemptOpt = attemptCtx?.eventOptions();
|
|
@@ -125,7 +136,7 @@ export async function runStageYieldLoop(options) {
|
|
|
125
136
|
}
|
|
126
137
|
}
|
|
127
138
|
export async function runStage(options) {
|
|
128
|
-
const { agent, store, runId, stage, task, dag, checkoutRoot, workspaceDir, hitl, skipStarted, existingHandle, workerMode, roots: rootsOverride, attemptCtx, factoryCwd, operatorCatalog, completedEnvelopes, skipGates, sessionMode, feedbackLoopContext, resumeToken, stageEnv, browser: browserSeams, } = options;
|
|
139
|
+
const { agent, store, runId, stage, task, dag, checkoutRoot, workspaceDir, hitl, skipStarted, existingHandle, workerMode, roots: rootsOverride, attemptCtx, factoryCwd, operatorCatalog, completedEnvelopes, skipGates, sessionMode, feedbackLoopContext, resumeToken, stageEnv, browser: browserSeams, beforeVerify, } = options;
|
|
129
140
|
const stageId = options.stageId ?? stage.id;
|
|
130
141
|
const attemptOpt = attemptCtx?.eventOptions();
|
|
131
142
|
const baseRoots = rootsOverride ??
|
|
@@ -242,20 +253,43 @@ export async function runStage(options) {
|
|
|
242
253
|
}
|
|
243
254
|
catch (err) {
|
|
244
255
|
const reason = err instanceof Error ? err.message : String(err);
|
|
256
|
+
const synced = await runBeforeVerify(beforeVerify);
|
|
257
|
+
const fullReason = synced === undefined ? reason : `${reason}; ${synced}`;
|
|
245
258
|
await activityChain;
|
|
246
259
|
hitl?.clearLiveWait(runId, stageId);
|
|
247
260
|
await appendLifecycleEventAndPatchExecution({
|
|
248
261
|
store,
|
|
249
262
|
runId,
|
|
250
263
|
stageId,
|
|
251
|
-
event: { event: "failed", reason },
|
|
264
|
+
event: { event: "failed", reason: fullReason },
|
|
252
265
|
attemptCtx,
|
|
253
266
|
});
|
|
254
|
-
console.error(`Stage ${stageId} failed: ${
|
|
267
|
+
console.error(`Stage ${stageId} failed: ${fullReason}`);
|
|
255
268
|
await handle
|
|
256
269
|
.close(isStageTimeoutReason(reason) ? { park: true } : undefined)
|
|
257
270
|
.catch(() => undefined);
|
|
258
|
-
return { ok: false, reason };
|
|
271
|
+
return { ok: false, reason: fullReason };
|
|
272
|
+
}
|
|
273
|
+
const syncError = await runBeforeVerify(beforeVerify);
|
|
274
|
+
if (syncError !== undefined) {
|
|
275
|
+
const reason = syncError;
|
|
276
|
+
if (isRunStageWaiting(outcome)) {
|
|
277
|
+
await activityChain;
|
|
278
|
+
hitl?.clearLiveWait(runId, stageId);
|
|
279
|
+
await appendLifecycleEventAndPatchExecution({
|
|
280
|
+
store,
|
|
281
|
+
runId,
|
|
282
|
+
stageId,
|
|
283
|
+
event: { event: "failed", reason },
|
|
284
|
+
attemptCtx,
|
|
285
|
+
});
|
|
286
|
+
console.error(`Stage ${stageId} failed: ${reason}`);
|
|
287
|
+
await handle.close().catch(() => undefined);
|
|
288
|
+
return { ok: false, reason };
|
|
289
|
+
}
|
|
290
|
+
outcome = outcome.ok
|
|
291
|
+
? { ok: false, reason }
|
|
292
|
+
: { ok: false, reason: `${outcome.reason}; ${reason}` };
|
|
259
293
|
}
|
|
260
294
|
if (isRunStageWaiting(outcome)) {
|
|
261
295
|
await activityChain;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type StageRoots } from "./stageRoots.js";
|
|
2
|
+
export type StageSandbox = {
|
|
3
|
+
roots: StageRoots;
|
|
4
|
+
beforeVerify?: () => Promise<void>;
|
|
5
|
+
close: () => Promise<void>;
|
|
6
|
+
failure?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function openStageSandbox(input: {
|
|
9
|
+
env: NodeJS.ProcessEnv;
|
|
10
|
+
roots: StageRoots;
|
|
11
|
+
runId: string;
|
|
12
|
+
stageId: string;
|
|
13
|
+
attempt: number;
|
|
14
|
+
attemptDir: string;
|
|
15
|
+
checkoutRoot?: string;
|
|
16
|
+
}): Promise<StageSandbox>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { resolveSandboxContainerOptions, startSandboxContainer, stopSandboxContainer, } from "./sandboxContainer.js";
|
|
2
|
+
import { buildSandboxDesk, populateSandboxDesk, syncSandboxDesk, } from "./sandboxDesk.js";
|
|
3
|
+
import { withContainerName } from "./stageRoots.js";
|
|
4
|
+
export async function openStageSandbox(input) {
|
|
5
|
+
const options = resolveSandboxContainerOptions(input.env);
|
|
6
|
+
if (!options) {
|
|
7
|
+
return { roots: input.roots, close: async () => { } };
|
|
8
|
+
}
|
|
9
|
+
const started = await startSandboxContainer({
|
|
10
|
+
image: options.image,
|
|
11
|
+
dockerBin: options.dockerBin,
|
|
12
|
+
runId: input.runId,
|
|
13
|
+
stageId: input.stageId,
|
|
14
|
+
attempt: input.attempt,
|
|
15
|
+
});
|
|
16
|
+
const roots = withContainerName(input.roots, started.containerName, options.dockerBin);
|
|
17
|
+
const checkout = input.checkoutRoot?.trim();
|
|
18
|
+
const desk = buildSandboxDesk({
|
|
19
|
+
attemptDir: input.attemptDir,
|
|
20
|
+
...(checkout ? { checkoutRoot: checkout } : {}),
|
|
21
|
+
});
|
|
22
|
+
const session = {
|
|
23
|
+
roots,
|
|
24
|
+
close: () => stopSandboxContainer({
|
|
25
|
+
containerName: started.containerName,
|
|
26
|
+
dockerBin: options.dockerBin,
|
|
27
|
+
}).catch((err) => {
|
|
28
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
29
|
+
process.stderr.write(`[stageWorker] failed to stop sandbox container ${started.containerName}: ${message}\n`);
|
|
30
|
+
}),
|
|
31
|
+
};
|
|
32
|
+
try {
|
|
33
|
+
await populateSandboxDesk({
|
|
34
|
+
containerName: started.containerName,
|
|
35
|
+
dockerBin: options.dockerBin,
|
|
36
|
+
desk,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
session.failure = err instanceof Error ? err.message : String(err);
|
|
41
|
+
return session;
|
|
42
|
+
}
|
|
43
|
+
session.beforeVerify = syncOnce(started.containerName, options.dockerBin, desk);
|
|
44
|
+
return session;
|
|
45
|
+
}
|
|
46
|
+
function syncOnce(containerName, dockerBin, desk) {
|
|
47
|
+
let synced = false;
|
|
48
|
+
return async () => {
|
|
49
|
+
if (synced)
|
|
50
|
+
return;
|
|
51
|
+
synced = true;
|
|
52
|
+
await syncSandboxDesk({ containerName, dockerBin, desk });
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -9,6 +9,11 @@ export type SecretRegistryEntry = {
|
|
|
9
9
|
name: string;
|
|
10
10
|
source: string;
|
|
11
11
|
pointerVar: string;
|
|
12
|
+
}
|
|
13
|
+
/** A secret saved from the console (Settings → Project MCP). It only fills `${NAME}` in MCP servers; never the stage environment. */
|
|
14
|
+
| {
|
|
15
|
+
kind: "console";
|
|
16
|
+
name: string;
|
|
12
17
|
};
|
|
13
18
|
export type SecretRegistry = Map<string, SecretRegistryEntry>;
|
|
14
19
|
export declare class SecretUnavailableError extends Error {
|
|
@@ -35,6 +40,6 @@ export type ResolveStageSecretsResult = {
|
|
|
35
40
|
credentialDirs: string[];
|
|
36
41
|
};
|
|
37
42
|
/** Presence-only check (no materialisation) for preflight / start gates. */
|
|
38
|
-
export declare function assertSecretPresent(name: string, registry: SecretRegistry, hostEnv: NodeJS.ProcessEnv): void;
|
|
43
|
+
export declare function assertSecretPresent(name: string, registry: SecretRegistry, hostEnv: NodeJS.ProcessEnv, home?: string): void;
|
|
39
44
|
export declare function resolveStageSecrets(input: ResolveStageSecretsInput): ResolveStageSecretsResult;
|
|
40
45
|
export { cleanupCredentialsDir };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { shouldRegisterValue } from "../logging/redact.js";
|
|
3
|
+
import { readMcpSecrets } from "../config/mcpHostCatalog.js";
|
|
3
4
|
import { globalStageflowHome } from "../project/globalHome.js";
|
|
4
5
|
import { attemptCredentialsDir, cleanupCredentialsDir, ensureStageAskpassHelper, materialiseCredentialFile, materialiseSecretBytes, readFileCredentialRegistry, } from "./credentialMaterialisation.js";
|
|
5
6
|
import { defaultSecretDelivery, } from "./stageSecretDecl.js";
|
|
@@ -66,10 +67,20 @@ export function loadSecretRegistry(env = process.env, home = globalStageflowHome
|
|
|
66
67
|
continue;
|
|
67
68
|
registry.set(name, { kind: "env", name });
|
|
68
69
|
}
|
|
70
|
+
// Console-stored MCP secrets: a stage may declare them so its `mcp:` servers can be filled.
|
|
71
|
+
try {
|
|
72
|
+
for (const name of Object.keys(readMcpSecrets(home))) {
|
|
73
|
+
if (!isForeverDeniedSecret(name) && !registry.has(name))
|
|
74
|
+
registry.set(name, { kind: "console", name });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// A corrupt host-state file is reported by the MCP routes; it must not stop other stages.
|
|
79
|
+
}
|
|
69
80
|
return registry;
|
|
70
81
|
}
|
|
71
82
|
/** Presence-only check (no materialisation) for preflight / start gates. */
|
|
72
|
-
export function assertSecretPresent(name, registry, hostEnv) {
|
|
83
|
+
export function assertSecretPresent(name, registry, hostEnv, home) {
|
|
73
84
|
if (isForeverDeniedSecret(name)) {
|
|
74
85
|
throw new Error(`secret "${name}" is permanently denied`);
|
|
75
86
|
}
|
|
@@ -86,11 +97,24 @@ export function assertSecretPresent(name, registry, hostEnv) {
|
|
|
86
97
|
}
|
|
87
98
|
return;
|
|
88
99
|
}
|
|
100
|
+
if (entry.kind === "console") {
|
|
101
|
+
if (!readMcpSecretValue(name, home))
|
|
102
|
+
throw new SecretUnavailableError(name);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
89
105
|
const value = readEnvSecretValue(name, hostEnv);
|
|
90
106
|
if (value === undefined || value === "") {
|
|
91
107
|
throw new SecretUnavailableError(name);
|
|
92
108
|
}
|
|
93
109
|
}
|
|
110
|
+
function readMcpSecretValue(name, home) {
|
|
111
|
+
try {
|
|
112
|
+
return readMcpSecrets(home)[name];
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
94
118
|
function materialiseGitAskpassGrant(grantEnv, credentialDirs, opts) {
|
|
95
119
|
const destDir = attemptCredentialsDir(opts.attemptDir, opts.secretName);
|
|
96
120
|
const tokenFile = materialiseSecretBytes({
|
|
@@ -119,6 +143,15 @@ export function resolveStageSecrets(input) {
|
|
|
119
143
|
if (entry === undefined) {
|
|
120
144
|
throw new Error(`stage.unknown_secret: "${decl.name}"`);
|
|
121
145
|
}
|
|
146
|
+
if (entry.kind === "console") {
|
|
147
|
+
const stored = readMcpSecretValue(decl.name, input.home);
|
|
148
|
+
if (!stored)
|
|
149
|
+
throw new SecretUnavailableError(decl.name);
|
|
150
|
+
if (shouldRegisterValue(stored))
|
|
151
|
+
knownValues.push({ name: decl.name, value: stored });
|
|
152
|
+
warnings.push(`secrets: ${decl.name} is a console secret; it fills MCP servers only and is not set in the stage environment`);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
122
155
|
if (entry.kind === "file") {
|
|
123
156
|
const destDir = attemptCredentialsDir(input.attemptDir, decl.name);
|
|
124
157
|
const homeRoot = input.home ?? globalStageflowHome();
|