@foldrun/core 0.3.0 → 0.4.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 +27 -10
- package/dist/src/approvals.d.ts +104 -1
- package/dist/src/approvals.js +192 -2
- package/dist/src/budget.d.ts +32 -0
- package/dist/src/budget.js +177 -0
- package/dist/src/catalog.js +2 -1
- package/dist/src/confine.js +6 -1
- package/dist/src/egress.d.ts +19 -2
- package/dist/src/egress.js +2 -1
- package/dist/src/evals.d.ts +4 -0
- package/dist/src/evals.js +20 -11
- package/dist/src/flow-lint.d.ts +4 -0
- package/dist/src/flow-lint.js +108 -0
- package/dist/src/host-env.d.ts +8 -0
- package/dist/src/host-env.js +48 -0
- package/dist/src/json-schema.d.ts +12 -0
- package/dist/src/json-schema.js +202 -0
- package/dist/src/library.js +13 -1
- package/dist/src/notify.d.ts +79 -1
- package/dist/src/notify.js +243 -9
- package/dist/src/paths.d.ts +5 -0
- package/dist/src/paths.js +32 -0
- package/dist/src/platform.d.ts +102 -2
- package/dist/src/platform.js +18 -2
- package/dist/src/provider-health.d.ts +55 -0
- package/dist/src/provider-health.js +232 -0
- package/dist/src/run-container.d.ts +57 -4
- package/dist/src/run-container.js +147 -41
- package/dist/src/runner.d.ts +39 -3
- package/dist/src/runner.js +422 -73
- package/dist/src/script-tools.d.ts +12 -0
- package/dist/src/script-tools.js +10 -2
- package/dist/src/secret-health.d.ts +59 -0
- package/dist/src/secret-health.js +155 -0
- package/dist/src/secrets.d.ts +19 -1
- package/dist/src/secrets.js +44 -12
- package/dist/src/starter.js +3 -0
- package/dist/src/step-exec.d.ts +47 -1
- package/dist/src/step-exec.js +155 -60
- package/dist/src/store.d.ts +276 -3
- package/dist/src/store.js +552 -26
- package/dist/src/tar.js +2 -1
- package/dist/src/test-mode.d.ts +111 -0
- package/dist/src/test-mode.js +285 -0
- package/dist/src/tool-test.js +48 -21
- package/dist/src/translator.js +2 -1
- package/dist/src/trigger-log.d.ts +43 -0
- package/dist/src/trigger-log.js +122 -0
- package/dist/src/triggers.d.ts +20 -0
- package/dist/src/triggers.js +91 -10
- package/dist/src/webhook.d.ts +15 -3
- package/dist/src/webhook.js +11 -11
- package/package.json +4 -4
- package/src/approvals.ts +253 -2
- package/src/budget.ts +178 -0
- package/src/catalog.ts +2 -1
- package/src/confine.ts +6 -1
- package/src/egress.ts +20 -4
- package/src/evals.ts +25 -9
- package/src/flow-lint.ts +118 -0
- package/src/host-env.ts +51 -0
- package/src/json-schema.ts +184 -0
- package/src/library.ts +13 -1
- package/src/notify.ts +272 -11
- package/src/paths.ts +29 -0
- package/src/platform.ts +107 -3
- package/src/provider-health.ts +274 -0
- package/src/run-container.ts +175 -36
- package/src/runner.ts +435 -65
- package/src/script-tools.ts +18 -2
- package/src/secret-health.ts +185 -0
- package/src/secrets.ts +44 -10
- package/src/starter.ts +3 -0
- package/src/step-exec.ts +128 -13
- package/src/storage.ts +8 -2
- package/src/store.ts +746 -25
- package/src/tar.ts +2 -1
- package/src/test-mode.ts +311 -0
- package/src/tool-test.ts +49 -20
- package/src/translator.ts +2 -1
- package/src/trigger-log.ts +159 -0
- package/src/triggers.ts +92 -10
- package/src/webhook.ts +17 -11
package/src/run-container.ts
CHANGED
|
@@ -25,6 +25,7 @@ import crypto from "node:crypto";
|
|
|
25
25
|
import { spawn, spawnSync } from "node:child_process";
|
|
26
26
|
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
|
27
27
|
import { isPlatformPath, type ApiSpec, type Effort } from "./store.ts";
|
|
28
|
+
import { divertedSummary, isDivertedPath } from "./test-mode.ts";
|
|
28
29
|
import type { EventExtra } from "./step-exec.ts";
|
|
29
30
|
import { isFileValue, fileContent } from "./secrets.ts";
|
|
30
31
|
import { safeTenantSegment, type RuntimeSpec } from "./runtime.ts";
|
|
@@ -61,10 +62,14 @@ export interface ContainerStepInput {
|
|
|
61
62
|
/** The step's spend ceiling and the model's per-token price — see
|
|
62
63
|
* step-exec.ts. Values, so the driver can enforce them in the pod. */
|
|
63
64
|
budgetUsd?: number | null;
|
|
65
|
+
budgetNote?: string;
|
|
64
66
|
price?: { input: number; output: number } | null;
|
|
65
67
|
verify?: string;
|
|
66
68
|
/** `output: json` — checked inside, where the reply is. */
|
|
67
69
|
output?: "json";
|
|
70
|
+
/** `schema:` and `max_turns:`, resolved host-side — see ExecOptions. */
|
|
71
|
+
schema?: Record<string, unknown> | boolean;
|
|
72
|
+
maxTurns?: number;
|
|
68
73
|
/** tools: [search] — the directories to search, as paths INSIDE the
|
|
69
74
|
* container (/workspace/…, /library/…). */
|
|
70
75
|
search?: SearchRoot[];
|
|
@@ -215,10 +220,45 @@ export function hashTree(dir: string): Record<string, string> {
|
|
|
215
220
|
return out;
|
|
216
221
|
}
|
|
217
222
|
|
|
223
|
+
const sha = (b: Buffer) => crypto.createHash("sha256").update(b).digest("hex");
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Both sides changed a file the step was handed. When each is the handed-in
|
|
227
|
+
* content with lines added on the end — a ledger, history.md, a .jsonl — the
|
|
228
|
+
* answer is the base, then the host's additions, then the step's. Only the
|
|
229
|
+
* base's hash may be known, so the base is found as the longest shared
|
|
230
|
+
* leading run of whole lines whose hash matches. Anything else is null: a
|
|
231
|
+
* real conflict, which the caller must not settle by dropping one side.
|
|
232
|
+
*/
|
|
233
|
+
export function mergeAppends(was: Buffer | string | null, host: Buffer, next: Buffer): Buffer | null {
|
|
234
|
+
// Both sides created the same new file: no shared base to append to, and
|
|
235
|
+
// joining two JSON documents is corruption, not a merge.
|
|
236
|
+
if (was === null) return null;
|
|
237
|
+
const h = host.toString("utf8");
|
|
238
|
+
const n = next.toString("utf8");
|
|
239
|
+
let common = 0;
|
|
240
|
+
const max = Math.min(h.length, n.length);
|
|
241
|
+
while (common < max && h.charCodeAt(common) === n.charCodeAt(common)) common++;
|
|
242
|
+
for (let end = common; end >= 0; end--) {
|
|
243
|
+
if (end > 0 && h[end - 1] !== "\n") continue;
|
|
244
|
+
const prefix = h.slice(0, end);
|
|
245
|
+
const matches =
|
|
246
|
+
typeof was === "string" ? sha(Buffer.from(prefix, "utf8")) === was : Buffer.from(prefix, "utf8").equals(was);
|
|
247
|
+
if (!matches) continue;
|
|
248
|
+
const hostAdded = h.slice(end);
|
|
249
|
+
const stepAdded = n.slice(end);
|
|
250
|
+
const joined = hostAdded && !hostAdded.endsWith("\n") ? hostAdded + "\n" : hostAdded;
|
|
251
|
+
return Buffer.from(prefix + joined + stepAdded, "utf8");
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
218
256
|
export function applyContainerChanges(
|
|
219
257
|
hostWs: string,
|
|
220
258
|
containerWs: string,
|
|
221
259
|
baseline?: string | Record<string, string>,
|
|
260
|
+
note?: (message: string) => void,
|
|
261
|
+
divert?: WriteDivert,
|
|
222
262
|
): string[] {
|
|
223
263
|
const applied: string[] = [];
|
|
224
264
|
const walk = (dir: string) => {
|
|
@@ -237,18 +277,60 @@ export function applyContainerChanges(
|
|
|
237
277
|
continue;
|
|
238
278
|
}
|
|
239
279
|
if (!allowedBack(rel)) continue;
|
|
240
|
-
|
|
241
|
-
//
|
|
242
|
-
//
|
|
280
|
+
let next: Buffer = fs.readFileSync(abs);
|
|
281
|
+
// What the step was handed: its bytes (a staging dir) or its hash.
|
|
282
|
+
// null when the file did not exist then; undefined when no baseline.
|
|
283
|
+
let was: Buffer | string | null | undefined;
|
|
243
284
|
if (typeof baseline === "string") {
|
|
244
|
-
const
|
|
245
|
-
|
|
285
|
+
const p = path.join(baseline, rel);
|
|
286
|
+
was = fs.existsSync(p) ? fs.readFileSync(p) : null;
|
|
246
287
|
} else if (baseline) {
|
|
247
|
-
|
|
248
|
-
if (was && was === crypto.createHash("sha256").update(next).digest("hex")) continue;
|
|
288
|
+
was = baseline[rel.replaceAll("\\", "/")] ?? null;
|
|
249
289
|
}
|
|
290
|
+
const same = (b: Buffer) =>
|
|
291
|
+
was === null || was === undefined ? false : typeof was === "string" ? sha(b) === was : b.equals(was);
|
|
292
|
+
// Untouched by this step: whatever the host says now is more current
|
|
293
|
+
// than what we handed in, including a change made while it ran.
|
|
294
|
+
if (same(next)) continue;
|
|
250
295
|
const target = path.join(hostWs, rel);
|
|
251
|
-
|
|
296
|
+
const hostNow = fs.existsSync(target) ? fs.readFileSync(target) : null;
|
|
297
|
+
if (hostNow && hostNow.equals(next)) continue;
|
|
298
|
+
// The host moved too since the step was handed its copy — another run
|
|
299
|
+
// or a person wrote it. Writing the step's copy over it erased the
|
|
300
|
+
// other run's ledger rows on 2026-09-14. Merge appends; keep both
|
|
301
|
+
// sides of anything else.
|
|
302
|
+
const hostMoved = was !== undefined && hostNow !== null && !same(hostNow);
|
|
303
|
+
if (hostMoved) {
|
|
304
|
+
const merged = mergeAppends(was ?? null, hostNow, next);
|
|
305
|
+
if (merged) {
|
|
306
|
+
next = merged;
|
|
307
|
+
} else {
|
|
308
|
+
const ext = path.extname(rel);
|
|
309
|
+
const stem = rel.slice(0, rel.length - ext.length);
|
|
310
|
+
const conflictRel = `${stem}.conflict-${Date.now().toString(36)}${ext}`;
|
|
311
|
+
const conflictAbs = path.join(hostWs, conflictRel);
|
|
312
|
+
fs.mkdirSync(path.dirname(conflictAbs), { recursive: true });
|
|
313
|
+
fs.writeFileSync(conflictAbs, next);
|
|
314
|
+
applied.push(conflictRel);
|
|
315
|
+
note?.(
|
|
316
|
+
`write-back conflict on ${rel.replaceAll("\\", "/")}: it changed on the workspace while this step ran, ` +
|
|
317
|
+
`and not only by appending — kept the workspace's version; this step's version is ${conflictRel.replaceAll("\\", "/")}`,
|
|
318
|
+
);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// A test run's state/ and storage/ writes land under the run, not on
|
|
323
|
+
// the workspace: the step ran against the real files and its output
|
|
324
|
+
// is kept where a person can read it, but the next real run starts
|
|
325
|
+
// from state a test never touched. Decided after the merge so the
|
|
326
|
+
// diverted copy is what WOULD have been written, conflicts included.
|
|
327
|
+
if (divert && isDivertedPath(rel)) {
|
|
328
|
+
const aside = path.join(divert.to, rel);
|
|
329
|
+
fs.mkdirSync(path.dirname(aside), { recursive: true });
|
|
330
|
+
fs.writeFileSync(aside, next);
|
|
331
|
+
divert.note(rel.replaceAll("\\", "/"), divertedSummary(rel.replaceAll("\\", "/"), hostNow, next));
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
252
334
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
253
335
|
fs.writeFileSync(target, next);
|
|
254
336
|
applied.push(rel);
|
|
@@ -378,9 +460,9 @@ try {
|
|
|
378
460
|
|
|
379
461
|
// API and script tools, rebuilt in here from their specs. Secrets were
|
|
380
462
|
// substituted into API headers before the input crossed the boundary, so
|
|
381
|
-
// buildApiTools gets an already-resolved environment.
|
|
382
|
-
//
|
|
383
|
-
// the
|
|
463
|
+
// buildApiTools gets an already-resolved environment. verify: gets the
|
|
464
|
+
// container's own environment, whole: it is the boundary, assembled on
|
|
465
|
+
// the host from an allowlist, and the declared secrets are in it.
|
|
384
466
|
const api = buildApiTools("", input.apis, undefined, { env, missing: [] });
|
|
385
467
|
const script = buildScriptTools(
|
|
386
468
|
agentDir,
|
|
@@ -425,10 +507,13 @@ try {
|
|
|
425
507
|
env,
|
|
426
508
|
timeoutSec: input.timeoutSec,
|
|
427
509
|
budgetUsd: input.budgetUsd,
|
|
510
|
+
budgetNote: input.budgetNote,
|
|
428
511
|
price: input.price,
|
|
429
512
|
verify: input.verify,
|
|
430
|
-
verifyEnv:
|
|
513
|
+
verifyEnv: env,
|
|
431
514
|
output: input.output,
|
|
515
|
+
schema: input.schema,
|
|
516
|
+
maxTurns: input.maxTurns,
|
|
432
517
|
// The container is the boundary; the SDK's own bash sandbox here would
|
|
433
518
|
// only block declared network use (SSH, curl) for no added safety.
|
|
434
519
|
sandboxBash: false,
|
|
@@ -536,8 +621,17 @@ export function runnerImageTag(): string {
|
|
|
536
621
|
* has not been built before. `npm pack` of the installed core, so the image
|
|
537
622
|
* runs the same bytes the server does.
|
|
538
623
|
*/
|
|
539
|
-
|
|
540
|
-
|
|
624
|
+
/**
|
|
625
|
+
* `platform`: build for another architecture — "linux/arm64" on the amd64
|
|
626
|
+
* box, through QEMU — and name the result with the arch appended so it never
|
|
627
|
+
* shadows the native image in the daemon. Only the publisher asks for this;
|
|
628
|
+
* a step always runs the native image. Needs binfmt registered on the host
|
|
629
|
+
* (tonistiigi/binfmt); the publisher does that itself, every time, because
|
|
630
|
+
* the registration does not survive a reboot.
|
|
631
|
+
*/
|
|
632
|
+
export function ensureRunnerImage(opts: { platform?: string } = {}): { tag: string; log: string[] } {
|
|
633
|
+
const arch = opts.platform ? `-${opts.platform.split("/").pop()}` : "";
|
|
634
|
+
const tag = process.env.FOLDRUN_RUNNER_IMAGE ?? runnerImageTag() + arch;
|
|
541
635
|
const log: string[] = [];
|
|
542
636
|
if (process.env.FOLDRUN_RUNNER_IMAGE) return { tag, log };
|
|
543
637
|
|
|
@@ -556,7 +650,8 @@ export function ensureRunnerImage(): { tag: string; log: string[] } {
|
|
|
556
650
|
fs.writeFileSync(path.join(build, "entry.sh"), ENTRY);
|
|
557
651
|
fs.writeFileSync(path.join(build, "Dockerfile"), DOCKERFILE);
|
|
558
652
|
log.push(`building runner image ${tag} (first run only)`);
|
|
559
|
-
const
|
|
653
|
+
const args = ["build", ...(opts.platform ? ["--platform", opts.platform] : []), "-t", tag, build];
|
|
654
|
+
const out = spawnSync(cli(), args, { encoding: "utf8" });
|
|
560
655
|
if (out.status !== 0) {
|
|
561
656
|
throw new Error(`runner image build failed:\n${(out.stderr || out.stdout).slice(-2000)}`);
|
|
562
657
|
}
|
|
@@ -643,6 +738,8 @@ function ensureRunNetwork(): string {
|
|
|
643
738
|
export interface RunInContainerArgs {
|
|
644
739
|
workspaceRoot: string;
|
|
645
740
|
libraryRoot: string;
|
|
741
|
+
/** The platform's gallery, staged under /library BEFORE the account library so the account's copies win. */
|
|
742
|
+
galleryRoot?: string;
|
|
646
743
|
/** The account this step belongs to — the key for its dependency cache.
|
|
647
744
|
* Optional so an embedder that has no tenancy simply gets no cache. */
|
|
648
745
|
tenant?: string;
|
|
@@ -666,6 +763,66 @@ export interface RunInContainerArgs {
|
|
|
666
763
|
* (consumed 0), and before each output line it applies. The runner
|
|
667
764
|
* records these on the step, so the next driver knows where to attach. */
|
|
668
765
|
checkpoint?: (ref: string, consumed: number) => void;
|
|
766
|
+
/** A test run: where state/ and storage/ changes go instead of the
|
|
767
|
+
* workspace, and who to tell about each. See test-mode.ts. */
|
|
768
|
+
divert?: WriteDivert;
|
|
769
|
+
/** The run is a test run. An isolated executor labels the sandbox so the
|
|
770
|
+
* cluster can cut its network down to the egress proxy: a script tool
|
|
771
|
+
* calls the network directly, not through the proxy where the test-mode
|
|
772
|
+
* policy lives, so without this a test run's script could still send. */
|
|
773
|
+
test?: boolean;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/** What a test run does with a write it must not apply. */
|
|
777
|
+
export interface WriteDivert {
|
|
778
|
+
/** The directory the diverted files are written under, by their
|
|
779
|
+
* workspace-relative path: runs/<id>/test-writes/. */
|
|
780
|
+
to: string;
|
|
781
|
+
note: (rel: string, summary: string) => void;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* What crosses into the container as its environment, and how.
|
|
786
|
+
*
|
|
787
|
+
* A docker --env-file is one `KEY=value` per line and cannot carry a
|
|
788
|
+
* newline. @file secrets are multi-line by nature (a PEM key, a cert), so
|
|
789
|
+
* they are staged as files NOW, into the copied-in workspace, and the env
|
|
790
|
+
* carries the container path — the one place a path is stable across the
|
|
791
|
+
* boundary. A plain secret whose value happens to hold a newline gets the
|
|
792
|
+
* same treatment, and the trace says so: it used to be dropped from the
|
|
793
|
+
* env file without a word, and the step failed later, elsewhere, on a
|
|
794
|
+
* variable that read as unset. Everything else goes through the env file.
|
|
795
|
+
*/
|
|
796
|
+
export function stageContainerEnv(
|
|
797
|
+
env: Record<string, string>,
|
|
798
|
+
wsIn: string,
|
|
799
|
+
agentRel: string,
|
|
800
|
+
emit: (type: "info" | "error", text: string) => void,
|
|
801
|
+
): Record<string, string> {
|
|
802
|
+
const containerEnv: Record<string, string> = {};
|
|
803
|
+
const stage = (k: string, content: string) => {
|
|
804
|
+
const rel = path.join(agentRel, ".secret-files", k.toLowerCase());
|
|
805
|
+
const abs = path.join(wsIn, rel);
|
|
806
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true, mode: 0o700 });
|
|
807
|
+
fs.writeFileSync(abs, content, { mode: 0o600 });
|
|
808
|
+
return `/workspace/${rel.replaceAll("\\", "/")}`;
|
|
809
|
+
};
|
|
810
|
+
for (const [k, v] of Object.entries(env)) {
|
|
811
|
+
if (typeof v !== "string") continue;
|
|
812
|
+
if (isFileValue(v)) {
|
|
813
|
+
containerEnv[k] = stage(k, fileContent(v));
|
|
814
|
+
} else if (/[\r\n]/.test(v)) {
|
|
815
|
+
containerEnv[k] = stage(k, v);
|
|
816
|
+
emit(
|
|
817
|
+
"info",
|
|
818
|
+
`${k} holds a line break and cannot cross as an environment variable — ` +
|
|
819
|
+
`passed as a file instead; $${k} is its path in the sandbox`,
|
|
820
|
+
);
|
|
821
|
+
} else {
|
|
822
|
+
containerEnv[k] = v;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return containerEnv;
|
|
669
826
|
}
|
|
670
827
|
|
|
671
828
|
/** The limits a size class reserves. Large is the install's configured
|
|
@@ -759,6 +916,7 @@ export async function runStepInContainer(args: RunInContainerArgs): Promise<Cont
|
|
|
759
916
|
filter: (src) => !isPlatformPath(path.relative(args.workspaceRoot, src)),
|
|
760
917
|
});
|
|
761
918
|
const libIn = path.join(staging, "library");
|
|
919
|
+
if (args.galleryRoot && fs.existsSync(args.galleryRoot)) fs.cpSync(args.galleryRoot, libIn, { recursive: true });
|
|
762
920
|
if (fs.existsSync(args.libraryRoot)) fs.cpSync(args.libraryRoot, libIn, { recursive: true });
|
|
763
921
|
else fs.mkdirSync(libIn, { recursive: true });
|
|
764
922
|
|
|
@@ -766,26 +924,7 @@ export async function runStepInContainer(args: RunInContainerArgs): Promise<Cont
|
|
|
766
924
|
fs.mkdirSync(jobIn);
|
|
767
925
|
fs.writeFileSync(path.join(jobIn, "input.json"), JSON.stringify(args.input, null, 2));
|
|
768
926
|
|
|
769
|
-
|
|
770
|
-
// --env-file cannot carry a newline. So they are staged as files *now*,
|
|
771
|
-
// into the copied-in workspace, and the env carries the container path —
|
|
772
|
-
// the one place a path is stable across the boundary. Everything else
|
|
773
|
-
// goes through the env file. (Without this the key silently vanished:
|
|
774
|
-
// the old filter dropped any value with a newline, and ssh got an empty
|
|
775
|
-
// -i path.)
|
|
776
|
-
const containerEnv: Record<string, string> = {};
|
|
777
|
-
for (const [k, v] of Object.entries(args.env)) {
|
|
778
|
-
if (typeof v !== "string") continue;
|
|
779
|
-
if (isFileValue(v)) {
|
|
780
|
-
const rel = path.join(args.input.agentRel, ".secret-files", k.toLowerCase());
|
|
781
|
-
const abs = path.join(wsIn, rel);
|
|
782
|
-
fs.mkdirSync(path.dirname(abs), { recursive: true, mode: 0o700 });
|
|
783
|
-
fs.writeFileSync(abs, fileContent(v), { mode: 0o600 });
|
|
784
|
-
containerEnv[k] = `/workspace/${rel.replaceAll("\\", "/")}`;
|
|
785
|
-
} else if (!v.includes("\n")) {
|
|
786
|
-
containerEnv[k] = v;
|
|
787
|
-
}
|
|
788
|
-
}
|
|
927
|
+
const containerEnv = stageContainerEnv(args.env, wsIn, args.input.agentRel, args.emit);
|
|
789
928
|
|
|
790
929
|
const envFile = path.join(staging, "env");
|
|
791
930
|
fs.writeFileSync(
|
|
@@ -903,7 +1042,7 @@ export async function runStepInContainer(args: RunInContainerArgs): Promise<Cont
|
|
|
903
1042
|
fs.mkdirSync(wsOut);
|
|
904
1043
|
const back = spawnSync(cli(), ["cp", `${containerId}:/workspace/.`, wsOut], { encoding: "utf8" });
|
|
905
1044
|
if (back.status === 0) {
|
|
906
|
-
applyContainerChanges(args.workspaceRoot, wsOut, wsIn);
|
|
1045
|
+
applyContainerChanges(args.workspaceRoot, wsOut, wsIn, (m) => args.emit("error", m), args.divert);
|
|
907
1046
|
} else {
|
|
908
1047
|
args.emit("error", `copy-out failed — the step's file changes were lost:\n${back.stderr.slice(0, 500)}`);
|
|
909
1048
|
}
|