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
|
@@ -11,6 +11,8 @@ import { MIGRATION_009 } from "./009-trigger-next-run.js";
|
|
|
11
11
|
import { MIGRATION_010 } from "./010-trigger-adapter-state.js";
|
|
12
12
|
import { MIGRATION_011 } from "./011-email-dispatch-key.js";
|
|
13
13
|
import { MIGRATION_012 } from "./012-trigger-fires.js";
|
|
14
|
+
import { MIGRATION_013 } from "./013-agents.js";
|
|
15
|
+
import { MIGRATION_014 } from "./014-agent-deliveries.js";
|
|
14
16
|
const DEFAULT_MIGRATIONS = [
|
|
15
17
|
MIGRATION_001,
|
|
16
18
|
MIGRATION_002,
|
|
@@ -24,6 +26,8 @@ const DEFAULT_MIGRATIONS = [
|
|
|
24
26
|
MIGRATION_010,
|
|
25
27
|
MIGRATION_011,
|
|
26
28
|
MIGRATION_012,
|
|
29
|
+
MIGRATION_013,
|
|
30
|
+
MIGRATION_014,
|
|
27
31
|
];
|
|
28
32
|
export const CURRENT_SCHEMA_VERSION = DEFAULT_MIGRATIONS[DEFAULT_MIGRATIONS.length - 1].version;
|
|
29
33
|
function throwStoreSchemaTooNew(db, version, maxKnown) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CheckoutCapability } from "./completionCheckRunner.js";
|
|
2
|
+
export declare function runGit(root: string, args: string[]): Promise<Buffer>;
|
|
2
3
|
/**
|
|
3
4
|
* Git-backed checkout observation. It fingerprints tracked and untracked
|
|
4
5
|
* files before the agent runs, so pre-existing changes are not attributed to
|
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { lstat, readFile, readlink } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { runGit as execGit } from "../git/exec.js";
|
|
5
|
-
async function runGit(root, args) {
|
|
5
|
+
export async function runGit(root, args) {
|
|
6
6
|
const result = await execGit({
|
|
7
7
|
cwd: root,
|
|
8
8
|
args,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { sanitizeContainerNameSegment } from "./sandboxContainer.js";
|
|
2
|
+
export declare const sanitizeBranchForPath: typeof sanitizeContainerNameSegment;
|
|
3
|
+
export declare function defaultBranchNameForTask(taskId: string): string;
|
|
4
|
+
export declare function worktreePathFor(projectRoot: string, branch: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Ensures a git worktree checked out to `branch` (creating the branch from
|
|
7
|
+
* `base` if it doesn't exist yet) exists at a deterministic path derived
|
|
8
|
+
* from the branch name, and returns that path.
|
|
9
|
+
*
|
|
10
|
+
* Deterministic (not per-run/random) on purpose: `resolveAndValidateCheckout`
|
|
11
|
+
* is called twice per run start (runManager.ts, for the checkout lease key,
|
|
12
|
+
* then pipelineRunner.ts, for the durable run record) — a random path would
|
|
13
|
+
* create two worktrees for one run. A stable path also means rerunning the
|
|
14
|
+
* same task/branch reuses the same worktree, matching the "leave it in
|
|
15
|
+
* place" behavior an operator gets from making one by hand.
|
|
16
|
+
*/
|
|
17
|
+
export declare function ensureWorktreeCheckout(params: {
|
|
18
|
+
projectRoot: string;
|
|
19
|
+
branch?: string;
|
|
20
|
+
base?: string;
|
|
21
|
+
taskId: string;
|
|
22
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { access, mkdir, realpath } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { runGit } from "./gitCheckoutCapability.js";
|
|
4
|
+
import { sanitizeContainerNameSegment } from "./sandboxContainer.js";
|
|
5
|
+
/**
|
|
6
|
+
* Auto-created worktrees live as siblings of the project root (not nested
|
|
7
|
+
* inside it) — the same place `examples/ship-feature/README.md` already
|
|
8
|
+
* tells operators to put a hand-made one, just automated.
|
|
9
|
+
*/
|
|
10
|
+
const WORKTREES_DIR_NAME = ".stageflow-worktrees";
|
|
11
|
+
export const sanitizeBranchForPath = sanitizeContainerNameSegment;
|
|
12
|
+
export function defaultBranchNameForTask(taskId) {
|
|
13
|
+
return `stageflow/${sanitizeBranchForPath(taskId)}`;
|
|
14
|
+
}
|
|
15
|
+
export function worktreePathFor(projectRoot, branch) {
|
|
16
|
+
return path.join(path.dirname(projectRoot), WORKTREES_DIR_NAME, sanitizeBranchForPath(branch));
|
|
17
|
+
}
|
|
18
|
+
async function git(projectRoot, args) {
|
|
19
|
+
const stdout = await runGit(projectRoot, args);
|
|
20
|
+
return stdout.toString("utf8");
|
|
21
|
+
}
|
|
22
|
+
async function registeredWorktreePaths(projectRoot) {
|
|
23
|
+
const stdout = await git(projectRoot, ["worktree", "list", "--porcelain"]);
|
|
24
|
+
const paths = new Set();
|
|
25
|
+
for (const line of stdout.split("\n")) {
|
|
26
|
+
const match = /^worktree (.+)$/.exec(line);
|
|
27
|
+
if (match)
|
|
28
|
+
paths.add(match[1]);
|
|
29
|
+
}
|
|
30
|
+
return paths;
|
|
31
|
+
}
|
|
32
|
+
async function branchExists(projectRoot, branch) {
|
|
33
|
+
try {
|
|
34
|
+
await git(projectRoot, ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function pathExists(target) {
|
|
42
|
+
try {
|
|
43
|
+
await access(target);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// A ref/branch value starting with `-` can get reinterpreted by git as a
|
|
51
|
+
// flag instead of a literal name (e.g. `-b --upload-pack=...`) — task YAML
|
|
52
|
+
// is operator-authored, not attacker input, but rejecting this outright is
|
|
53
|
+
// free and closes the class of mistake/footgun entirely.
|
|
54
|
+
function assertNotFlagLike(value, label) {
|
|
55
|
+
if (value.startsWith("-")) {
|
|
56
|
+
throw new Error(`Invalid ${label} "${value}": must not start with "-"`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Ensures a git worktree checked out to `branch` (creating the branch from
|
|
61
|
+
* `base` if it doesn't exist yet) exists at a deterministic path derived
|
|
62
|
+
* from the branch name, and returns that path.
|
|
63
|
+
*
|
|
64
|
+
* Deterministic (not per-run/random) on purpose: `resolveAndValidateCheckout`
|
|
65
|
+
* is called twice per run start (runManager.ts, for the checkout lease key,
|
|
66
|
+
* then pipelineRunner.ts, for the durable run record) — a random path would
|
|
67
|
+
* create two worktrees for one run. A stable path also means rerunning the
|
|
68
|
+
* same task/branch reuses the same worktree, matching the "leave it in
|
|
69
|
+
* place" behavior an operator gets from making one by hand.
|
|
70
|
+
*/
|
|
71
|
+
export async function ensureWorktreeCheckout(params) {
|
|
72
|
+
// `git worktree list --porcelain` reports canonicalized paths (symlinks
|
|
73
|
+
// resolved, e.g. macOS's /var -> /private/var) — realpath the project
|
|
74
|
+
// root up front so every path this function builds or compares matches
|
|
75
|
+
// what git itself reports, instead of drifting apart on a symlinked tmp
|
|
76
|
+
// dir or similar.
|
|
77
|
+
const projectRoot = await realpath(params.projectRoot);
|
|
78
|
+
const branch = params.branch ?? defaultBranchNameForTask(params.taskId);
|
|
79
|
+
assertNotFlagLike(branch, "checkout.branch");
|
|
80
|
+
if (params.base !== undefined)
|
|
81
|
+
assertNotFlagLike(params.base, "checkout.base");
|
|
82
|
+
const targetPath = worktreePathFor(projectRoot, branch);
|
|
83
|
+
const registered = await registeredWorktreePaths(projectRoot);
|
|
84
|
+
if (registered.has(targetPath)) {
|
|
85
|
+
if (await pathExists(targetPath)) {
|
|
86
|
+
return targetPath;
|
|
87
|
+
}
|
|
88
|
+
// Registered but the directory is gone (e.g. deleted with `rm -rf`
|
|
89
|
+
// instead of `git worktree remove`) — clear the stale registration so
|
|
90
|
+
// `worktree add` below doesn't refuse to reuse the path.
|
|
91
|
+
await git(projectRoot, ["worktree", "prune"]);
|
|
92
|
+
}
|
|
93
|
+
else if (await pathExists(targetPath)) {
|
|
94
|
+
throw new Error(`Worktree path already exists but is not a registered git worktree: ${targetPath}. Remove it or use a different branch name.`);
|
|
95
|
+
}
|
|
96
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
97
|
+
if (await branchExists(projectRoot, branch)) {
|
|
98
|
+
await git(projectRoot, ["worktree", "add", targetPath, branch]);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
await git(projectRoot, [
|
|
102
|
+
"worktree",
|
|
103
|
+
"add",
|
|
104
|
+
targetPath,
|
|
105
|
+
"-b",
|
|
106
|
+
branch,
|
|
107
|
+
params.base ?? "HEAD",
|
|
108
|
+
]);
|
|
109
|
+
}
|
|
110
|
+
// Two concurrent run-starts auto-creating the same branch/worktree can
|
|
111
|
+
// race inside `git worktree add` itself — for the "branch already
|
|
112
|
+
// exists" path this has been observed to exit 0 while leaving no
|
|
113
|
+
// directory behind (the loser silently no-ops instead of erroring).
|
|
114
|
+
// Turn that into a loud, unambiguous failure rather than handing back a
|
|
115
|
+
// "successful" path that doesn't exist.
|
|
116
|
+
if (!(await pathExists(targetPath))) {
|
|
117
|
+
throw new Error(`git worktree add reported success but ${targetPath} does not exist — likely a concurrent worktree creation for the same branch ("${branch}"); retry.`);
|
|
118
|
+
}
|
|
119
|
+
return targetPath;
|
|
120
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export declare const SANDBOX_CONTAINER_IMAGE_ENV = "STAGEFLOW_STAGE_CONTAINER_IMAGE";
|
|
2
|
+
export declare const SANDBOX_CONTAINER_DOCKER_BIN_ENV = "STAGEFLOW_STAGE_CONTAINER_DOCKER_BIN";
|
|
3
|
+
/** Docker container names allow only `[a-zA-Z0-9_.-]`. */
|
|
4
|
+
export declare function sanitizeContainerNameSegment(value: string): string;
|
|
5
|
+
export declare function buildContainerName(input: {
|
|
6
|
+
runId: string;
|
|
7
|
+
stageId: string;
|
|
8
|
+
attempt?: number;
|
|
9
|
+
}, suffix: string): string;
|
|
10
|
+
export type SandboxContainerOptions = {
|
|
11
|
+
image: string;
|
|
12
|
+
dockerBin: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function resolveSandboxContainerOptions(env: Record<string, string | undefined>, override?: {
|
|
15
|
+
image: string;
|
|
16
|
+
dockerBin?: string;
|
|
17
|
+
}): SandboxContainerOptions | undefined;
|
|
18
|
+
/** Fixed path inside the sandbox. The daemon owns the volume; we never pass it a path from this process. */
|
|
19
|
+
export declare const SANDBOX_MOUNT_PATH = "/workspace";
|
|
20
|
+
export declare function sandboxVolumeName(containerName: string): string;
|
|
21
|
+
export declare function buildStartContainerArgs(params: {
|
|
22
|
+
image: string;
|
|
23
|
+
containerName: string;
|
|
24
|
+
}): string[];
|
|
25
|
+
export declare function startSandboxContainer(params: {
|
|
26
|
+
image: string;
|
|
27
|
+
runId: string;
|
|
28
|
+
stageId: string;
|
|
29
|
+
attempt?: number;
|
|
30
|
+
dockerBin?: string;
|
|
31
|
+
}): Promise<{
|
|
32
|
+
containerName: string;
|
|
33
|
+
}>;
|
|
34
|
+
export declare function stopSandboxContainer(params: {
|
|
35
|
+
containerName: string;
|
|
36
|
+
dockerBin?: string;
|
|
37
|
+
}): Promise<void>;
|
|
38
|
+
export type SandboxExecResult = {
|
|
39
|
+
stdout: string;
|
|
40
|
+
stderr: string;
|
|
41
|
+
exitCode: number;
|
|
42
|
+
};
|
|
43
|
+
export declare function execInSandboxContainer(params: {
|
|
44
|
+
containerName: string;
|
|
45
|
+
command: string;
|
|
46
|
+
dockerBin?: string;
|
|
47
|
+
timeoutMs?: number;
|
|
48
|
+
}): Promise<SandboxExecResult>;
|
|
49
|
+
export declare function copyToSandboxContainer(params: {
|
|
50
|
+
containerName: string;
|
|
51
|
+
hostPath: string;
|
|
52
|
+
containerPath: string;
|
|
53
|
+
dockerBin?: string;
|
|
54
|
+
contents?: boolean;
|
|
55
|
+
}): Promise<void>;
|
|
56
|
+
export declare function copyFromSandboxContainer(params: {
|
|
57
|
+
containerName: string;
|
|
58
|
+
containerPath: string;
|
|
59
|
+
hostPath: string;
|
|
60
|
+
dockerBin?: string;
|
|
61
|
+
contents?: boolean;
|
|
62
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
export const SANDBOX_CONTAINER_IMAGE_ENV = "STAGEFLOW_STAGE_CONTAINER_IMAGE";
|
|
5
|
+
export const SANDBOX_CONTAINER_DOCKER_BIN_ENV = "STAGEFLOW_STAGE_CONTAINER_DOCKER_BIN";
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
/** Docker container names allow only `[a-zA-Z0-9_.-]`. */
|
|
8
|
+
export function sanitizeContainerNameSegment(value) {
|
|
9
|
+
return value.replace(/[^a-zA-Z0-9_.-]/g, "-");
|
|
10
|
+
}
|
|
11
|
+
export function buildContainerName(input, suffix) {
|
|
12
|
+
const runId = sanitizeContainerNameSegment(input.runId);
|
|
13
|
+
const stageId = sanitizeContainerNameSegment(input.stageId);
|
|
14
|
+
const attempt = input.attempt ?? 1;
|
|
15
|
+
return `stageflow-${runId}-${stageId}-${attempt}-${suffix}`;
|
|
16
|
+
}
|
|
17
|
+
export function resolveSandboxContainerOptions(env, override) {
|
|
18
|
+
if (override) {
|
|
19
|
+
if (!override.image || override.image.trim() === "") {
|
|
20
|
+
throw new Error("sandboxContainer: image must be a non-empty string");
|
|
21
|
+
}
|
|
22
|
+
return { image: override.image, dockerBin: override.dockerBin || "docker" };
|
|
23
|
+
}
|
|
24
|
+
const image = env[SANDBOX_CONTAINER_IMAGE_ENV];
|
|
25
|
+
if (!image || image.trim() === "")
|
|
26
|
+
return undefined;
|
|
27
|
+
return {
|
|
28
|
+
image,
|
|
29
|
+
dockerBin: env[SANDBOX_CONTAINER_DOCKER_BIN_ENV] || "docker",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Fixed path inside the sandbox. The daemon owns the volume; we never pass it a path from this process. */
|
|
33
|
+
export const SANDBOX_MOUNT_PATH = "/workspace";
|
|
34
|
+
export function sandboxVolumeName(containerName) {
|
|
35
|
+
const safe = containerName.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
36
|
+
return `sf-bash-${safe}`;
|
|
37
|
+
}
|
|
38
|
+
export function buildStartContainerArgs(params) {
|
|
39
|
+
const { image, containerName } = params;
|
|
40
|
+
return [
|
|
41
|
+
"run",
|
|
42
|
+
"-d",
|
|
43
|
+
"--name",
|
|
44
|
+
containerName,
|
|
45
|
+
"-v",
|
|
46
|
+
`${sandboxVolumeName(containerName)}:${SANDBOX_MOUNT_PATH}`,
|
|
47
|
+
"-w",
|
|
48
|
+
SANDBOX_MOUNT_PATH,
|
|
49
|
+
image,
|
|
50
|
+
"sleep",
|
|
51
|
+
"infinity",
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
function errorMessage(err) {
|
|
55
|
+
if (err && typeof err === "object" && "stderr" in err) {
|
|
56
|
+
const stderr = err.stderr;
|
|
57
|
+
if (typeof stderr === "string" && stderr.trim().length > 0)
|
|
58
|
+
return stderr;
|
|
59
|
+
}
|
|
60
|
+
return err instanceof Error ? err.message : String(err);
|
|
61
|
+
}
|
|
62
|
+
export async function startSandboxContainer(params) {
|
|
63
|
+
const containerName = buildContainerName({ runId: params.runId, stageId: params.stageId, attempt: params.attempt }, randomUUID().slice(0, 8));
|
|
64
|
+
const args = buildStartContainerArgs({
|
|
65
|
+
image: params.image,
|
|
66
|
+
containerName,
|
|
67
|
+
});
|
|
68
|
+
try {
|
|
69
|
+
await execFileAsync(params.dockerBin ?? "docker", args);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
throw new Error(`failed to start sandbox container: ${errorMessage(err)}`);
|
|
73
|
+
}
|
|
74
|
+
return { containerName };
|
|
75
|
+
}
|
|
76
|
+
const NO_SUCH_CONTAINER_PATTERN = /no such container/i;
|
|
77
|
+
const NO_SUCH_VOLUME_PATTERN = /no such volume/i;
|
|
78
|
+
export async function stopSandboxContainer(params) {
|
|
79
|
+
try {
|
|
80
|
+
await execFileAsync(params.dockerBin ?? "docker", [
|
|
81
|
+
"rm",
|
|
82
|
+
"-f",
|
|
83
|
+
params.containerName,
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
const stderr = stderrOf(err);
|
|
88
|
+
if (!NO_SUCH_CONTAINER_PATTERN.test(stderr)) {
|
|
89
|
+
throw new Error(`failed to stop sandbox container: ${errorMessage(err)}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
await execFileAsync(params.dockerBin ?? "docker", [
|
|
94
|
+
"volume",
|
|
95
|
+
"rm",
|
|
96
|
+
sandboxVolumeName(params.containerName),
|
|
97
|
+
]);
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
const stderr = stderrOf(err);
|
|
101
|
+
if (NO_SUCH_VOLUME_PATTERN.test(stderr))
|
|
102
|
+
return;
|
|
103
|
+
throw new Error(`failed to remove sandbox volume: ${errorMessage(err)}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function stderrOf(err) {
|
|
107
|
+
if (err && typeof err === "object" && "stderr" in err) {
|
|
108
|
+
return String(err.stderr ?? "");
|
|
109
|
+
}
|
|
110
|
+
return "";
|
|
111
|
+
}
|
|
112
|
+
export async function execInSandboxContainer(params) {
|
|
113
|
+
const args = ["exec", "-w", SANDBOX_MOUNT_PATH, params.containerName, "bash", "-c", params.command];
|
|
114
|
+
try {
|
|
115
|
+
const { stdout, stderr } = await execFileAsync(params.dockerBin ?? "docker", args, {
|
|
116
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
117
|
+
...(params.timeoutMs !== undefined ? { timeout: params.timeoutMs } : {}),
|
|
118
|
+
});
|
|
119
|
+
return { stdout, stderr, exitCode: 0 };
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
const e = err;
|
|
123
|
+
// `code` is a number only for a completed exec that exited non-zero.
|
|
124
|
+
// Anything else (ENOENT for a missing docker binary, the container
|
|
125
|
+
// having been removed mid-exec, etc.) is an infra failure, not a
|
|
126
|
+
// command result — rethrow so callers don't mistake it for "the
|
|
127
|
+
// command ran and exited 1".
|
|
128
|
+
if (typeof e.code !== "number") {
|
|
129
|
+
throw new Error(`failed to exec in sandbox container: ${errorMessage(err)}`);
|
|
130
|
+
}
|
|
131
|
+
return { stdout: e.stdout ?? "", stderr: e.stderr ?? "", exitCode: e.code };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export async function copyToSandboxContainer(params) {
|
|
135
|
+
const src = params.contents ? `${params.hostPath}/.` : params.hostPath;
|
|
136
|
+
await dockerCp(params.dockerBin, src, `${params.containerName}:${params.containerPath}`, "into");
|
|
137
|
+
}
|
|
138
|
+
export async function copyFromSandboxContainer(params) {
|
|
139
|
+
const src = params.contents
|
|
140
|
+
? `${params.containerName}:${params.containerPath}/.`
|
|
141
|
+
: `${params.containerName}:${params.containerPath}`;
|
|
142
|
+
await dockerCp(params.dockerBin, src, params.hostPath, "from");
|
|
143
|
+
}
|
|
144
|
+
async function dockerCp(dockerBin, src, dest, direction) {
|
|
145
|
+
try {
|
|
146
|
+
await execFileAsync(dockerBin ?? "docker", ["cp", src, dest]);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
throw new Error(`failed to copy ${direction} sandbox: ${errorMessage(err)}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type SandboxDeskRole = "artifacts" | "envelope" | "checkout" | "grant" | "work";
|
|
2
|
+
export type SandboxDeskBinding = {
|
|
3
|
+
role: SandboxDeskRole;
|
|
4
|
+
hostPath: string;
|
|
5
|
+
containerPath: string;
|
|
6
|
+
shape: "directory" | "file" | "mirror";
|
|
7
|
+
copyIn: boolean;
|
|
8
|
+
copyOut: boolean;
|
|
9
|
+
optional: boolean;
|
|
10
|
+
};
|
|
11
|
+
export type SandboxDesk = {
|
|
12
|
+
bindings: readonly SandboxDeskBinding[];
|
|
13
|
+
};
|
|
14
|
+
export declare function buildSandboxDesk(input: {
|
|
15
|
+
attemptDir: string;
|
|
16
|
+
checkoutRoot?: string;
|
|
17
|
+
grantedPaths?: readonly string[];
|
|
18
|
+
}): SandboxDesk;
|
|
19
|
+
export declare function populateSandboxDesk(params: {
|
|
20
|
+
containerName: string;
|
|
21
|
+
dockerBin?: string;
|
|
22
|
+
desk: SandboxDesk;
|
|
23
|
+
}): Promise<void>;
|
|
24
|
+
export declare function syncSandboxDesk(params: {
|
|
25
|
+
containerName: string;
|
|
26
|
+
dockerBin?: string;
|
|
27
|
+
desk: SandboxDesk;
|
|
28
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { access, mkdir, rename, rm, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { copyFromSandboxContainer, copyToSandboxContainer, execInSandboxContainer, SANDBOX_MOUNT_PATH, } from "./sandboxContainer.js";
|
|
5
|
+
import { shellQuote } from "./sandboxFiles.js";
|
|
6
|
+
export function buildSandboxDesk(input) {
|
|
7
|
+
const attemptDir = input.attemptDir;
|
|
8
|
+
const bindings = [
|
|
9
|
+
{
|
|
10
|
+
role: "artifacts",
|
|
11
|
+
hostPath: path.join(attemptDir, "artifacts"),
|
|
12
|
+
containerPath: `${SANDBOX_MOUNT_PATH}/artifacts`,
|
|
13
|
+
shape: "directory",
|
|
14
|
+
copyIn: true,
|
|
15
|
+
copyOut: false,
|
|
16
|
+
optional: true,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
role: "envelope",
|
|
20
|
+
hostPath: path.join(attemptDir, "envelope.json"),
|
|
21
|
+
containerPath: `${SANDBOX_MOUNT_PATH}/envelope.json`,
|
|
22
|
+
shape: "file",
|
|
23
|
+
copyIn: true,
|
|
24
|
+
copyOut: false,
|
|
25
|
+
optional: true,
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
const checkout = input.checkoutRoot?.trim();
|
|
29
|
+
if (checkout) {
|
|
30
|
+
bindings.push({
|
|
31
|
+
role: "checkout",
|
|
32
|
+
hostPath: checkout,
|
|
33
|
+
containerPath: `${SANDBOX_MOUNT_PATH}/checkout`,
|
|
34
|
+
shape: "directory",
|
|
35
|
+
copyIn: true,
|
|
36
|
+
copyOut: true,
|
|
37
|
+
optional: false,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const grants = input.grantedPaths ?? [];
|
|
41
|
+
for (let index = 0; index < grants.length; index += 1) {
|
|
42
|
+
const hostPath = grants[index];
|
|
43
|
+
bindings.push({
|
|
44
|
+
role: "grant",
|
|
45
|
+
hostPath,
|
|
46
|
+
containerPath: `${SANDBOX_MOUNT_PATH}/grants/${grantSegment(index, hostPath)}`,
|
|
47
|
+
shape: "mirror",
|
|
48
|
+
copyIn: true,
|
|
49
|
+
copyOut: true,
|
|
50
|
+
optional: false,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
bindings.push({
|
|
54
|
+
role: "work",
|
|
55
|
+
hostPath: path.join(attemptDir, "work"),
|
|
56
|
+
containerPath: `${SANDBOX_MOUNT_PATH}/work`,
|
|
57
|
+
shape: "directory",
|
|
58
|
+
copyIn: false,
|
|
59
|
+
copyOut: true,
|
|
60
|
+
optional: true,
|
|
61
|
+
});
|
|
62
|
+
return { bindings };
|
|
63
|
+
}
|
|
64
|
+
function grantSegment(index, hostPath) {
|
|
65
|
+
const base = path.basename(hostPath).replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
66
|
+
const safe = base.length > 0 && base !== "." && base !== ".." ? base : "path";
|
|
67
|
+
return `${index}-${safe}`;
|
|
68
|
+
}
|
|
69
|
+
async function pathExists(target) {
|
|
70
|
+
try {
|
|
71
|
+
await access(target);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export async function populateSandboxDesk(params) {
|
|
79
|
+
const dirs = new Set([
|
|
80
|
+
`${SANDBOX_MOUNT_PATH}/artifacts`,
|
|
81
|
+
`${SANDBOX_MOUNT_PATH}/work`,
|
|
82
|
+
`${SANDBOX_MOUNT_PATH}/grants`,
|
|
83
|
+
]);
|
|
84
|
+
for (const binding of params.desk.bindings) {
|
|
85
|
+
if (binding.role === "checkout" || binding.shape === "mirror") {
|
|
86
|
+
dirs.add(binding.containerPath);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const made = await execInSandboxContainer({
|
|
90
|
+
containerName: params.containerName,
|
|
91
|
+
dockerBin: params.dockerBin,
|
|
92
|
+
command: `mkdir -p -- ${[...dirs].map(shellQuote).join(" ")}`,
|
|
93
|
+
});
|
|
94
|
+
if (made.exitCode !== 0) {
|
|
95
|
+
throw new Error(`failed to prepare sandbox desk: ${made.stderr.trim() || made.stdout.trim() || `exit ${made.exitCode}`}`);
|
|
96
|
+
}
|
|
97
|
+
for (const binding of params.desk.bindings) {
|
|
98
|
+
if (!binding.copyIn)
|
|
99
|
+
continue;
|
|
100
|
+
if (!(await pathExists(binding.hostPath))) {
|
|
101
|
+
if (binding.optional)
|
|
102
|
+
continue;
|
|
103
|
+
throw new Error(`sandbox desk source missing: ${binding.hostPath}`);
|
|
104
|
+
}
|
|
105
|
+
await copyBindingIn(params, binding);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export async function syncSandboxDesk(params) {
|
|
109
|
+
for (const binding of params.desk.bindings) {
|
|
110
|
+
if (!binding.copyOut)
|
|
111
|
+
continue;
|
|
112
|
+
await copyBindingOut(params, binding);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function copyBindingIn(params, binding) {
|
|
116
|
+
if (binding.shape === "file") {
|
|
117
|
+
await copyToSandboxContainer({
|
|
118
|
+
containerName: params.containerName,
|
|
119
|
+
dockerBin: params.dockerBin,
|
|
120
|
+
hostPath: binding.hostPath,
|
|
121
|
+
containerPath: binding.containerPath,
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (binding.shape === "directory") {
|
|
126
|
+
await copyToSandboxContainer({
|
|
127
|
+
containerName: params.containerName,
|
|
128
|
+
dockerBin: params.dockerBin,
|
|
129
|
+
hostPath: binding.hostPath,
|
|
130
|
+
containerPath: binding.containerPath,
|
|
131
|
+
contents: true,
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const info = await stat(binding.hostPath);
|
|
136
|
+
if (info.isDirectory()) {
|
|
137
|
+
await copyToSandboxContainer({
|
|
138
|
+
containerName: params.containerName,
|
|
139
|
+
dockerBin: params.dockerBin,
|
|
140
|
+
hostPath: binding.hostPath,
|
|
141
|
+
containerPath: binding.containerPath,
|
|
142
|
+
contents: true,
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
await copyToSandboxContainer({
|
|
147
|
+
containerName: params.containerName,
|
|
148
|
+
dockerBin: params.dockerBin,
|
|
149
|
+
hostPath: binding.hostPath,
|
|
150
|
+
containerPath: path.posix.join(binding.containerPath, path.basename(binding.hostPath)),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
async function copyBindingOut(params, binding) {
|
|
154
|
+
if (binding.role === "checkout" || binding.role === "work") {
|
|
155
|
+
await mirrorDirectoryFromContainer(params, binding.containerPath, binding.hostPath);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (binding.shape === "directory") {
|
|
159
|
+
await mkdir(binding.hostPath, { recursive: true });
|
|
160
|
+
await copyFromSandboxContainer({
|
|
161
|
+
containerName: params.containerName,
|
|
162
|
+
dockerBin: params.dockerBin,
|
|
163
|
+
containerPath: binding.containerPath,
|
|
164
|
+
hostPath: binding.hostPath,
|
|
165
|
+
contents: true,
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const info = await stat(binding.hostPath);
|
|
170
|
+
if (info.isDirectory()) {
|
|
171
|
+
await mkdir(binding.hostPath, { recursive: true });
|
|
172
|
+
await copyFromSandboxContainer({
|
|
173
|
+
containerName: params.containerName,
|
|
174
|
+
dockerBin: params.dockerBin,
|
|
175
|
+
containerPath: binding.containerPath,
|
|
176
|
+
hostPath: binding.hostPath,
|
|
177
|
+
contents: true,
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
await mkdir(path.dirname(binding.hostPath), { recursive: true });
|
|
182
|
+
await copyFromSandboxContainer({
|
|
183
|
+
containerName: params.containerName,
|
|
184
|
+
dockerBin: params.dockerBin,
|
|
185
|
+
containerPath: path.posix.join(binding.containerPath, path.basename(binding.hostPath)),
|
|
186
|
+
hostPath: binding.hostPath,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
async function mirrorDirectoryFromContainer(params, containerPath, hostPath) {
|
|
190
|
+
let probe;
|
|
191
|
+
try {
|
|
192
|
+
probe = await execInSandboxContainer({
|
|
193
|
+
containerName: params.containerName,
|
|
194
|
+
dockerBin: params.dockerBin,
|
|
195
|
+
command: `test -d ${shellQuote(containerPath)}`,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
throw new Error(`sandbox desk directory unavailable, not replacing ${hostPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
200
|
+
}
|
|
201
|
+
if (probe.exitCode !== 0) {
|
|
202
|
+
throw new Error(`sandbox desk directory missing, not replacing ${hostPath}`);
|
|
203
|
+
}
|
|
204
|
+
await mkdir(path.dirname(hostPath), { recursive: true });
|
|
205
|
+
const staging = path.join(path.dirname(hostPath), `.${path.basename(hostPath)}.sf-sync-${randomBytes(4).toString("hex")}`);
|
|
206
|
+
await mkdir(staging);
|
|
207
|
+
try {
|
|
208
|
+
await copyFromSandboxContainer({
|
|
209
|
+
containerName: params.containerName,
|
|
210
|
+
dockerBin: params.dockerBin,
|
|
211
|
+
containerPath,
|
|
212
|
+
hostPath: staging,
|
|
213
|
+
contents: true,
|
|
214
|
+
});
|
|
215
|
+
await replaceDirectory(hostPath, staging);
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
await rm(staging, { recursive: true, force: true });
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async function replaceDirectory(hostPath, staging) {
|
|
223
|
+
const backup = path.join(path.dirname(hostPath), `.${path.basename(hostPath)}.sf-sync-bak-${randomBytes(4).toString("hex")}`);
|
|
224
|
+
let hostMoved = false;
|
|
225
|
+
try {
|
|
226
|
+
if (await pathExists(hostPath)) {
|
|
227
|
+
await rename(hostPath, backup);
|
|
228
|
+
hostMoved = true;
|
|
229
|
+
}
|
|
230
|
+
await rename(staging, hostPath);
|
|
231
|
+
hostMoved = false;
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
if (hostMoved && !(await pathExists(hostPath)) && (await pathExists(backup))) {
|
|
235
|
+
await rename(backup, hostPath);
|
|
236
|
+
}
|
|
237
|
+
throw err;
|
|
238
|
+
}
|
|
239
|
+
if (await pathExists(backup)) {
|
|
240
|
+
await rm(backup, { recursive: true, force: true });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type SandboxExecResult } from "./sandboxContainer.js";
|
|
2
|
+
export declare class SandboxPathError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
export declare function shellQuote(value: string): string;
|
|
6
|
+
/** Resolve a tool path onto the sandbox mount. Host absolutes and `..` escapes are rejected. */
|
|
7
|
+
export declare function confineSandboxPath(inputPath: string): string;
|
|
8
|
+
export type SandboxFileTarget = {
|
|
9
|
+
containerName: string;
|
|
10
|
+
dockerBin?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function readSandboxFile(target: SandboxFileTarget, inputPath: string): Promise<string>;
|
|
13
|
+
export declare function writeSandboxFile(target: SandboxFileTarget, inputPath: string, content: string): Promise<void>;
|
|
14
|
+
export declare function editSandboxFile(target: SandboxFileTarget, inputPath: string, edits: readonly {
|
|
15
|
+
oldText: string;
|
|
16
|
+
newText: string;
|
|
17
|
+
}[]): Promise<void>;
|
|
18
|
+
export declare function sliceSandboxLines(text: string, offset?: number, limit?: number): string;
|
|
19
|
+
export declare function formatSandboxBashResult(result: SandboxExecResult): string;
|
|
20
|
+
export declare function runSandboxBash(target: SandboxFileTarget, command: string, timeoutMs?: number): Promise<string>;
|